A powerful, extensible, and zero-dependency logging library for Go with built-in support for structured logging, colored output, and data pseudonymization.
- 🎨 Colored Console Output - Beautiful, syntax-highlighted logs with automatic terminal detection
- 📝 Structured Logging - Support for custom fields and structured data
- 🔒 Data Pseudonymization - Built-in HMAC-based deterministic pseudonymization for sensitive data
- 📊 Multiple Encoders - Formatted text and JSON encoding out of the box
- 📁 Flexible Writers - Console and file writers with easy extensibility
- 🎯 Zero Dependencies - Uses only Go standard library
- 🔧 Extensible Architecture - Easy to add custom encoders and writers
- ⚡ Performance - Efficient logging with minimal overhead
- 🎛️ Configurable Log Levels - Trace, Debug, Info, Warn, Error
go get git.secnex.io/secnex/masterlogpackage main
import (
"git.secnex.io/secnex/masterlog"
)
func main() {
// Simple logging using the global logger
masterlog.Info("Hello, World!")
// Logging with fields
masterlog.Info("User logged in", map[string]interface{}{
"user_id": 12345,
"ip": "192.168.1.1",
})
}Output:
2025-11-10T05:06:02+01:00 INF main.go:9 > Hello, World! go_version=go1.25.3 pid=12345
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
Configure the global logger once at application startup:
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
- Log Levels
- Structured Logging
- Colored Output
- Encoders
- Writers
- Custom Logger Instances
- Data Pseudonymization
- Extending MasterLog
- API Reference
- Best Practices
MasterLog supports five log levels:
TRC(Trace) - Most verbose, for detailed debuggingDBG(Debug) - Debug informationINF(Info) - General informational messagesWRN(Warn) - Warning messagesERR(Error) - Error messages
// Set level for default logger
masterlog.SetLevel(masterlog.LevelDebug)
// Create logger with specific level
logger := masterlog.New(masterlog.LevelTrace)
logger.Trace("This will be logged")
logger.Debug("This will also be logged")masterlog.Trace("trace message")
masterlog.Debug("debug message")
masterlog.Info("info message")
masterlog.Warn("warning message")
masterlog.Error("error message")MasterLog supports structured logging with custom fields. Custom fields are displayed before default fields (go_version, pid).
masterlog.Info("User action", map[string]interface{}{
"user_id": 12345,
"action": "purchase",
"amount": 99.99,
"ip": "192.168.1.1",
})Output:
2025-11-10T05:06:02+01:00 INF main.go:12 > User action user_id=12345 action=purchase amount=99.99 ip=192.168.1.1 go_version=go1.25.3 pid=12345
Custom fields (user-provided) are always displayed before default fields:
- Custom fields (user-provided)
- Default fields (go_version, pid)
MasterLog automatically detects if output is going to a terminal and applies colors accordingly. Colors are never written to files.
- Timestamp: Dark gray
- Log Level:
TRC: GrayDBG: CyanINF: GreenWRN: YellowERR: Red
- File:Line: Dark gray
- Message: White
- Field Keys: Turquoise
- Field Values: White
When logging to a terminal, you'll see beautifully colored output. When redirecting to a file or non-terminal, colors are automatically disabled.
masterlog.Info("Colored output example", map[string]interface{}{
"key": "value",
})Encoders determine the format of log entries. MasterLog provides two built-in encoders:
The default encoder that produces human-readable formatted text.
logger := masterlog.New()
// FormattedEncoder is used by default
logger.Info("Formatted log")Output:
2025-11-10T05:06:02+01:00 INF main.go:12 > Formatted log go_version=go1.25.3 pid=12345
Produces JSON-formatted log entries, perfect for log aggregation systems.
logger := masterlog.New()
logger.AddEncoder(&masterlog.JSONEncoder{})
logger.Info("JSON log", map[string]interface{}{
"key": "value",
})Output:
{"timestamp":"2025-11-10T05:06:02+01:00","level":"INF","file":"main.go","line":12,"message":"JSON log","fields":{"key":"value","go_version":"go1.25.3","pid":12345}}Note: JSON encoder output is automatically excluded from console output when using the default logger to keep console logs readable.
You can use multiple encoders simultaneously:
logger := masterlog.New()
logger.AddEncoder(&masterlog.JSONEncoder{})
// Now logs will be written in both formatted and JSON formats
logger.Info("Dual format log")Writers determine where log entries are written. MasterLog provides two built-in writers:
Writes to stdout (default). Automatically detects if output is a terminal.
logger := masterlog.New()
// ConsoleWriter is used by default
logger.Info("Console log")Writes log entries to a file.
Using with global logger (recommended):
// 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 loggerUsing with custom logger:
logger := masterlog.New()
fileWriter, err := masterlog.NewFileWriter("app.log")
if err != nil {
log.Fatal(err)
}
defer fileWriter.Close()
logger.AddWriter(fileWriter)
logger.Info("File log")You can write to multiple destinations simultaneously:
logger := masterlog.New()
// Add file writer
fileWriter, _ := masterlog.NewFileWriter("app.log")
defer fileWriter.Close()
logger.AddWriter(fileWriter)
// Logs will be written to both console and file
logger.Info("Multi-destination log")MasterLog provides two ways to use logging:
The global logger is pre-configured and ready to use. Configure it once at startup:
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
Create custom logger instances when you need different configurations for different components:
// Create logger with custom level
logger := masterlog.New(masterlog.LevelDebug)
// Add JSON encoder
logger.AddEncoder(&masterlog.JSONEncoder{})
// Add file writer
fileWriter, _ := masterlog.NewFileWriter("custom.log")
defer fileWriter.Close()
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
You can also replace the global logger with a custom instance:
// 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")Access the global logger instance directly if needed:
globalLogger := masterlog.GetDefaultLogger()
globalLogger.AddWriter(fileWriter)MasterLog includes built-in support for deterministic pseudonymization of sensitive data using HMAC-SHA256. This allows you to:
- Protect sensitive information in logs
- Maintain traceability (same input always produces same pseudonymized output)
- Comply with data protection regulations
// Create pseudonymizer with secret
pseudonymizer := masterlog.NewPseudonymizerFromString("your-secret-key")
logger := masterlog.New()
logger.SetPseudonymizer(pseudonymizer)
// Mark fields as sensitive
logger.AddSensitiveFields("user_id", "email", "ip", "ssn")
// Log with sensitive data
logger.Info("User logged in", map[string]interface{}{
"user_id": 12345, // Will be pseudonymized
"email": "user@ex.com", // Will be pseudonymized
"ip": "192.168.1.1", // Will be pseudonymized
"action": "login", // Not sensitive, remains unchanged
})Output:
2025-11-10T05:06:02+01:00 INF main.go:15 > User logged in user_id=a1b2c3d4 email=e5f6g7h8 ip=i9j0k1l2 action=login go_version=go1.25.3 pid=12345
The same input always produces the same pseudonymized output, allowing you to trace the same entity across multiple log entries:
logger.Info("User action 1", map[string]interface{}{
"user_id": 12345, // Pseudonymized to: a1b2c3d4
})
logger.Info("User action 2", map[string]interface{}{
"user_id": 12345, // Pseudonymized to: a1b2c3d4 (same as above)
})For production, use environment variables to store the secret:
// Read secret from environment variable
pseudonymizer := masterlog.NewPseudonymizerFromEnv("LOG_PSEUDONYMIZER_SECRET")
logger.SetPseudonymizer(pseudonymizer)Adjust the length of pseudonymized values (default: 8 hex characters):
pseudonymizer := masterlog.NewPseudonymizerFromString("secret")
pseudonymizer.SetHashLength(16) // Use 16 hex characters instead of 8// Add single field
logger.AddSensitiveField("user_id")
// Add multiple fields
logger.AddSensitiveFields("email", "ip", "ssn", "phone")
// Remove field (if needed)
pseudonymizer := masterlog.NewPseudonymizerFromString("secret")
pseudonymizer.RemoveSensitiveField("phone")- Secret Management: Store the pseudonymization secret securely (environment variables, secrets manager)
- Secret Rotation: Changing the secret will produce different pseudonymized values
- Hash Length: Longer hashes provide better uniqueness but are less readable
- Field Selection: Only mark truly sensitive fields to maintain log usefulness
MasterLog is designed to be easily extensible. You can create custom encoders and writers.
type CustomEncoder struct{}
func (e *CustomEncoder) Encode(entry masterlog.Entry) ([]byte, error) {
// Your custom encoding logic
return []byte("custom format"), nil
}
// Usage
logger := masterlog.New()
logger.AddEncoder(&CustomEncoder{})type CustomWriter struct {
// Your writer fields
}
func (w *CustomWriter) Write(data []byte) error {
// Your custom writing logic
return nil
}
func (w *CustomWriter) Close() error {
// Cleanup logic
return nil
}
func (w *CustomWriter) SupportsColors() bool {
return false // or true if your writer supports colors
}
// Usage
logger := masterlog.New()
logger.AddWriter(&CustomWriter{})func Trace(message string, fields ...map[string]interface{})
func Debug(message string, fields ...map[string]interface{})
func Info(message string, fields ...map[string]interface{})
func Warn(message string, fields ...map[string]interface{})
func Error(message string, fields ...map[string]interface{})func SetLevel(level Level)
func AddWriter(writer Writer)
func AddEncoder(encoder Encoder)
func SetPseudonymizer(pseudonymizer *Pseudonymizer)
func AddSensitiveField(fieldName string)
func AddSensitiveFields(fieldNames ...string)
func SetDefaultLogger(logger *MasterLogger)
func GetDefaultLogger() *MasterLoggertype MasterLogger struct {
// ...
}
func New(level ...Level) *MasterLogger
func (l *MasterLogger) SetLevel(level Level)
func (l *MasterLogger) AddWriter(writer Writer)
func (l *MasterLogger) AddEncoder(encoder Encoder)
func (l *MasterLogger) SetPseudonymizer(pseudonymizer *Pseudonymizer)
func (l *MasterLogger) AddSensitiveField(fieldName string)
func (l *MasterLogger) AddSensitiveFields(fieldNames ...string)
func (l *MasterLogger) Trace(message string, fields ...map[string]interface{})
func (l *MasterLogger) Debug(message string, fields ...map[string]interface{})
func (l *MasterLogger) Info(message string, fields ...map[string]interface{})
func (l *MasterLogger) Warn(message string, fields ...map[string]interface{})
func (l *MasterLogger) Error(message string, fields ...map[string]interface{})type Pseudonymizer struct {
// ...
}
func NewPseudonymizer(secret []byte) *Pseudonymizer
func NewPseudonymizerFromString(secret string) *Pseudonymizer
func NewPseudonymizerFromEnv(envVar string) *Pseudonymizer
func (p *Pseudonymizer) SetHashLength(length int) error
func (p *Pseudonymizer) AddSensitiveField(fieldName string)
func (p *Pseudonymizer) AddSensitiveFields(fieldNames ...string)
func (p *Pseudonymizer) RemoveSensitiveField(fieldName string)
func (p *Pseudonymizer) IsSensitive(fieldName string) bool
func (p *Pseudonymizer) Pseudonymize(value interface{}) string
func (p *Pseudonymizer) PseudonymizeFields(fields map[string]interface{}) map[string]interface{}func NewConsoleWriter() *ConsoleWriter
func NewFileWriter(filename string) (*FileWriter, error)func NewFormattedEncoder(useColors bool) *FormattedEncoder
// JSONEncoder has no constructor, use &masterlog.JSONEncoder{}Always use structured fields instead of string concatenation:
// ❌ Bad
masterlog.Info(fmt.Sprintf("User %d logged in from %s", userID, ip))
// ✅ Good
masterlog.Info("User logged in", map[string]interface{}{
"user_id": userID,
"ip": ip,
})Use log levels appropriately:
TRC: Detailed trace information (usually disabled in production)DBG: Debug information (disabled in production)INF: General information about application flowWRN: Warning conditions that don't stop executionERR: Error conditions that need attention
Always pseudonymize sensitive information:
logger.AddSensitiveFields("user_id", "email", "ip", "ssn", "credit_card")Never hardcode secrets:
// ❌ Bad
pseudonymizer := masterlog.NewPseudonymizerFromString("hardcoded-secret")
// ✅ Good
pseudonymizer := masterlog.NewPseudonymizerFromEnv("LOG_PSEUDONYMIZER_SECRET")Use different configurations for development and production:
logger := masterlog.New()
if os.Getenv("ENV") == "production" {
// Production: JSON to file, no colors
logger.AddEncoder(&masterlog.JSONEncoder{})
fileWriter, _ := masterlog.NewFileWriter("/var/log/app.log")
logger.AddWriter(fileWriter)
// Enable pseudonymization
pseudonymizer := masterlog.NewPseudonymizerFromEnv("LOG_SECRET")
logger.SetPseudonymizer(pseudonymizer)
logger.AddSensitiveFields("user_id", "email", "ip")
} else {
// Development: Formatted output with colors
logger.SetLevel(masterlog.LevelDebug)
}Always close file writers when done:
fileWriter, err := masterlog.NewFileWriter("app.log")
if err != nil {
return err
}
defer fileWriter.Close()Create separate logger instances for different components:
var (
apiLogger = masterlog.New(masterlog.LevelInfo)
dbLogger = masterlog.New(masterlog.LevelDebug)
authLogger = masterlog.New(masterlog.LevelWarn)
)See the example directory for complete working examples.
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a pull request.
Please feel free to open an issue if you have any questions or suggestions. You can also send an email to support@secnex.io.