- internal/logging: LOG_LEVEL/LOG_FORMAT parsing and slog handler - request_id middleware (X-Request-ID or generated, echoed) + debug access log - httpx registers errors; middleware logs internal causes with request_id - health excluded, objects/sse debug-only, no query/body/secrets in logs - new observability capability spec; README/.env.example docs
96 lines
2.3 KiB
Go
96 lines
2.3 KiB
Go
// Package logging configures the process-wide structured logger from
|
|
// environment-driven settings.
|
|
package logging
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// ParseLevel maps a textual level name to a slog.Level.
|
|
func ParseLevel(name string) (slog.Level, error) {
|
|
switch strings.ToLower(strings.TrimSpace(name)) {
|
|
case "debug":
|
|
return slog.LevelDebug, nil
|
|
case "info":
|
|
return slog.LevelInfo, nil
|
|
case "warn", "warning":
|
|
return slog.LevelWarn, nil
|
|
case "error":
|
|
return slog.LevelError, nil
|
|
default:
|
|
return 0, fmt.Errorf("invalid log level %q (want debug, info, warn or error)", name)
|
|
}
|
|
}
|
|
|
|
// ParseFormat normalizes a format name. An empty name defaults to JSON.
|
|
func ParseFormat(name string) (string, error) {
|
|
switch strings.ToLower(strings.TrimSpace(name)) {
|
|
case "", "json":
|
|
return "json", nil
|
|
case "text":
|
|
return "text", nil
|
|
default:
|
|
return "", fmt.Errorf("invalid log format %q (want json or text)", name)
|
|
}
|
|
}
|
|
|
|
// NewHandler builds a slog handler writing to stdout in the requested format.
|
|
// An empty format defaults to JSON.
|
|
func NewHandler(format string, level slog.Level) (slog.Handler, error) {
|
|
f, err := ParseFormat(format)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts := &slog.HandlerOptions{Level: level}
|
|
if f == "text" {
|
|
return slog.NewTextHandler(os.Stdout, opts), nil
|
|
}
|
|
return slog.NewJSONHandler(os.Stdout, opts), nil
|
|
}
|
|
|
|
type requestIDKey struct{}
|
|
|
|
// WithRequestID stores a request identifier in the context.
|
|
func WithRequestID(ctx context.Context, id string) context.Context {
|
|
return context.WithValue(ctx, requestIDKey{}, id)
|
|
}
|
|
|
|
// RequestID returns the request identifier stored in the context, or "".
|
|
func RequestID(ctx context.Context) string {
|
|
if id, ok := ctx.Value(requestIDKey{}).(string); ok {
|
|
return id
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// NewRequestID returns a fresh random request identifier.
|
|
func NewRequestID() string {
|
|
var b [16]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
return "unknown"
|
|
}
|
|
return hex.EncodeToString(b[:])
|
|
}
|
|
|
|
// ValidRequestID reports whether an incoming identifier is safe to reuse.
|
|
func ValidRequestID(id string) bool {
|
|
if id == "" || len(id) > 128 {
|
|
return false
|
|
}
|
|
for _, r := range id {
|
|
switch {
|
|
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
|
|
case r == '-', r == '_', r == '.':
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|