package masterlog import ( "os" "runtime" "time" ) // MasterLogger is the main logger implementation type MasterLogger struct { level Level encoders []Encoder writers []Writer pseudonymizer *Pseudonymizer } // New creates a new logger with default console writer and formatted encoder // If level is provided, it will be used; otherwise LevelInfo is the default func New(level ...Level) *MasterLogger { logLevel := LevelInfo if len(level) > 0 { logLevel = level[0] } consoleWriter := NewConsoleWriter() useColors := consoleWriter.isTerminal logger := &MasterLogger{ level: logLevel, encoders: []Encoder{NewFormattedEncoder(useColors)}, writers: []Writer{consoleWriter}, } return logger } // SetLevel sets the minimum log level func (l *MasterLogger) SetLevel(level Level) { l.level = level } // AddWriter adds a writer to the logger func (l *MasterLogger) AddWriter(writer Writer) { l.writers = append(l.writers, writer) } // AddEncoder adds an encoder to the logger func (l *MasterLogger) AddEncoder(encoder Encoder) { l.encoders = append(l.encoders, encoder) } // SetPseudonymizer sets the pseudonymizer for the logger func (l *MasterLogger) SetPseudonymizer(pseudonymizer *Pseudonymizer) { l.pseudonymizer = pseudonymizer } // AddSensitiveField marks a field name as sensitive, so it will be pseudonymized func (l *MasterLogger) AddSensitiveField(fieldName string) { if l.pseudonymizer != nil { l.pseudonymizer.AddSensitiveField(fieldName) } } // AddSensitiveFields marks multiple field names as sensitive func (l *MasterLogger) AddSensitiveFields(fieldNames ...string) { if l.pseudonymizer != nil { l.pseudonymizer.AddSensitiveFields(fieldNames...) } } // log writes a log entry // If file and line are empty/zero, they will be determined from the call stack func (l *MasterLogger) log(level Level, message string, file string, line int, fields ...map[string]interface{}) { if level < l.level { return } // If file/line not provided, get from call stack if file == "" && line == 0 { // For instance methods: 0=Caller, 1=log(), 2=Info(), 3=user code _, file, line, _ = runtime.Caller(3) if file == "" { file = "unknown" } } // Collect custom fields (user-provided) customFields := make(map[string]interface{}) for _, fieldMap := range fields { for k, v := range fieldMap { customFields[k] = v } } // Collect default fields defaultFields := map[string]interface{}{ "go_version": runtime.Version(), "pid": os.Getpid(), } // Apply pseudonymization if enabled if l.pseudonymizer != nil { customFields = l.pseudonymizer.PseudonymizeFields(customFields) defaultFields = l.pseudonymizer.PseudonymizeFields(defaultFields) } // Merge all fields for compatibility allFields := make(map[string]interface{}) for k, v := range customFields { allFields[k] = v } for k, v := range defaultFields { allFields[k] = v } entry := Entry{ Timestamp: time.Now(), Level: level, File: file, Line: line, Message: message, Fields: allFields, CustomFields: customFields, DefaultFields: defaultFields, } // Encode and write with each encoder/writer combination // For formatted encoder, we need to encode differently for each writer (with/without colors) for _, encoder := range l.encoders { for _, writer := range l.writers { // Skip JSON encoder for console writers (only use formatted encoder) if _, isJSON := encoder.(*JSONEncoder); isJSON { if _, isConsole := writer.(*ConsoleWriter); isConsole { continue // Don't write JSON to console } } // Skip FormattedEncoder for file writers if JSONEncoder is present // This ensures files only get JSON format when JSONEncoder is added if _, isFormatted := encoder.(*FormattedEncoder); isFormatted { if _, isFile := writer.(*FileWriter); isFile { // Check if JSONEncoder exists in encoders hasJSONEncoder := false for _, e := range l.encoders { if _, ok := e.(*JSONEncoder); ok { hasJSONEncoder = true break } } if hasJSONEncoder { continue // Don't write formatted to file if JSON encoder exists } } } var data []byte var err error // If it's a FormattedEncoder, check if writer supports colors if _, ok := encoder.(*FormattedEncoder); ok { // Create a temporary encoder with the right color setting tempEncoder := &FormattedEncoder{useColors: writer.SupportsColors()} data, err = tempEncoder.Encode(entry) } else { // For other encoders (JSON, etc.), use as-is data, err = encoder.Encode(entry) } if err != nil { continue } _ = writer.Write(data) } } } func (l *MasterLogger) Trace(message string, fields ...map[string]interface{}) { _, file, line, _ := runtime.Caller(1) l.log(LevelTrace, message, file, line, fields...) } func (l *MasterLogger) Debug(message string, fields ...map[string]interface{}) { _, file, line, _ := runtime.Caller(1) l.log(LevelDebug, message, file, line, fields...) } func (l *MasterLogger) Info(message string, fields ...map[string]interface{}) { _, file, line, _ := runtime.Caller(1) l.log(LevelInfo, message, file, line, fields...) } func (l *MasterLogger) Warn(message string, fields ...map[string]interface{}) { _, file, line, _ := runtime.Caller(1) l.log(LevelWarn, message, file, line, fields...) } func (l *MasterLogger) Error(message string, fields ...map[string]interface{}) { _, file, line, _ := runtime.Caller(1) l.log(LevelError, message, file, line, fields...) } // Default logger instance var defaultLogger = New(LevelInfo) // SetDefaultLogger replaces the default logger instance // This allows you to configure a custom logger and use it globally func SetDefaultLogger(logger *MasterLogger) { defaultLogger = logger } // GetDefaultLogger returns the current default logger instance // Useful if you want to configure it directly func GetDefaultLogger() *MasterLogger { return defaultLogger } // Package-level convenience functions // These collect caller info before calling the instance method func Trace(message string, fields ...map[string]interface{}) { _, file, line, _ := runtime.Caller(1) defaultLogger.log(LevelTrace, message, file, line, fields...) } func Debug(message string, fields ...map[string]interface{}) { _, file, line, _ := runtime.Caller(1) defaultLogger.log(LevelDebug, message, file, line, fields...) } func Info(message string, fields ...map[string]interface{}) { _, file, line, _ := runtime.Caller(1) defaultLogger.log(LevelInfo, message, file, line, fields...) } func Warn(message string, fields ...map[string]interface{}) { _, file, line, _ := runtime.Caller(1) defaultLogger.log(LevelWarn, message, file, line, fields...) } func Error(message string, fields ...map[string]interface{}) { _, file, line, _ := runtime.Caller(1) defaultLogger.log(LevelError, message, file, line, fields...) } func SetLevel(level Level) { defaultLogger.SetLevel(level) } func AddWriter(writer Writer) { defaultLogger.AddWriter(writer) } func AddEncoder(encoder Encoder) { defaultLogger.AddEncoder(encoder) } func SetPseudonymizer(pseudonymizer *Pseudonymizer) { defaultLogger.SetPseudonymizer(pseudonymizer) } func AddSensitiveField(fieldName string) { defaultLogger.AddSensitiveField(fieldName) } func AddSensitiveFields(fieldNames ...string) { defaultLogger.AddSensitiveFields(fieldNames...) }