feat(log): Formatted console logging with file output #1
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 SecNex
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
580
README.md
580
README.md
@@ -1,2 +1,582 @@
|
|||||||
# MasterLog - Logging Library for Golang
|
# MasterLog - Logging Library for Golang
|
||||||
|
|
||||||
|
A powerful, extensible, and zero-dependency logging library for Go with built-in support for structured logging, colored output, and data pseudonymization.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🎨 **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
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get git.secnex.io/secnex/masterlog
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.secnex.io/secnex/masterlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Simple logging
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Log Levels](#log-levels)
|
||||||
|
- [Structured Logging](#structured-logging)
|
||||||
|
- [Colored Output](#colored-output)
|
||||||
|
- [Encoders](#encoders)
|
||||||
|
- [Writers](#writers)
|
||||||
|
- [Custom Logger Instances](#custom-logger-instances)
|
||||||
|
- [Data Pseudonymization](#data-pseudonymization)
|
||||||
|
- [Extending MasterLog](#extending-masterlog)
|
||||||
|
- [API Reference](#api-reference)
|
||||||
|
- [Best Practices](#best-practices)
|
||||||
|
|
||||||
|
## Log Levels
|
||||||
|
|
||||||
|
MasterLog supports five log levels:
|
||||||
|
|
||||||
|
- `TRC` (Trace) - Most verbose, for detailed debugging
|
||||||
|
- `DBG` (Debug) - Debug information
|
||||||
|
- `INF` (Info) - General informational messages
|
||||||
|
- `WRN` (Warn) - Warning messages
|
||||||
|
- `ERR` (Error) - Error messages
|
||||||
|
|
||||||
|
### Setting Log Level
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 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")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging Methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
masterlog.Trace("trace message")
|
||||||
|
masterlog.Debug("debug message")
|
||||||
|
masterlog.Info("info message")
|
||||||
|
masterlog.Warn("warning message")
|
||||||
|
masterlog.Error("error message")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structured Logging
|
||||||
|
|
||||||
|
MasterLog supports structured logging with custom fields. Custom fields are displayed before default fields (go_version, pid).
|
||||||
|
|
||||||
|
```go
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field Ordering
|
||||||
|
|
||||||
|
Custom fields (user-provided) are always displayed before default fields:
|
||||||
|
1. Custom fields (user-provided)
|
||||||
|
2. Default fields (go_version, pid)
|
||||||
|
|
||||||
|
## Colored Output
|
||||||
|
|
||||||
|
MasterLog automatically detects if output is going to a terminal and applies colors accordingly. Colors are never written to files.
|
||||||
|
|
||||||
|
### Color Scheme
|
||||||
|
|
||||||
|
- **Timestamp**: Dark gray
|
||||||
|
- **Log Level**:
|
||||||
|
- `TRC`: Gray
|
||||||
|
- `DBG`: Cyan
|
||||||
|
- `INF`: Green
|
||||||
|
- `WRN`: Yellow
|
||||||
|
- `ERR`: Red
|
||||||
|
- **File:Line**: Dark gray
|
||||||
|
- **Message**: White
|
||||||
|
- **Field Keys**: Turquoise
|
||||||
|
- **Field Values**: White
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
|
||||||
|
When logging to a terminal, you'll see beautifully colored output. When redirecting to a file or non-terminal, colors are automatically disabled.
|
||||||
|
|
||||||
|
```go
|
||||||
|
masterlog.Info("Colored output example", map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Encoders
|
||||||
|
|
||||||
|
Encoders determine the format of log entries. MasterLog provides two built-in encoders:
|
||||||
|
|
||||||
|
### FormattedEncoder
|
||||||
|
|
||||||
|
The default encoder that produces human-readable formatted text.
|
||||||
|
|
||||||
|
```go
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSONEncoder
|
||||||
|
|
||||||
|
Produces JSON-formatted log entries, perfect for log aggregation systems.
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger := masterlog.New()
|
||||||
|
logger.AddEncoder(&masterlog.JSONEncoder{})
|
||||||
|
logger.Info("JSON log", map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
```json
|
||||||
|
{"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.
|
||||||
|
|
||||||
|
### Multiple Encoders
|
||||||
|
|
||||||
|
You can use multiple encoders simultaneously:
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger := masterlog.New()
|
||||||
|
logger.AddEncoder(&masterlog.JSONEncoder{})
|
||||||
|
// Now logs will be written in both formatted and JSON formats
|
||||||
|
logger.Info("Dual format log")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writers
|
||||||
|
|
||||||
|
Writers determine where log entries are written. MasterLog provides two built-in writers:
|
||||||
|
|
||||||
|
### ConsoleWriter
|
||||||
|
|
||||||
|
Writes to stdout (default). Automatically detects if output is a terminal.
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger := masterlog.New()
|
||||||
|
// ConsoleWriter is used by default
|
||||||
|
logger.Info("Console log")
|
||||||
|
```
|
||||||
|
|
||||||
|
### FileWriter
|
||||||
|
|
||||||
|
Writes log entries to a file.
|
||||||
|
|
||||||
|
```go
|
||||||
|
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")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Writers
|
||||||
|
|
||||||
|
You can write to multiple destinations simultaneously:
|
||||||
|
|
||||||
|
```go
|
||||||
|
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")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Logger Instances
|
||||||
|
|
||||||
|
Create custom logger instances with specific configurations:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 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")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Pseudonymization
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deterministic Behavior
|
||||||
|
|
||||||
|
The same input always produces the same pseudonymized output, allowing you to trace the same entity across multiple log entries:
|
||||||
|
|
||||||
|
```go
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
#### Using Environment Variables
|
||||||
|
|
||||||
|
For production, use environment variables to store the secret:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Read secret from environment variable
|
||||||
|
pseudonymizer := masterlog.NewPseudonymizerFromEnv("LOG_PSEUDONYMIZER_SECRET")
|
||||||
|
logger.SetPseudonymizer(pseudonymizer)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom Hash Length
|
||||||
|
|
||||||
|
Adjust the length of pseudonymized values (default: 8 hex characters):
|
||||||
|
|
||||||
|
```go
|
||||||
|
pseudonymizer := masterlog.NewPseudonymizerFromString("secret")
|
||||||
|
pseudonymizer.SetHashLength(16) // Use 16 hex characters instead of 8
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Managing Sensitive Fields
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 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")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
|
||||||
|
1. **Secret Management**: Store the pseudonymization secret securely (environment variables, secrets manager)
|
||||||
|
2. **Secret Rotation**: Changing the secret will produce different pseudonymized values
|
||||||
|
3. **Hash Length**: Longer hashes provide better uniqueness but are less readable
|
||||||
|
4. **Field Selection**: Only mark truly sensitive fields to maintain log usefulness
|
||||||
|
|
||||||
|
## Extending MasterLog
|
||||||
|
|
||||||
|
MasterLog is designed to be easily extensible. You can create custom encoders and writers.
|
||||||
|
|
||||||
|
### Custom Encoder
|
||||||
|
|
||||||
|
```go
|
||||||
|
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{})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Writer
|
||||||
|
|
||||||
|
```go
|
||||||
|
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{})
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Package-Level Functions
|
||||||
|
|
||||||
|
#### Logging Functions
|
||||||
|
|
||||||
|
```go
|
||||||
|
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{})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configuration Functions
|
||||||
|
|
||||||
|
```go
|
||||||
|
func SetLevel(level Level)
|
||||||
|
func AddWriter(writer Writer)
|
||||||
|
func AddEncoder(encoder Encoder)
|
||||||
|
func SetPseudonymizer(pseudonymizer *Pseudonymizer)
|
||||||
|
func AddSensitiveField(fieldName string)
|
||||||
|
func AddSensitiveFields(fieldNames ...string)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logger Methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
type 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{})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pseudonymizer
|
||||||
|
|
||||||
|
```go
|
||||||
|
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{}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writers
|
||||||
|
|
||||||
|
```go
|
||||||
|
func NewConsoleWriter() *ConsoleWriter
|
||||||
|
func NewFileWriter(filename string) (*FileWriter, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Encoders
|
||||||
|
|
||||||
|
```go
|
||||||
|
func NewFormattedEncoder(useColors bool) *FormattedEncoder
|
||||||
|
// JSONEncoder has no constructor, use &masterlog.JSONEncoder{}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use Structured Logging
|
||||||
|
|
||||||
|
Always use structured fields instead of string concatenation:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ❌ 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,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Set Appropriate Log Levels
|
||||||
|
|
||||||
|
Use log levels appropriately:
|
||||||
|
|
||||||
|
- `TRC`: Detailed trace information (usually disabled in production)
|
||||||
|
- `DBG`: Debug information (disabled in production)
|
||||||
|
- `INF`: General information about application flow
|
||||||
|
- `WRN`: Warning conditions that don't stop execution
|
||||||
|
- `ERR`: Error conditions that need attention
|
||||||
|
|
||||||
|
### 3. Pseudonymize Sensitive Data
|
||||||
|
|
||||||
|
Always pseudonymize sensitive information:
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger.AddSensitiveFields("user_id", "email", "ip", "ssn", "credit_card")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Use Environment Variables for Secrets
|
||||||
|
|
||||||
|
Never hardcode secrets:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ❌ Bad
|
||||||
|
pseudonymizer := masterlog.NewPseudonymizerFromString("hardcoded-secret")
|
||||||
|
|
||||||
|
// ✅ Good
|
||||||
|
pseudonymizer := masterlog.NewPseudonymizerFromEnv("LOG_PSEUDONYMIZER_SECRET")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Separate Logs by Environment
|
||||||
|
|
||||||
|
Use different configurations for development and production:
|
||||||
|
|
||||||
|
```go
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Close File Writers
|
||||||
|
|
||||||
|
Always close file writers when done:
|
||||||
|
|
||||||
|
```go
|
||||||
|
fileWriter, err := masterlog.NewFileWriter("app.log")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fileWriter.Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Use Custom Logger Instances
|
||||||
|
|
||||||
|
Create separate logger instances for different components:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var (
|
||||||
|
apiLogger = masterlog.New(masterlog.LevelInfo)
|
||||||
|
dbLogger = masterlog.New(masterlog.LevelDebug)
|
||||||
|
authLogger = masterlog.New(masterlog.LevelWarn)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See the [example](example/) directory for complete working examples.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a pull request.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Please feel free to open an issue if you have any questions or suggestions. You can also send an email to support@secnex.io.
|
||||||
|
|||||||
22
colors.go
Normal file
22
colors.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package masterlog
|
||||||
|
|
||||||
|
// ANSI color codes
|
||||||
|
const (
|
||||||
|
colorReset = "\033[0m"
|
||||||
|
colorGray = "\033[90m"
|
||||||
|
colorLightGray = "\033[90m" // Dark gray for timestamp (dimmer)
|
||||||
|
colorCyan = "\033[36m"
|
||||||
|
colorTurquoise = "\033[36m" // Turquoise for field keys
|
||||||
|
colorGreen = "\033[32m"
|
||||||
|
colorYellow = "\033[33m"
|
||||||
|
colorRed = "\033[31m"
|
||||||
|
colorWhite = "\033[37m" // White for field values
|
||||||
|
)
|
||||||
|
|
||||||
|
var levelColors = map[Level]string{
|
||||||
|
LevelTrace: colorGray,
|
||||||
|
LevelDebug: colorCyan,
|
||||||
|
LevelInfo: colorGreen,
|
||||||
|
LevelWarn: colorYellow,
|
||||||
|
LevelError: colorRed,
|
||||||
|
}
|
||||||
105
encoder.go
Normal file
105
encoder.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package masterlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getShortFile extracts the filename from a full path
|
||||||
|
func getShortFile(file string) string {
|
||||||
|
for i := len(file) - 1; i >= 0; i-- {
|
||||||
|
if file[i] == '/' {
|
||||||
|
return file[i+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormattedEncoder encodes entries in the formatted text format
|
||||||
|
type FormattedEncoder struct {
|
||||||
|
useColors bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFormattedEncoder creates a new FormattedEncoder
|
||||||
|
func NewFormattedEncoder(useColors bool) *FormattedEncoder {
|
||||||
|
return &FormattedEncoder{useColors: useColors}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FormattedEncoder) Encode(entry Entry) ([]byte, error) {
|
||||||
|
// Format: 2022-09-01T10:05:03+01:00 TRC main.go:23 trace message go_version=go1.19 pid=2620043
|
||||||
|
format := entry.Timestamp.Format(time.RFC3339)
|
||||||
|
levelStr := levelNames[entry.Level]
|
||||||
|
shortFile := getShortFile(entry.File)
|
||||||
|
|
||||||
|
var result string
|
||||||
|
if e.useColors {
|
||||||
|
// Timestamp in light gray
|
||||||
|
timestampColor := colorLightGray
|
||||||
|
// Level in its specific color
|
||||||
|
levelColor := levelColors[entry.Level]
|
||||||
|
// File:line in light gray
|
||||||
|
fileColor := colorLightGray
|
||||||
|
// Separator in light gray
|
||||||
|
separator := ">"
|
||||||
|
// Message in white
|
||||||
|
messageColor := colorWhite
|
||||||
|
|
||||||
|
result = fmt.Sprintf("%s%s%s %s%s%s %s%s%s:%d %s%s%s %s%s%s",
|
||||||
|
timestampColor, format, colorReset,
|
||||||
|
levelColor, levelStr, colorReset,
|
||||||
|
fileColor, shortFile, colorReset, entry.Line,
|
||||||
|
fileColor, separator, colorReset,
|
||||||
|
messageColor, entry.Message, colorReset)
|
||||||
|
} else {
|
||||||
|
result = fmt.Sprintf("%s %s %s:%d > %s", format, levelStr, shortFile, entry.Line, entry.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add fields - first custom fields, then default fields
|
||||||
|
// Custom fields (user-provided)
|
||||||
|
for key, value := range entry.CustomFields {
|
||||||
|
if e.useColors {
|
||||||
|
// Field key in turquoise, value in white
|
||||||
|
result += fmt.Sprintf(" %s%s%s=%s%v%s", colorTurquoise, key, colorReset, colorWhite, value, colorReset)
|
||||||
|
} else {
|
||||||
|
result += fmt.Sprintf(" %s=%v", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default fields (go_version, pid)
|
||||||
|
for key, value := range entry.DefaultFields {
|
||||||
|
if e.useColors {
|
||||||
|
// Field key in turquoise, value in white
|
||||||
|
result += fmt.Sprintf(" %s%s%s=%s%v%s", colorTurquoise, key, colorReset, colorWhite, value, colorReset)
|
||||||
|
} else {
|
||||||
|
result += fmt.Sprintf(" %s=%v", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result += "\n"
|
||||||
|
return []byte(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONEncoder encodes entries in JSON format
|
||||||
|
type JSONEncoder struct{}
|
||||||
|
|
||||||
|
func (e *JSONEncoder) Encode(entry Entry) ([]byte, error) {
|
||||||
|
type LogEntry struct {
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
File string `json:"file"`
|
||||||
|
Line int `json:"line"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Fields map[string]interface{} `json:"fields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry := LogEntry{
|
||||||
|
Timestamp: entry.Timestamp.Format(time.RFC3339),
|
||||||
|
Level: levelNames[entry.Level],
|
||||||
|
File: getShortFile(entry.File),
|
||||||
|
Line: entry.Line,
|
||||||
|
Message: entry.Message,
|
||||||
|
Fields: entry.Fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(logEntry)
|
||||||
|
}
|
||||||
10
example/app.log
Normal file
10
example/app.log
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
7
example/go.mod
Normal file
7
example/go.mod
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module example
|
||||||
|
|
||||||
|
go 1.25.3
|
||||||
|
|
||||||
|
replace git.secnex.io/secnex/masterlog => ../
|
||||||
|
|
||||||
|
require git.secnex.io/secnex/masterlog v0.0.0-00010101000000-000000000000
|
||||||
83
example/main.go
Normal file
83
example/main.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.secnex.io/secnex/masterlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Example 1: Simple formatted logging to console (with colors if terminal)
|
||||||
|
masterlog.Info("Hello, World!")
|
||||||
|
|
||||||
|
// Example 2: Logging with fields
|
||||||
|
masterlog.Info("User logged in", map[string]interface{}{
|
||||||
|
"user_id": 12345,
|
||||||
|
"ip": "192.168.1.1",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Example 3: Different log levels (colored output)
|
||||||
|
masterlog.Trace("trace message")
|
||||||
|
masterlog.Debug("debug message")
|
||||||
|
masterlog.Info("info message")
|
||||||
|
masterlog.Warn("warning message")
|
||||||
|
masterlog.Error("error message")
|
||||||
|
|
||||||
|
// Example 4: Create logger with specific log level
|
||||||
|
traceLogger := masterlog.New(masterlog.LevelTrace)
|
||||||
|
traceLogger.Info("Logger created with LevelTrace")
|
||||||
|
|
||||||
|
// Example 5: Create custom logger with JSON encoder
|
||||||
|
jsonLogger := masterlog.New()
|
||||||
|
jsonLogger.AddEncoder(&masterlog.JSONEncoder{})
|
||||||
|
jsonLogger.Info("JSON formatted log", 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 7: Logger with multiple encoders (formatted + JSON)
|
||||||
|
multiLogger := masterlog.New()
|
||||||
|
multiLogger.AddEncoder(&masterlog.JSONEncoder{})
|
||||||
|
multiLogger.Info("This will be logged in both formats", map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Example 8: Set log level dynamically
|
||||||
|
masterlog.SetLevel(masterlog.LevelDebug)
|
||||||
|
masterlog.Trace("This won't be logged (level too low)")
|
||||||
|
masterlog.Debug("This will be logged")
|
||||||
|
|
||||||
|
// Example 9: Create logger with custom level and colored output
|
||||||
|
debugLogger := masterlog.New(masterlog.LevelDebug)
|
||||||
|
debugLogger.Debug("Debug logger with colors")
|
||||||
|
debugLogger.Trace("This won't be logged")
|
||||||
|
|
||||||
|
// Example 10: Pseudonymization of sensitive data
|
||||||
|
pseudoLogger := masterlog.New()
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Mark fields as sensitive
|
||||||
|
pseudoLogger.AddSensitiveFields("user_id", "email", "ip")
|
||||||
|
|
||||||
|
// Log with sensitive data - it will be pseudonymized
|
||||||
|
pseudoLogger.Info("User logged in", map[string]interface{}{
|
||||||
|
"user_id": 12345,
|
||||||
|
"email": "user@example.com",
|
||||||
|
"ip": "192.168.1.1",
|
||||||
|
"action": "login", // This field is not sensitive, so it won't be pseudonymized
|
||||||
|
})
|
||||||
|
|
||||||
|
// Same user_id will produce the same pseudonymized value (deterministic)
|
||||||
|
pseudoLogger.Info("User performed action", map[string]interface{}{
|
||||||
|
"user_id": 12345, // Same value, same pseudonymized output
|
||||||
|
"action": "purchase",
|
||||||
|
})
|
||||||
|
}
|
||||||
9
level.go
Normal file
9
level.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package masterlog
|
||||||
|
|
||||||
|
var levelNames = map[Level]string{
|
||||||
|
LevelTrace: "TRC",
|
||||||
|
LevelDebug: "DBG",
|
||||||
|
LevelInfo: "INF",
|
||||||
|
LevelWarn: "WRN",
|
||||||
|
LevelError: "ERR",
|
||||||
|
}
|
||||||
237
logger.go
Normal file
237
logger.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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...)
|
||||||
|
}
|
||||||
133
pseudonymizer.go
Normal file
133
pseudonymizer.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package masterlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pseudonymizer handles deterministic pseudonymization of sensitive data
|
||||||
|
type Pseudonymizer struct {
|
||||||
|
secret []byte
|
||||||
|
mu sync.RWMutex
|
||||||
|
// sensitiveFields is a set of field names that should be pseudonymized
|
||||||
|
sensitiveFields map[string]bool
|
||||||
|
// hashLength is the length of the pseudonymized value (in hex characters)
|
||||||
|
hashLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPseudonymizer creates a new pseudonymizer with the given secret
|
||||||
|
// The secret should be kept secure and consistent across application restarts
|
||||||
|
// to ensure deterministic pseudonymization
|
||||||
|
func NewPseudonymizer(secret []byte) *Pseudonymizer {
|
||||||
|
return &Pseudonymizer{
|
||||||
|
secret: secret,
|
||||||
|
sensitiveFields: make(map[string]bool),
|
||||||
|
hashLength: 8, // Default: 8 hex characters (4 bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPseudonymizerFromString creates a new pseudonymizer from a string secret
|
||||||
|
func NewPseudonymizerFromString(secret string) *Pseudonymizer {
|
||||||
|
return NewPseudonymizer([]byte(secret))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPseudonymizerFromEnv(envVar string) *Pseudonymizer {
|
||||||
|
secret := os.Getenv(envVar)
|
||||||
|
if secret == "" {
|
||||||
|
panic(fmt.Errorf("secret not found in environment variable %s", envVar))
|
||||||
|
}
|
||||||
|
return NewPseudonymizerFromString(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHashLength sets the length of the pseudonymized hash (in hex characters)
|
||||||
|
// Default is 8. Must be between 4 and 64 (max SHA256 hex length)
|
||||||
|
func (p *Pseudonymizer) SetHashLength(length int) error {
|
||||||
|
if length < 4 || length > 64 {
|
||||||
|
return fmt.Errorf("hash length must be between 4 and 64, got %d", length)
|
||||||
|
}
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
p.hashLength = length
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSensitiveField marks a field name as sensitive, so it will be pseudonymized
|
||||||
|
func (p *Pseudonymizer) AddSensitiveField(fieldName string) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
p.sensitiveFields[fieldName] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSensitiveFields marks multiple field names as sensitive
|
||||||
|
func (p *Pseudonymizer) AddSensitiveFields(fieldNames ...string) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
for _, name := range fieldNames {
|
||||||
|
p.sensitiveFields[name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveSensitiveField removes a field from the sensitive fields list
|
||||||
|
func (p *Pseudonymizer) RemoveSensitiveField(fieldName string) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
delete(p.sensitiveFields, fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSensitive checks if a field name is marked as sensitive
|
||||||
|
func (p *Pseudonymizer) IsSensitive(fieldName string) bool {
|
||||||
|
p.mu.RLock()
|
||||||
|
defer p.mu.RUnlock()
|
||||||
|
return p.sensitiveFields[fieldName]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pseudonymize deterministically pseudonymizes a value using HMAC-SHA256
|
||||||
|
// The same input will always produce the same output (deterministic)
|
||||||
|
func (p *Pseudonymizer) Pseudonymize(value interface{}) string {
|
||||||
|
p.mu.RLock()
|
||||||
|
hashLength := p.hashLength
|
||||||
|
secret := p.secret
|
||||||
|
p.mu.RUnlock()
|
||||||
|
|
||||||
|
// Convert value to string
|
||||||
|
valueStr := fmt.Sprintf("%v", value)
|
||||||
|
|
||||||
|
// Create HMAC-SHA256 hash
|
||||||
|
h := hmac.New(sha256.New, secret)
|
||||||
|
h.Write([]byte(valueStr))
|
||||||
|
hash := h.Sum(nil)
|
||||||
|
|
||||||
|
// Convert to hex and truncate to desired length
|
||||||
|
hexHash := hex.EncodeToString(hash)
|
||||||
|
if len(hexHash) > hashLength {
|
||||||
|
hexHash = hexHash[:hashLength]
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// PseudonymizeFields applies pseudonymization to sensitive fields in a map
|
||||||
|
func (p *Pseudonymizer) PseudonymizeFields(fields map[string]interface{}) map[string]interface{} {
|
||||||
|
p.mu.RLock()
|
||||||
|
sensitiveFields := make(map[string]bool)
|
||||||
|
for k, v := range p.sensitiveFields {
|
||||||
|
sensitiveFields[k] = v
|
||||||
|
}
|
||||||
|
p.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
for key, value := range fields {
|
||||||
|
if sensitiveFields[key] {
|
||||||
|
// Pseudonymize sensitive fields
|
||||||
|
result[key] = p.Pseudonymize(value)
|
||||||
|
} else {
|
||||||
|
// Keep non-sensitive fields as-is
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
53
types.go
Normal file
53
types.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package masterlog
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Level represents the log level
|
||||||
|
type Level int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LevelTrace Level = iota
|
||||||
|
LevelDebug
|
||||||
|
LevelInfo
|
||||||
|
LevelWarn
|
||||||
|
LevelError
|
||||||
|
)
|
||||||
|
|
||||||
|
// Entry represents a log entry
|
||||||
|
type Entry struct {
|
||||||
|
Timestamp time.Time
|
||||||
|
Level Level
|
||||||
|
File string
|
||||||
|
Line int
|
||||||
|
Message string
|
||||||
|
Fields map[string]interface{} // All fields
|
||||||
|
CustomFields map[string]interface{} // User-provided fields (to be shown first)
|
||||||
|
DefaultFields map[string]interface{} // Default fields (go_version, pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoder encodes log entries to a specific format
|
||||||
|
type Encoder interface {
|
||||||
|
Encode(entry Entry) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer writes encoded log entries
|
||||||
|
type Writer interface {
|
||||||
|
Write(data []byte) error
|
||||||
|
Close() error
|
||||||
|
SupportsColors() bool // Returns true if the writer supports ANSI color codes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger is the main logger interface
|
||||||
|
type Logger interface {
|
||||||
|
Trace(message string, fields ...map[string]interface{})
|
||||||
|
Debug(message string, fields ...map[string]interface{})
|
||||||
|
Info(message string, fields ...map[string]interface{})
|
||||||
|
Warn(message string, fields ...map[string]interface{})
|
||||||
|
Error(message string, fields ...map[string]interface{})
|
||||||
|
SetLevel(level Level)
|
||||||
|
AddWriter(writer Writer)
|
||||||
|
AddEncoder(encoder Encoder)
|
||||||
|
SetPseudonymizer(pseudonymizer *Pseudonymizer)
|
||||||
|
AddSensitiveField(fieldName string)
|
||||||
|
AddSensitiveFields(fieldNames ...string)
|
||||||
|
}
|
||||||
84
writer.go
Normal file
84
writer.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package masterlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// isTerminal checks if the writer is a terminal (TTY)
|
||||||
|
func isTerminal(w io.Writer) bool {
|
||||||
|
file, ok := w.(*os.File)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a terminal by trying to get file info
|
||||||
|
stat, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a character device (terminal)
|
||||||
|
return (stat.Mode() & os.ModeCharDevice) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsoleWriter writes to stdout
|
||||||
|
type ConsoleWriter struct {
|
||||||
|
writer io.Writer
|
||||||
|
isTerminal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConsoleWriter() *ConsoleWriter {
|
||||||
|
writer := os.Stdout
|
||||||
|
return &ConsoleWriter{
|
||||||
|
writer: writer,
|
||||||
|
isTerminal: isTerminal(writer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ConsoleWriter) Write(data []byte) error {
|
||||||
|
_, err := w.writer.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ConsoleWriter) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ConsoleWriter) SupportsColors() bool {
|
||||||
|
return w.isTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileWriter writes to a file
|
||||||
|
type FileWriter struct {
|
||||||
|
file *os.File
|
||||||
|
writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileWriter(filename string) (*FileWriter, error) {
|
||||||
|
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FileWriter{
|
||||||
|
file: file,
|
||||||
|
writer: file,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *FileWriter) Write(data []byte) error {
|
||||||
|
_, err := w.writer.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *FileWriter) Close() error {
|
||||||
|
if w.file != nil {
|
||||||
|
return w.file.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *FileWriter) SupportsColors() bool {
|
||||||
|
return false // File writers never support colors
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user