feat(log): Formatted console logging with file output
This commit is contained in:
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...)
|
||||
}
|
||||
Reference in New Issue
Block a user