Files
homelab-frontend/internal/logging/logger.go
T

201 lines
4.8 KiB
Go
Raw Normal View History

2026-08-19 20:52:13 -07:00
// 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)
}