package observability import ( "context" "os" "time" "github.com/rs/zerolog" "go.opentelemetry.io/otel/trace" ) // Logger is a wrapper around zerolog.Logger to provide a consistent logging interface. type Logger struct { *zerolog.Logger } // NewLogger creates a new Logger instance. // It writes to a human-friendly console in "development" environment, // and writes JSON to stdout otherwise. func NewLogger(serviceName, environment string) *Logger { var logger zerolog.Logger if environment == "development" { logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With(). Timestamp(). Str("service", serviceName). Logger() } else { zerolog.TimeFieldFormat = time.RFC3339 logger = zerolog.New(os.Stdout).With(). Timestamp(). Str("service", serviceName). Logger() } return &Logger{&logger} } // Ctx returns a new logger with context-specific fields, such as trace and span IDs. func (l *Logger) Ctx(ctx context.Context) *Logger { log := l.Logger // log is a *zerolog.Logger span := trace.SpanFromContext(ctx) if span.SpanContext().IsValid() { // .Logger() returns a value, not a pointer. // We create a new logger value... newLogger := log.With(). Str("trace_id", span.SpanContext().TraceID().String()). Str("span_id", span.SpanContext().SpanID().String()). Logger() // ...and then use its address. log = &newLogger } // `log` is now the correct *zerolog.Logger, so we wrap it. return &Logger{log} }