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...)) }