tercul-backend/internal/platform/log/logger.go
google-labs-jules[bot] 3bcd8d08f5 feat: Implement observability stack
This commit introduces a comprehensive observability stack to the application, addressing a key objective from the TODO list.

The following features have been implemented:

- **Centralized Logging:** Replaced the existing custom logger with `zerolog` for structured, leveled, and performant logging. The logger is configured to output human-friendly console logs in development and JSON logs in production.

- **Distributed Tracing:** Integrated OpenTelemetry to provide distributed tracing capabilities. A new middleware has been added to create spans for all incoming HTTP requests, and the trace context is propagated.

- **Prometheus Metrics:** Added Prometheus metrics to monitor HTTP request latency and total request counts. A new `/metrics` endpoint is exposed on port 9090 to serve these metrics.

- **Request ID:** Implemented a middleware to add a unique request ID to every incoming request and response, improving traceability.

The new observability components are encapsulated in the `internal/observability` package, and the existing `internal/platform/log` package has been refactored to be a backward-compatible wrapper around the new logger.

The main application entry point (`cmd/api/main.go`) has been updated to initialize and gracefully shut down the new observability components.
2025-10-03 16:43:01 +00:00

239 lines
6.1 KiB
Go

package log
import (
"context"
"fmt"
"tercul/internal/observability"
"github.com/rs/zerolog"
)
// LogLevel represents the severity level of a log message.
type LogLevel int
const (
// DebugLevel for detailed troubleshooting.
DebugLevel LogLevel = iota
// InfoLevel for general operational information.
InfoLevel
// WarnLevel for potentially harmful situations.
WarnLevel
// ErrorLevel for error events that might still allow the application to continue.
ErrorLevel
// FatalLevel for severe error events that will lead the application to abort.
FatalLevel
)
// Field represents a key-value pair for structured logging.
type Field struct {
Key string
Value interface{}
}
// F creates a new Field.
func F(key string, value interface{}) Field {
return Field{Key: key, Value: value}
}
// Logger provides structured logging capabilities.
type Logger struct {
*observability.Logger
}
var defaultLogger = &Logger{observability.NewLogger("tercul", "development")}
// Init re-initializes the default logger. This is useful for applications
// that need to configure the logger with dynamic values.
func Init(serviceName, environment string) {
defaultLogger = &Logger{observability.NewLogger(serviceName, environment)}
}
// SetDefaultLevel sets the log level for the default logger.
func SetDefaultLevel(level LogLevel) {
var zlevel zerolog.Level
switch level {
case DebugLevel:
zlevel = zerolog.DebugLevel
case InfoLevel:
zlevel = zerolog.InfoLevel
case WarnLevel:
zlevel = zerolog.WarnLevel
case ErrorLevel:
zlevel = zerolog.ErrorLevel
case FatalLevel:
zlevel = zerolog.FatalLevel
default:
zlevel = zerolog.InfoLevel
}
zerolog.SetGlobalLevel(zlevel)
}
func log(level LogLevel, msg string, fields ...Field) {
var event *zerolog.Event
// Access the embedded observability.Logger to get to zerolog's methods.
zlog := defaultLogger.Logger
switch level {
case DebugLevel:
event = zlog.Debug()
case InfoLevel:
event = zlog.Info()
case WarnLevel:
event = zlog.Warn()
case ErrorLevel:
event = zlog.Error()
case FatalLevel:
event = zlog.Fatal()
default:
event = zlog.Info()
}
for _, f := range fields {
event.Interface(f.Key, f.Value)
}
event.Msg(msg)
}
// LogDebug logs a message at debug level using the default logger.
func LogDebug(msg string, fields ...Field) {
log(DebugLevel, msg, fields...)
}
// LogInfo logs a message at info level using the default logger.
func LogInfo(msg string, fields ...Field) {
log(InfoLevel, msg, fields...)
}
// LogWarn logs a message at warn level using the default logger.
func LogWarn(msg string, fields ...Field) {
log(WarnLevel, msg, fields...)
}
// LogError logs a message at error level using the default logger.
func LogError(msg string, fields ...Field) {
log(ErrorLevel, msg, fields...)
}
// LogFatal logs a message at fatal level using the default logger and then calls os.Exit(1).
func LogFatal(msg string, fields ...Field) {
log(FatalLevel, msg, fields...)
}
// WithFields returns a new logger with the given fields added using the default logger.
func WithFields(fields ...Field) *Logger {
sublogger := defaultLogger.With().Logger()
for _, f := range fields {
sublogger = sublogger.With().Interface(f.Key, f.Value).Logger()
}
return &Logger{&observability.Logger{&sublogger}}
}
// WithContext returns a new logger with the given context added using the default logger.
func WithContext(ctx context.Context) *Logger {
return &Logger{defaultLogger.Ctx(ctx)}
}
// The following functions are kept for compatibility but are now simplified or deprecated.
// SetDefaultLogger is deprecated. Use Init.
func SetDefaultLogger(logger *Logger) {
// Deprecated: Logger is now initialized via Init.
}
// String returns the string representation of the log level.
func (l LogLevel) String() string {
switch l {
case DebugLevel:
return "DEBUG"
case InfoLevel:
return "INFO"
case WarnLevel:
return "WARN"
case ErrorLevel:
return "ERROR"
case FatalLevel:
return "FATAL"
default:
return "UNKNOWN"
}
}
// Debug logs a message at debug level.
func (l *Logger) Debug(msg string, fields ...Field) {
l.log(DebugLevel, msg, fields...)
}
// Info logs a message at info level.
func (l *Logger) Info(msg string, fields ...Field) {
l.log(InfoLevel, msg, fields...)
}
// Warn logs a message at warn level.
func (l *Logger) Warn(msg string, fields ...Field) {
l.log(WarnLevel, msg, fields...)
}
// Error logs a message at error level.
func (l *Logger) Error(msg string, fields ...Field) {
l.log(ErrorLevel, msg, fields...)
}
// Fatal logs a message at fatal level and then calls os.Exit(1).
func (l *Logger) Fatal(msg string, fields ...Field) {
l.log(FatalLevel, msg, fields...)
}
func (l *Logger) log(level LogLevel, msg string, fields ...Field) {
var event *zerolog.Event
switch level {
case DebugLevel:
event = l.Logger.Debug()
case InfoLevel:
event = l.Logger.Info()
case WarnLevel:
event = l.Logger.Warn()
case ErrorLevel:
event = l.Logger.Error()
case FatalLevel:
event = l.Logger.Fatal()
default:
event = l.Logger.Info()
}
for _, f := range fields {
event.Interface(f.Key, f.Value)
}
event.Msg(msg)
}
// WithFields returns a new logger with the given fields added.
func (l *Logger) WithFields(fields ...Field) *Logger {
sublogger := l.With().Logger()
for _, f := range fields {
sublogger = sublogger.With().Interface(f.Key, f.Value).Logger()
}
return &Logger{&observability.Logger{&sublogger}}
}
func (l *Logger) WithContext(ctx map[string]interface{}) *Logger {
// To maintain compatibility with the old API, we will convert the map to a context.
// This is not ideal and should be refactored in the future.
zlog := l.Logger.With().Logger()
for k, v := range ctx {
zlog = zlog.With().Interface(k, v).Logger()
}
return &Logger{&observability.Logger{&zlog}}
}
func (l *Logger) SetLevel(level LogLevel) {
// This now controls the global log level.
SetDefaultLevel(level)
}
// Fmt versions for simple string formatting
func LogInfof(format string, v ...interface{}) {
log(InfoLevel, fmt.Sprintf(format, v...))
}
func LogErrorf(format string, v ...interface{}) {
log(ErrorLevel, fmt.Sprintf(format, v...))
}