feat(log): Formatted console logging with file output
This commit is contained in:
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
|
||||
|
||||
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