diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30fa77d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.log +example/app.log \ No newline at end of file diff --git a/README.md b/README.md index 31f49f5..dfa717a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ go get git.secnex.io/secnex/masterlog ## Quick Start +### Basic Usage + ```go package main @@ -30,7 +32,7 @@ import ( ) func main() { - // Simple logging + // Simple logging using the global logger masterlog.Info("Hello, World!") // Logging with fields @@ -47,6 +49,45 @@ func main() { 2025-11-10T05:06:02+01:00 INF main.go:12 > User logged in user_id=12345 ip=192.168.1.1 go_version=go1.25.3 pid=12345 ``` +### Configuring the Global Logger + +Configure the global logger once at application startup: + +```go +package main + +import ( + "git.secnex.io/secnex/masterlog" +) + +func main() { + // Configure global logger at startup + masterlog.SetLevel(masterlog.LevelDebug) + + // Add file writer + fileWriter, _ := masterlog.NewFileWriter("app.log") + defer fileWriter.Close() + masterlog.AddWriter(fileWriter) + + // Add JSON encoder + masterlog.AddEncoder(&masterlog.JSONEncoder{}) + + // Configure pseudonymization + pseudonymizer := masterlog.NewPseudonymizerFromEnv("LOG_SECRET") + masterlog.SetPseudonymizer(pseudonymizer) + masterlog.AddSensitiveFields("user_id", "email", "ip") + + // Now all package-level functions use the configured global logger + masterlog.Info("Application started") + masterlog.Debug("Debug message") +} +``` + +**Benefits:** +- Configure once, use everywhere +- No need to pass logger instances around +- Simple and convenient for most use cases + ## Table of Contents - [Log Levels](#log-levels) @@ -211,6 +252,22 @@ logger.Info("Console log") Writes log entries to a file. +**Using with global logger (recommended):** + +```go +// Add file writer to global logger +fileWriter, err := masterlog.NewFileWriter("app.log") +if err != nil { + log.Fatal(err) +} +defer fileWriter.Close() + +masterlog.AddWriter(fileWriter) +masterlog.Info("File log") // Uses global logger +``` + +**Using with custom logger:** + ```go logger := masterlog.New() fileWriter, err := masterlog.NewFileWriter("app.log") @@ -239,9 +296,42 @@ logger.AddWriter(fileWriter) logger.Info("Multi-destination log") ``` -## Custom Logger Instances +## Global Logger vs Custom Instances -Create custom logger instances with specific configurations: +MasterLog provides two ways to use logging: + +### Global Logger (Recommended for Most Cases) + +The global logger is pre-configured and ready to use. Configure it once at startup: + +```go +func main() { + // Configure global logger at startup + masterlog.SetLevel(masterlog.LevelDebug) + + // Add file writer + fileWriter, _ := masterlog.NewFileWriter("app.log") + defer fileWriter.Close() + masterlog.AddWriter(fileWriter) + + // Add JSON encoder + masterlog.AddEncoder(&masterlog.JSONEncoder{}) + + // Use package-level functions anywhere in your code + masterlog.Info("This uses the global logger") + masterlog.Debug("No need to pass logger instances around") +} +``` + +**Advantages:** +- Simple and convenient +- No need to pass logger instances +- Configure once, use everywhere +- Perfect for most applications + +### Custom Logger Instances + +Create custom logger instances when you need different configurations for different components: ```go // Create logger with custom level @@ -258,6 +348,37 @@ logger.AddWriter(fileWriter) logger.Info("Custom logger example") ``` +**Use Cases:** +- Different log levels for different components +- Separate log files for different modules +- Different encoders for different outputs +- Component-specific pseudonymization rules + +### Replacing the Global Logger + +You can also replace the global logger with a custom instance: + +```go +// Create and configure a custom logger +customLogger := masterlog.New(masterlog.LevelTrace) +customLogger.AddEncoder(&masterlog.JSONEncoder{}) + +// Replace the global logger +masterlog.SetDefaultLogger(customLogger) + +// Now all package-level functions use your custom logger +masterlog.Info("Uses the custom logger") +``` + +### Getting the Global Logger + +Access the global logger instance directly if needed: + +```go +globalLogger := masterlog.GetDefaultLogger() +globalLogger.AddWriter(fileWriter) +``` + ## Data Pseudonymization MasterLog includes built-in support for deterministic pseudonymization of sensitive data using HMAC-SHA256. This allows you to: @@ -415,6 +536,8 @@ func AddEncoder(encoder Encoder) func SetPseudonymizer(pseudonymizer *Pseudonymizer) func AddSensitiveField(fieldName string) func AddSensitiveFields(fieldNames ...string) +func SetDefaultLogger(logger *MasterLogger) +func GetDefaultLogger() *MasterLogger ``` ### Logger Methods diff --git a/example/app.log b/example/app.log deleted file mode 100644 index bd79ad7..0000000 --- a/example/app.log +++ /dev/null @@ -1,10 +0,0 @@ -2025-11-10T05:00:34+01:00 INF proc.go:285 > This will be written to file go_version=go1.25.3 pid=71020 -2025-11-10T05:01:36+01:00 INF proc.go:285 > This will be written to file go_version=go1.25.3 pid=71657 -2025-11-10T05:01:59+01:00 INF proc.go:285 > This will be written to file go_version=go1.25.3 pid=72132 -2025-11-10T05:02:14+01:00 INF proc.go:285 > This will be written to file go_version=go1.25.3 pid=72401 -2025-11-10T05:05:14+01:00 INF main.go:41 > This will be written to file go_version=go1.25.3 pid=73856 -2025-11-10T05:06:02+01:00 INF main.go:41 > This will be written to file go_version=go1.25.3 pid=74313 -2025-11-10T05:07:19+01:00 INF main.go:41 > This will be written to file go_version=go1.25.3 pid=75031 -2025-11-10T05:09:48+01:00 INF main.go:41 > This will be written to file go_version=go1.25.3 pid=76511 -2025-11-10T05:15:16+01:00 INF main.go:41 > This will be written to file go_version=go1.25.3 pid=79210 -2025-11-10T05:15:26+01:00 INF main.go:41 > This will be written to file go_version=go1.25.3 pid=79455 diff --git a/example/main.go b/example/main.go index 2f8b602..235734c 100644 --- a/example/main.go +++ b/example/main.go @@ -5,7 +5,22 @@ import ( ) func main() { + // Example 0: Configure global logger once at startup + // Add file writer to global logger + fileWriter, err := masterlog.NewFileWriter("app.log") + if err == nil { + masterlog.AddWriter(fileWriter) + defer fileWriter.Close() + } + + // Add JSON encoder to global logger + masterlog.AddEncoder(&masterlog.JSONEncoder{}) + + // Set log level for global logger + masterlog.SetLevel(masterlog.LevelDebug) + // Example 1: Simple formatted logging to console (with colors if terminal) + // Now uses the configured global logger (writes to console, file, and JSON) masterlog.Info("Hello, World!") // Example 2: Logging with fields @@ -25,21 +40,22 @@ func main() { traceLogger := masterlog.New(masterlog.LevelTrace) traceLogger.Info("Logger created with LevelTrace") - // Example 5: Create custom logger with JSON encoder + // Example 5: JSON encoder is already added to global logger in Example 0 + // Here's how you'd use it with a custom logger if needed: jsonLogger := masterlog.New() jsonLogger.AddEncoder(&masterlog.JSONEncoder{}) - jsonLogger.Info("JSON formatted log", map[string]interface{}{ + jsonLogger.Info("JSON formatted log (custom logger)", map[string]interface{}{ "key": "value", }) - // Example 6: Create logger with file writer - fileLogger := masterlog.New() - fileWriter, err := masterlog.NewFileWriter("app.log") - if err == nil { - fileLogger.AddWriter(fileWriter) - defer fileWriter.Close() - fileLogger.Info("This will be written to file") - } + // Example 6: Add file writer to global logger (already configured above) + // Note: The fileWriter was already added in Example 0, but here's how you'd do it separately + // fileWriter, err := masterlog.NewFileWriter("app.log") + // if err == nil { + // masterlog.AddWriter(fileWriter) + // defer fileWriter.Close() + // } + masterlog.Info("This will be written to console, file, and JSON (all configured globally)") // Example 7: Logger with multiple encoders (formatted + JSON) multiLogger := masterlog.New() @@ -58,17 +74,16 @@ func main() { debugLogger.Debug("Debug logger with colors") debugLogger.Trace("This won't be logged") - // Example 10: Pseudonymization of sensitive data - pseudoLogger := masterlog.New() + // Example 10: Pseudonymization of sensitive data using global logger // Create a pseudonymizer with a secret (in production, use a secure secret from config/env) pseudonymizer := masterlog.NewPseudonymizerFromString("my-secret-key-change-in-production") - pseudoLogger.SetPseudonymizer(pseudonymizer) + masterlog.SetPseudonymizer(pseudonymizer) - // Mark fields as sensitive - pseudoLogger.AddSensitiveFields("user_id", "email", "ip") + // Mark fields as sensitive (on global logger) + masterlog.AddSensitiveFields("user_id", "email", "ip") - // Log with sensitive data - it will be pseudonymized - pseudoLogger.Info("User logged in", map[string]interface{}{ + // Log with sensitive data - it will be pseudonymized (using global logger) + masterlog.Info("User logged in", map[string]interface{}{ "user_id": 12345, "email": "user@example.com", "ip": "192.168.1.1", @@ -76,7 +91,7 @@ func main() { }) // Same user_id will produce the same pseudonymized value (deterministic) - pseudoLogger.Info("User performed action", map[string]interface{}{ + masterlog.Info("User performed action", map[string]interface{}{ "user_id": 12345, // Same value, same pseudonymized output "action": "purchase", }) diff --git a/logger.go b/logger.go index 9ebc261..67cc6a1 100644 --- a/logger.go +++ b/logger.go @@ -135,6 +135,24 @@ func (l *MasterLogger) log(level Level, message string, file string, line int, f } } + // 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 @@ -185,6 +203,18 @@ func (l *MasterLogger) Error(message string, fields ...map[string]interface{}) { // 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{}) {