// Package logger provides unified structured logging for the ja4-platform services. // It merges the component-based logger from sentinel and the prefix/fields-based // logger from correlator into a single implementation. package logger 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 a 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 prefix+fields-based logging (correlator style). type Logger struct { mu sync.RWMutex logger *log.Logger prefix string fields map[string]any minLevel LogLevel } // New creates a new Logger with INFO level. func New(prefix string) *Logger { return &Logger{ logger: log.New(os.Stderr, "", log.LstdFlags|log.Lmicroseconds), prefix: prefix, fields: make(map[string]any), minLevel: INFO, } } // NewWithLevel creates a new Logger with the specified minimum level. func NewWithLevel(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 structured 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) { l.emit("INFO", msg) } } // Infof logs a formatted info message. func (l *Logger) Infof(msg string, args ...any) { if l.ShouldLog(INFO) { l.emit("INFO", fmt.Sprintf(msg, args...)) } } // Warn logs a warning message. func (l *Logger) Warn(msg string) { if l.ShouldLog(WARN) { l.emit("WARN", msg) } } // Warnf logs a formatted warning message. func (l *Logger) Warnf(msg string, args ...any) { if l.ShouldLog(WARN) { l.emit("WARN", fmt.Sprintf(msg, args...)) } } // Error logs an error message with an optional error value. func (l *Logger) Error(msg string, err error) { if !l.ShouldLog(ERROR) { return } if err != nil { l.emit("ERROR", msg+" "+err.Error()) } else { l.emit("ERROR", msg) } } // Debug logs a debug message. func (l *Logger) Debug(msg string) { if l.ShouldLog(DEBUG) { l.emit("DEBUG", msg) } } // Debugf logs a formatted debug message. func (l *Logger) Debugf(msg string, args ...any) { if l.ShouldLog(DEBUG) { l.emit("DEBUG", fmt.Sprintf(msg, args...)) } } func (l *Logger) emit(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()) } // ComponentLogger wraps Logger to satisfy the sentinel-style component-based Logger interface. // This allows new services to use ja4common while sentinel's existing api.Logger interface // is still satisfied. type ComponentLogger struct { *Logger } // NewComponentLogger creates a ComponentLogger with the specified log level. func NewComponentLogger(level string) *ComponentLogger { return &ComponentLogger{Logger: NewWithLevel("", level)} } // Log emits a structured log entry for the given component. func (c *ComponentLogger) Log(component, level, message string, details map[string]string) { fields := make(map[string]any, len(details)+1) fields["component"] = component for k, v := range details { fields[k] = v } cl := c.Logger.WithFields(fields) switch strings.ToLower(level) { case "debug": cl.Debug(message) case "warn", "warning": cl.Warn(message) case "error": cl.Error(message, nil) default: cl.Info(message) } } // Debug logs a debug entry for the given component. func (c *ComponentLogger) Debug(component, message string, details map[string]string) { c.Log(component, "debug", message, details) } // Info logs an info entry for the given component. func (c *ComponentLogger) Info(component, message string, details map[string]string) { c.Log(component, "info", message, details) } // Warn logs a warning entry for the given component. func (c *ComponentLogger) Warn(component, message string, details map[string]string) { c.Log(component, "warn", message, details) } // Error logs an error entry for the given component. func (c *ComponentLogger) Error(component, message string, details map[string]string) { c.Log(component, "error", message, details) }