package observability import ( "fmt" "log" "os" "sort" "strings" "sync" ) // LogLevel represents the severity of a log message. type LogLevel int const ( DEBUG LogLevel = iota INFO WARN ERROR ) // ParseLogLevel converts a string to LogLevel. func ParseLogLevel(level string) LogLevel { switch strings.ToUpper(level) { case "DEBUG": return DEBUG case "INFO": return INFO case "WARN", "WARNING": return WARN case "ERROR": return ERROR default: return INFO } } // String returns the string representation of LogLevel. func (l LogLevel) String() string { switch l { case DEBUG: return "DEBUG" case INFO: return "INFO" case WARN: return "WARN" case ERROR: return "ERROR" default: return "INFO" } } // Logger provides structured logging. type Logger struct { mu sync.RWMutex logger *log.Logger prefix string fields map[string]any minLevel LogLevel } // NewLogger creates a new logger with INFO level by default. func NewLogger(prefix string) *Logger { return &Logger{ logger: log.New(os.Stderr, "", log.LstdFlags|log.Lmicroseconds), prefix: prefix, fields: make(map[string]any), minLevel: INFO, } } // NewLoggerWithLevel creates a new logger with specified minimum level. func NewLoggerWithLevel(prefix string, level string) *Logger { return &Logger{ logger: log.New(os.Stderr, "", log.LstdFlags|log.Lmicroseconds), prefix: prefix, fields: make(map[string]any), minLevel: ParseLogLevel(level), } } // SetLevel sets the minimum log level. func (l *Logger) SetLevel(level string) { l.mu.Lock() defer l.mu.Unlock() l.minLevel = ParseLogLevel(level) } // ShouldLog returns true if the given level should be logged. func (l *Logger) ShouldLog(level LogLevel) bool { l.mu.RLock() defer l.mu.RUnlock() return level >= l.minLevel } // WithFields returns a new logger with additional fields. func (l *Logger) WithFields(fields map[string]any) *Logger { l.mu.RLock() minLevel := l.minLevel prefix := l.prefix existing := make(map[string]any, len(l.fields)) for k, v := range l.fields { existing[k] = v } l.mu.RUnlock() for k, v := range fields { existing[k] = v } return &Logger{ logger: l.logger, prefix: prefix, fields: existing, minLevel: minLevel, } } // Info logs an info message. func (l *Logger) Info(msg string) { if !l.ShouldLog(INFO) { return } l.log("INFO", msg) } // Warn logs a warning message. func (l *Logger) Warn(msg string) { if !l.ShouldLog(WARN) { return } l.log("WARN", msg) } // Error logs an error message. func (l *Logger) Error(msg string, err error) { if !l.ShouldLog(ERROR) { return } if err != nil { l.log("ERROR", msg+" "+err.Error()) } else { l.log("ERROR", msg) } } // Debug logs a debug message (only if debug level is enabled). func (l *Logger) Debug(msg string) { if !l.ShouldLog(DEBUG) { return } l.log("DEBUG", msg) } // Debugf logs a formatted debug message (only if debug level is enabled). func (l *Logger) Debugf(msg string, args ...any) { if !l.ShouldLog(DEBUG) { return } l.log("DEBUG", fmt.Sprintf(msg, args...)) } // Warnf logs a formatted warning message. func (l *Logger) Warnf(msg string, args ...any) { if !l.ShouldLog(WARN) { return } l.log("WARN", fmt.Sprintf(msg, args...)) } // Infof logs a formatted info message. func (l *Logger) Infof(msg string, args ...any) { if !l.ShouldLog(INFO) { return } l.log("INFO", fmt.Sprintf(msg, args...)) } func (l *Logger) log(level, msg string) { l.mu.RLock() prefix := l.prefix fields := make(map[string]any, len(l.fields)) for k, v := range l.fields { fields[k] = v } l.mu.RUnlock() var b strings.Builder if prefix != "" { b.WriteString("[") b.WriteString(prefix) b.WriteString("] ") } b.WriteString(level) b.WriteString(" ") b.WriteString(msg) if len(fields) > 0 { keys := make([]string, 0, len(fields)) for k := range fields { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { b.WriteString(" ") b.WriteString(k) b.WriteString("=") b.WriteString(fmt.Sprintf("%v", fields[k])) } } l.logger.Print(b.String()) }