// Package logging provides structured JSON logging for the gateway. // All logs are emitted as single-line JSON records. package logging import ( "context" "encoding/json" "io" "log" "os" "strings" "time" ) // Level represents a log level. type Level int const ( LevelDebug Level = iota LevelInfo LevelWarn LevelError ) // Logger provides structured logging with sensitive data redaction. type Logger struct { level Level out io.Writer } // New creates a new structured logger with the given level writing to out. func New(level Level, out io.Writer) *Logger { if out == nil { out = os.Stderr } return &Logger{level: level, out: out} } // ParseLevel parses a log level string (debug, info, warn, error). func ParseLevel(s string) Level { switch strings.ToLower(s) { case "debug": return LevelDebug case "info": return LevelInfo case "warn": return LevelWarn case "error": return LevelError default: return LevelInfo } } // LogRecord is a single structured log entry. type LogRecord struct { Timestamp string `json:"timestamp"` Level string `json:"level"` Message string `json:"message"` Route string `json:"route,omitempty"` Upstream string `json:"upstream,omitempty"` Method string `json:"method,omitempty"` Path string `json:"path,omitempty"` Status int `json:"status,omitempty"` Duration string `json:"duration,omitempty"` Reason string `json:"reason,omitempty"` Error string `json:"error,omitempty"` Extra map[string]string `json:"extra,omitempty"` } // RequestLog logs a request with response information. func (l *Logger) RequestLog(route, upstream, method, path string, status int, duration time.Duration, reason string) { if l.level > LevelInfo { return } record := LogRecord{ Timestamp: time.Now().UTC().Format(time.RFC3339Nano), Level: "info", Message: "request", Route: route, Upstream: upstream, Method: method, Path: path, Status: status, Duration: duration.String(), Reason: reason, } l.emit(record) } // Infof logs an info-level message. func (l *Logger) Infof(msg string, fields map[string]string) { if l.level > LevelInfo { return } record := LogRecord{ Timestamp: time.Now().UTC().Format(time.RFC3339Nano), Level: "info", Message: msg, Extra: fields, } l.emit(record) } // Warnf logs a warn-level message. func (l *Logger) Warnf(msg string, fields map[string]string) { if l.level > LevelWarn { return } record := LogRecord{ Timestamp: time.Now().UTC().Format(time.RFC3339Nano), Level: "warn", Message: msg, Extra: fields, } l.emit(record) } // Errorf logs an error-level message. func (l *Logger) Errorf(msg string, err error, fields map[string]string) { if l.level > LevelError { return } errStr := "" if err != nil { errStr = err.Error() } record := LogRecord{ Timestamp: time.Now().UTC().Format(time.RFC3339Nano), Level: "error", Message: msg, Error: errStr, Extra: fields, } l.emit(record) } // emit writes a record as a single JSON line. func (l *Logger) emit(record LogRecord) { data, err := json.Marshal(record) if err != nil { log.Printf("failed to marshal log record: %v", err) return } _, _ = l.out.Write(append(data, '\n')) } // global is the singleton logger instance. var global *Logger // Init initializes the global logger with the given level. // If not called, defaults to Info level on stderr. func Init(level Level, out io.Writer) { global = New(level, out) } // ensure global is initialized func ensure() { if global == nil { global = New(LevelInfo, os.Stderr) } } // RequestLog logs a request with response information using the global logger. func RequestLog(route, upstream, method, path string, status int, duration time.Duration, reason string) { ensure() global.RequestLog(route, upstream, method, path, status, duration, reason) } // Infof logs an info message using the global logger. func Infof(msg string, fields map[string]string) { ensure() global.Infof(msg, fields) } // Warnf logs a warn message using the global logger. func Warnf(msg string, fields map[string]string) { ensure() global.Warnf(msg, fields) } // Errorf logs an error message using the global logger. func Errorf(msg string, err error, fields map[string]string) { ensure() global.Errorf(msg, err, fields) } // FromContext retrieves the logger from a context, or returns the global logger. func FromContext(ctx context.Context) *Logger { ensure() if l, ok := ctx.Value("logger").(*Logger); ok { return l } return global } // WithContext returns a new context with the logger attached. func WithContext(ctx context.Context, l *Logger) context.Context { return context.WithValue(ctx, "logger", l) }