e1.9.0 Structured JSON log method added, refactored.

This commit is contained in:
2025-07-14 20:49:22 -04:00
parent 146141a38e
commit b0d26a313d
9 changed files with 138 additions and 159 deletions

View File

@ -48,7 +48,7 @@ func WithLevelDetector(detector func(string) int64) FastHTTPOption {
} }
// Printf implements fasthttp's Logger interface // Printf implements fasthttp's Logger interface
func (a *FastHTTPAdapter) Printf(format string, args ...interface{}) { func (a *FastHTTPAdapter) Printf(format string, args ...any) {
msg := fmt.Sprintf(format, args...) msg := fmt.Sprintf(format, args...)
// Detect log level from message content // Detect log level from message content

View File

@ -42,31 +42,31 @@ func WithFatalHandler(handler func(string)) GnetOption {
} }
// Debugf logs at debug level with printf-style formatting // Debugf logs at debug level with printf-style formatting
func (a *GnetAdapter) Debugf(format string, args ...interface{}) { func (a *GnetAdapter) Debugf(format string, args ...any) {
msg := fmt.Sprintf(format, args...) msg := fmt.Sprintf(format, args...)
a.logger.Debug("msg", msg, "source", "gnet") a.logger.Debug("msg", msg, "source", "gnet")
} }
// Infof logs at info level with printf-style formatting // Infof logs at info level with printf-style formatting
func (a *GnetAdapter) Infof(format string, args ...interface{}) { func (a *GnetAdapter) Infof(format string, args ...any) {
msg := fmt.Sprintf(format, args...) msg := fmt.Sprintf(format, args...)
a.logger.Info("msg", msg, "source", "gnet") a.logger.Info("msg", msg, "source", "gnet")
} }
// Warnf logs at warn level with printf-style formatting // Warnf logs at warn level with printf-style formatting
func (a *GnetAdapter) Warnf(format string, args ...interface{}) { func (a *GnetAdapter) Warnf(format string, args ...any) {
msg := fmt.Sprintf(format, args...) msg := fmt.Sprintf(format, args...)
a.logger.Warn("msg", msg, "source", "gnet") a.logger.Warn("msg", msg, "source", "gnet")
} }
// Errorf logs at error level with printf-style formatting // Errorf logs at error level with printf-style formatting
func (a *GnetAdapter) Errorf(format string, args ...interface{}) { func (a *GnetAdapter) Errorf(format string, args ...any) {
msg := fmt.Sprintf(format, args...) msg := fmt.Sprintf(format, args...)
a.logger.Error("msg", msg, "source", "gnet") a.logger.Error("msg", msg, "source", "gnet")
} }
// Fatalf logs at error level and triggers fatal handler // Fatalf logs at error level and triggers fatal handler
func (a *GnetAdapter) Fatalf(format string, args ...interface{}) { func (a *GnetAdapter) Fatalf(format string, args ...any) {
msg := fmt.Sprintf(format, args...) msg := fmt.Sprintf(format, args...)
a.logger.Error("msg", msg, "source", "gnet", "fatal", true) a.logger.Error("msg", msg, "source", "gnet", "fatal", true)

View File

@ -11,18 +11,18 @@ import (
// parseFormat attempts to extract structured fields from printf-style format strings // parseFormat attempts to extract structured fields from printf-style format strings
// This is useful for preserving structured logging semantics // This is useful for preserving structured logging semantics
func parseFormat(format string, args []interface{}) []interface{} { func parseFormat(format string, args []any) []any {
// Pattern to detect common structured patterns like "key=%v" or "key: %v" // Pattern to detect common structured patterns like "key=%v" or "key: %v"
keyValuePattern := regexp.MustCompile(`(\w+)\s*[:=]\s*%[vsdqxXeEfFgGpbcU]`) keyValuePattern := regexp.MustCompile(`(\w+)\s*[:=]\s*%[vsdqxXeEfFgGpbcU]`)
matches := keyValuePattern.FindAllStringSubmatchIndex(format, -1) matches := keyValuePattern.FindAllStringSubmatchIndex(format, -1)
if len(matches) == 0 || len(matches) > len(args) { if len(matches) == 0 || len(matches) > len(args) {
// Fallback to simple message if pattern doesn't match // Fallback to simple message if pattern doesn't match
return []interface{}{"msg", fmt.Sprintf(format, args...)} return []any{"msg", fmt.Sprintf(format, args...)}
} }
// Build structured fields // Build structured fields
fields := make([]interface{}, 0, len(matches)*2+2) fields := make([]any, 0, len(matches)*2+2)
lastEnd := 0 lastEnd := 0
argIndex := 0 argIndex := 0
@ -91,7 +91,7 @@ func NewStructuredGnetAdapter(logger *log.Logger, opts ...GnetOption) *Structure
} }
// Debugf logs with structured field extraction // Debugf logs with structured field extraction
func (a *StructuredGnetAdapter) Debugf(format string, args ...interface{}) { func (a *StructuredGnetAdapter) Debugf(format string, args ...any) {
if a.extractFields { if a.extractFields {
fields := parseFormat(format, args) fields := parseFormat(format, args)
a.logger.Debug(append(fields, "source", "gnet")...) a.logger.Debug(append(fields, "source", "gnet")...)
@ -101,7 +101,7 @@ func (a *StructuredGnetAdapter) Debugf(format string, args ...interface{}) {
} }
// Infof logs with structured field extraction // Infof logs with structured field extraction
func (a *StructuredGnetAdapter) Infof(format string, args ...interface{}) { func (a *StructuredGnetAdapter) Infof(format string, args ...any) {
if a.extractFields { if a.extractFields {
fields := parseFormat(format, args) fields := parseFormat(format, args)
a.logger.Info(append(fields, "source", "gnet")...) a.logger.Info(append(fields, "source", "gnet")...)
@ -111,7 +111,7 @@ func (a *StructuredGnetAdapter) Infof(format string, args ...interface{}) {
} }
// Warnf logs with structured field extraction // Warnf logs with structured field extraction
func (a *StructuredGnetAdapter) Warnf(format string, args ...interface{}) { func (a *StructuredGnetAdapter) Warnf(format string, args ...any) {
if a.extractFields { if a.extractFields {
fields := parseFormat(format, args) fields := parseFormat(format, args)
a.logger.Warn(append(fields, "source", "gnet")...) a.logger.Warn(append(fields, "source", "gnet")...)
@ -121,7 +121,7 @@ func (a *StructuredGnetAdapter) Warnf(format string, args ...interface{}) {
} }
// Errorf logs with structured field extraction // Errorf logs with structured field extraction
func (a *StructuredGnetAdapter) Errorf(format string, args ...interface{}) { func (a *StructuredGnetAdapter) Errorf(format string, args ...any) {
if a.extractFields { if a.extractFields {
fields := parseFormat(format, args) fields := parseFormat(format, args)
a.logger.Error(append(fields, "source", "gnet")...) a.logger.Error(append(fields, "source", "gnet")...)

View File

@ -63,11 +63,11 @@ type GnetAdapter struct {
} }
// Methods implemented: // Methods implemented:
// - Debugf(format string, args ...interface{}) // - Debugf(format string, args ...any)
// - Infof(format string, args ...interface{}) // - Infof(format string, args ...any)
// - Warnf(format string, args ...interface{}) // - Warnf(format string, args ...any)
// - Errorf(format string, args ...interface{}) // - Errorf(format string, args ...any)
// - Fatalf(format string, args ...interface{}) // - Fatalf(format string, args ...any)
``` ```
### Custom Fatal Behavior ### Custom Fatal Behavior

174
format.go
View File

@ -4,8 +4,8 @@ package log
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -41,7 +41,12 @@ func (s *serializer) serialize(format string, flags int64, timestamp time.Time,
return s.serializeRaw(args) return s.serializeRaw(args)
} }
// 2. Handle the instance-wide configuration setting // 2. Check for structured JSON flag
if flags&FlagStructuredJSON != 0 && format == "json" {
return s.serializeStructuredJSON(flags, timestamp, level, trace, args)
}
// 3. Handle the instance-wide configuration setting
if format == "raw" { if format == "raw" {
return s.serializeRaw(args) return s.serializeRaw(args)
} }
@ -122,86 +127,6 @@ func (s *serializer) writeRawValue(v any) {
} }
} }
// This is the safe, dependency-free replacement for fmt.Sprintf.
func (s *serializer) reflectValue(v reflect.Value) {
// Safely handle invalid, nil pointer, or nil interface values.
if !v.IsValid() {
s.buf = append(s.buf, "nil"...)
return
}
// Dereference pointers and interfaces to get the concrete value.
// Recurse to handle multiple levels of pointers.
kind := v.Kind()
if kind == reflect.Ptr || kind == reflect.Interface {
if v.IsNil() {
s.buf = append(s.buf, "nil"...)
return
}
s.reflectValue(v.Elem())
return
}
switch kind {
case reflect.String:
s.buf = append(s.buf, v.String()...)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
s.buf = strconv.AppendInt(s.buf, v.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
s.buf = strconv.AppendUint(s.buf, v.Uint(), 10)
case reflect.Float32, reflect.Float64:
s.buf = strconv.AppendFloat(s.buf, v.Float(), 'f', -1, 64)
case reflect.Bool:
s.buf = strconv.AppendBool(s.buf, v.Bool())
case reflect.Slice, reflect.Array:
// Check if it's a byte slice ([]uint8) and hex-encode it for safety.
if v.Type().Elem().Kind() == reflect.Uint8 {
s.buf = append(s.buf, "0x"...)
s.buf = hex.AppendEncode(s.buf, v.Bytes())
return
}
s.buf = append(s.buf, '[')
for i := 0; i < v.Len(); i++ {
if i > 0 {
s.buf = append(s.buf, ' ')
}
s.reflectValue(v.Index(i))
}
s.buf = append(s.buf, ']')
case reflect.Struct:
s.buf = append(s.buf, '{')
for i := 0; i < v.NumField(); i++ {
if !v.Type().Field(i).IsExported() {
continue // Skip unexported fields
}
if i > 0 {
s.buf = append(s.buf, ' ')
}
s.buf = append(s.buf, v.Type().Field(i).Name...)
s.buf = append(s.buf, ':')
s.reflectValue(v.Field(i))
}
s.buf = append(s.buf, '}')
case reflect.Map:
s.buf = append(s.buf, '{')
for i, key := range v.MapKeys() {
if i > 0 {
s.buf = append(s.buf, ' ')
}
s.reflectValue(key)
s.buf = append(s.buf, ':')
s.reflectValue(v.MapIndex(key))
}
s.buf = append(s.buf, '}')
default:
// As a final fallback, use fmt, but this should rarely be hit.
s.buf = append(s.buf, fmt.Sprint(v.Interface())...)
}
}
// serializeJSON formats log entries as JSON (time, level, trace, fields). // serializeJSON formats log entries as JSON (time, level, trace, fields).
func (s *serializer) serializeJSON(flags int64, timestamp time.Time, level int64, trace string, args []any) []byte { func (s *serializer) serializeJSON(flags int64, timestamp time.Time, level int64, trace string, args []any) []byte {
s.buf = append(s.buf, '{') s.buf = append(s.buf, '{')
@ -390,6 +315,91 @@ func (s *serializer) writeJSONValue(v any) {
} }
} }
// serializeStructuredJSON formats log entries as structured JSON with proper field marshaling
func (s *serializer) serializeStructuredJSON(flags int64, timestamp time.Time, level int64, trace string, args []any) []byte {
// Validate args structure
if len(args) < 2 {
// Fallback to regular JSON if args are malformed
return s.serializeJSON(flags, timestamp, level, trace, args)
}
message, ok := args[0].(string)
if !ok {
// Fallback if message is not a string
return s.serializeJSON(flags, timestamp, level, trace, args)
}
fields, ok := args[1].(map[string]any)
if !ok {
// Fallback if fields is not a map
return s.serializeJSON(flags, timestamp, level, trace, args)
}
s.buf = append(s.buf, '{')
needsComma := false
// Add timestamp
if flags&FlagShowTimestamp != 0 {
s.buf = append(s.buf, `"time":"`...)
s.buf = timestamp.AppendFormat(s.buf, s.timestampFormat)
s.buf = append(s.buf, '"')
needsComma = true
}
// Add level
if flags&FlagShowLevel != 0 {
if needsComma {
s.buf = append(s.buf, ',')
}
s.buf = append(s.buf, `"level":"`...)
s.buf = append(s.buf, levelToString(level)...)
s.buf = append(s.buf, '"')
needsComma = true
}
// Add message
if needsComma {
s.buf = append(s.buf, ',')
}
s.buf = append(s.buf, `"message":"`...)
s.writeString(message)
s.buf = append(s.buf, '"')
needsComma = true
// // Add trace if present
// if trace != "" {
// if needsComma {
// s.buf = append(s.buf, ',')
// }
// s.buf = append(s.buf, `"trace":"`...)
// s.writeString(trace)
// s.buf = append(s.buf, '"')
// needsComma = true
// }
// Marshal fields using encoding/json
if len(fields) > 0 {
if needsComma {
s.buf = append(s.buf, ',')
}
s.buf = append(s.buf, `"fields":`...)
// Use json.Marshal for proper encoding
marshaledFields, err := json.Marshal(fields)
if err != nil {
// SECURITY: Log marshaling error as a string to prevent log injection
s.buf = append(s.buf, `{"_marshal_error":"`...)
s.writeString(err.Error())
s.buf = append(s.buf, `"}`...)
} else {
s.buf = append(s.buf, marshaledFields...)
}
}
s.buf = append(s.buf, '}', '\n')
return s.buf
}
// Update the levelToString function to include the new heartbeat levels // Update the levelToString function to include the new heartbeat levels
func levelToString(level int64) string { func levelToString(level int64) string {
switch level { switch level {

View File

@ -22,10 +22,11 @@ const (
// Record flags for controlling output structure // Record flags for controlling output structure
const ( const (
FlagShowTimestamp int64 = 0b001 FlagShowTimestamp int64 = 0b0001
FlagShowLevel int64 = 0b010 FlagShowLevel int64 = 0b0010
FlagRaw int64 = 0b100 FlagRaw int64 = 0b0100
FlagDefault = FlagShowTimestamp | FlagShowLevel FlagStructuredJSON int64 = 0b1000
FlagDefault = FlagShowTimestamp | FlagShowLevel
) )
// logRecord represents a single log entry. // logRecord represents a single log entry.
@ -107,6 +108,11 @@ func (l *Logger) LogTrace(depth int, args ...any) {
l.log(FlagShowTimestamp, LevelInfo, int64(depth), args...) l.log(FlagShowTimestamp, LevelInfo, int64(depth), args...)
} }
// LogStructured logs a message with structured fields as proper JSON
func (l *Logger) LogStructured(level int64, message string, fields map[string]any) {
l.log(l.getFlags()|FlagStructuredJSON, level, 0, []any{message, fields})
}
// Write outputs raw, unformatted data regardless of configured format. // Write outputs raw, unformatted data regardless of configured format.
// This method bypasses all formatting (timestamps, levels, JSON structure) // This method bypasses all formatting (timestamps, levels, JSON structure)
// and writes args as space-separated strings without a trailing newline. // and writes args as space-separated strings without a trailing newline.

View File

@ -77,7 +77,7 @@ func (l *Logger) LoadConfig(path string, args []string) error {
l.initMu.Lock() l.initMu.Lock()
defer l.initMu.Unlock() defer l.initMu.Unlock()
return l.applyAndReconfigureLocked() return l.applyConfig()
} }
// SaveConfig saves the current logger configuration to a file // SaveConfig saves the current logger configuration to a file
@ -143,9 +143,9 @@ func (l *Logger) updateConfigFromExternal(extCfg *config.Config, basePath string
return nil return nil
} }
// applyAndReconfigureLocked applies the configuration and reconfigures logger components // applyConfig applies the configuration and reconfigures logger components
// Assumes initMu is held // Assumes initMu is held
func (l *Logger) applyAndReconfigureLocked() error { func (l *Logger) applyConfig() error {
// Check parameter relationship issues // Check parameter relationship issues
minInterval, _ := l.config.Int64("log.min_check_interval_ms") minInterval, _ := l.config.Int64("log.min_check_interval_ms")
maxInterval, _ := l.config.Int64("log.max_check_interval_ms") maxInterval, _ := l.config.Int64("log.max_check_interval_ms")
@ -331,7 +331,7 @@ func (l *Logger) getFlags() int64 {
// log handles the core logging logic // log handles the core logging logic
func (l *Logger) log(flags int64, level int64, depth int64, args ...any) { func (l *Logger) log(flags int64, level int64, depth int64, args ...any) {
if l.state.LoggerDisabled.Load() || !l.state.IsInitialized.Load() { if !l.state.IsInitialized.Load() {
return return
} }

View File

@ -462,59 +462,22 @@ func (l *Logger) logSysHeartbeat() {
l.writeHeartbeatRecord(LevelSys, sysArgs) l.writeHeartbeatRecord(LevelSys, sysArgs)
} }
// writeHeartbeatRecord handles common logic for writing a heartbeat record // writeHeartbeatRecord creates and sends a heartbeat log record through the main processing channel
func (l *Logger) writeHeartbeatRecord(level int64, args []any) { func (l *Logger) writeHeartbeatRecord(level int64, args []any) {
if l.state.LoggerDisabled.Load() || l.state.ShutdownCalled.Load() { if l.state.LoggerDisabled.Load() || l.state.ShutdownCalled.Load() {
return return
} }
// Serialize heartbeat data // Create heartbeat record with appropriate flags
format, _ := l.config.String("log.format") record := logRecord{
hbData := l.serializer.serialize(format, FlagDefault|FlagShowLevel, time.Now(), level, "", args) Flags: FlagDefault | FlagShowLevel,
TimeStamp: time.Now(),
// Mirror to stdout if enabled Level: level,
enableStdout, _ := l.config.Bool("log.enable_stdout") Trace: "",
if enableStdout { Args: args,
if s := l.state.StdoutWriter.Load(); s != nil { unreportedDrops: 0,
// Assert to concrete type: *sink
if sinkWrapper, ok := s.(*sink); ok && sinkWrapper != nil {
// Use the wrapped writer (sinkWrapper.w)
_, _ = sinkWrapper.w.Write(hbData)
}
}
} }
disableFile, _ := l.config.Bool("log.disable_file") // Send through the main processing channel
if disableFile || !l.state.DiskStatusOK.Load() { l.sendLogRecord(record)
return
}
// Write to file
cfPtr := l.state.CurrentFile.Load()
if cfPtr == nil {
l.internalLog("error - current file handle is nil during heartbeat\n")
return
}
currentLogFile, isFile := cfPtr.(*os.File)
if !isFile || currentLogFile == nil {
l.internalLog("error - invalid file handle type during heartbeat\n")
return
}
n, err := currentLogFile.Write(hbData)
if err != nil {
l.internalLog("failed to write heartbeat: %v\n", err)
l.performDiskCheck(true) // Force disk check on write failure
// One retry after disk check
n, err = currentLogFile.Write(hbData)
if err != nil {
l.internalLog("failed to write heartbeat on retry: %v\n", err)
} else {
l.state.CurrentSize.Add(int64(n))
}
} else {
l.state.CurrentSize.Add(int64(n))
}
} }

View File

@ -64,7 +64,7 @@ func (l *Logger) Init(cfg *config.Config, basePath string) error {
return err return err
} }
return l.applyAndReconfigureLocked() return l.applyConfig()
} }
// InitWithDefaults initializes the logger with built-in defaults and optional overrides // InitWithDefaults initializes the logger with built-in defaults and optional overrides
@ -94,7 +94,7 @@ func (l *Logger) InitWithDefaults(overrides ...string) error {
return fmtErrorf("failed to get current value for '%s'", key) return fmtErrorf("failed to get current value for '%s'", key)
} }
var parsedValue interface{} var parsedValue any
var parseErr error var parseErr error
switch currentVal.(type) { switch currentVal.(type) {
@ -124,7 +124,7 @@ func (l *Logger) InitWithDefaults(overrides ...string) error {
} }
} }
return l.applyAndReconfigureLocked() return l.applyConfig()
} }
// Shutdown gracefully closes the logger, attempting to flush pending records // Shutdown gracefully closes the logger, attempting to flush pending records