fetch_ml/internal/logging/logging.go
Jeremie Fraeys 803677be57 feat: implement Go backend with comprehensive API and internal packages
- Add API server with WebSocket support and REST endpoints
- Implement authentication system with API keys and permissions
- Add task queue system with Redis backend and error handling
- Include storage layer with database migrations and schemas
- Add comprehensive logging, metrics, and telemetry
- Implement security middleware and network utilities
- Add experiment management and container orchestration
- Include configuration management with smart defaults
2025-12-04 16:53:53 -05:00

172 lines
4.3 KiB
Go

package logging
import (
"context"
"io"
"log/slog"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
)
type ctxKey string
const (
CtxTraceID ctxKey = "trace_id"
CtxSpanID ctxKey = "span_id"
CtxWorker ctxKey = "worker_id"
CtxJob ctxKey = "job_name"
CtxTask ctxKey = "task_id"
)
type Logger struct {
*slog.Logger
}
// NewLogger creates a logger that writes to stderr (development mode)
func NewLogger(level slog.Level, jsonOutput bool) *Logger {
opts := &slog.HandlerOptions{
Level: level,
AddSource: os.Getenv("LOG_ADD_SOURCE") == "1",
}
var handler slog.Handler
if jsonOutput || os.Getenv("LOG_FORMAT") == "json" {
handler = slog.NewJSONHandler(os.Stderr, opts)
} else {
handler = NewColorTextHandler(os.Stderr, opts)
}
return &Logger{slog.New(handler)}
}
// NewFileLogger creates a logger that writes to a file only (production mode)
func NewFileLogger(level slog.Level, jsonOutput bool, logFile string) *Logger {
opts := &slog.HandlerOptions{
Level: level,
AddSource: os.Getenv("LOG_ADD_SOURCE") == "1",
}
// Create log directory if it doesn't exist
if logFile != "" {
logDir := filepath.Dir(logFile)
if err := os.MkdirAll(logDir, 0755); err != nil {
// Fallback to stderr only if directory creation fails
return NewLogger(level, jsonOutput)
}
}
// Open log file
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
// Fallback to stderr only if file creation fails
return NewLogger(level, jsonOutput)
}
// Write to file only (production)
var handler slog.Handler
if jsonOutput || os.Getenv("LOG_FORMAT") == "json" {
handler = slog.NewJSONHandler(file, opts)
} else {
handler = slog.NewTextHandler(file, opts)
}
return &Logger{slog.New(handler)}
}
// Inject trace + span if missing
func EnsureTrace(ctx context.Context) context.Context {
if ctx.Value(CtxTraceID) == nil {
ctx = context.WithValue(ctx, CtxTraceID, uuid.NewString())
}
if ctx.Value(CtxSpanID) == nil {
ctx = context.WithValue(ctx, CtxSpanID, uuid.NewString())
}
return ctx
}
func (l *Logger) WithContext(ctx context.Context, args ...any) *Logger {
if trace := ctx.Value(CtxTraceID); trace != nil {
args = append(args, "trace_id", trace)
}
if span := ctx.Value(CtxSpanID); span != nil {
args = append(args, "span_id", span)
}
if worker := ctx.Value(CtxWorker); worker != nil {
args = append(args, "worker_id", worker)
}
if job := ctx.Value(CtxJob); job != nil {
args = append(args, "job_name", job)
}
if task := ctx.Value(CtxTask); task != nil {
args = append(args, "task_id", task)
}
return &Logger{Logger: l.With(args...)}
}
func CtxWithWorker(ctx context.Context, worker string) context.Context {
return context.WithValue(ctx, CtxWorker, worker)
}
func CtxWithJob(ctx context.Context, job string) context.Context {
return context.WithValue(ctx, CtxJob, job)
}
func CtxWithTask(ctx context.Context, task string) context.Context {
return context.WithValue(ctx, CtxTask, task)
}
func (l *Logger) Component(ctx context.Context, name string) *Logger {
return l.WithContext(ctx, "component", name)
}
func (l *Logger) Worker(ctx context.Context, workerID string) *Logger {
return l.WithContext(ctx, "worker_id", workerID)
}
func (l *Logger) Job(ctx context.Context, job string, task string) *Logger {
return l.WithContext(ctx, "job_name", job, "task_id", task)
}
func (l *Logger) Fatal(msg string, args ...any) {
l.Error(msg, args...)
os.Exit(1)
}
func (l *Logger) Panic(msg string, args ...any) {
l.Error(msg, args...)
panic(msg)
}
// -----------------------------------------------------
// Colorized human-friendly console logs
// -----------------------------------------------------
type ColorTextHandler struct {
slog.Handler
}
func NewColorTextHandler(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
base := slog.NewTextHandler(w, opts)
return &ColorTextHandler{Handler: base}
}
func (h *ColorTextHandler) Handle(ctx context.Context, r slog.Record) error {
// Add uniform timestamp (override default)
r.Time = time.Now()
switch r.Level {
case slog.LevelDebug:
r.Add("lvl_color", "\033[34mDBG\033[0m")
case slog.LevelInfo:
r.Add("lvl_color", "\033[32mINF\033[0m")
case slog.LevelWarn:
r.Add("lvl_color", "\033[33mWRN\033[0m")
case slog.LevelError:
r.Add("lvl_color", "\033[31mERR\033[0m")
}
return h.Handler.Handle(ctx, r)
}