From b0d26a313d0dca6f6a36a2547034d58f4a87347db51292a024980f748841ed88 Mon Sep 17 00:00:00 2001 From: Lixen Wraith Date: Mon, 14 Jul 2025 20:49:22 -0400 Subject: [PATCH] e1.9.0 Structured JSON log method added, refactored. --- compat/fasthttp.go | 2 +- compat/gnet.go | 10 +- compat/structured.go | 14 +-- doc/compatibility-adapters.md | 10 +- format.go | 174 ++++++++++++++++++---------------- interface.go | 14 ++- logger.go | 8 +- processor.go | 59 +++--------- state.go | 6 +- 9 files changed, 138 insertions(+), 159 deletions(-) diff --git a/compat/fasthttp.go b/compat/fasthttp.go index b6a6952..765fc42 100644 --- a/compat/fasthttp.go +++ b/compat/fasthttp.go @@ -48,7 +48,7 @@ func WithLevelDetector(detector func(string) int64) FastHTTPOption { } // 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...) // Detect log level from message content diff --git a/compat/gnet.go b/compat/gnet.go index 786eccf..a3cb081 100644 --- a/compat/gnet.go +++ b/compat/gnet.go @@ -42,31 +42,31 @@ func WithFatalHandler(handler func(string)) GnetOption { } // 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...) a.logger.Debug("msg", msg, "source", "gnet") } // 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...) a.logger.Info("msg", msg, "source", "gnet") } // 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...) a.logger.Warn("msg", msg, "source", "gnet") } // 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...) a.logger.Error("msg", msg, "source", "gnet") } // 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...) a.logger.Error("msg", msg, "source", "gnet", "fatal", true) diff --git a/compat/structured.go b/compat/structured.go index d6731b9..20b2c95 100644 --- a/compat/structured.go +++ b/compat/structured.go @@ -11,18 +11,18 @@ import ( // parseFormat attempts to extract structured fields from printf-style format strings // 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" keyValuePattern := regexp.MustCompile(`(\w+)\s*[:=]\s*%[vsdqxXeEfFgGpbcU]`) matches := keyValuePattern.FindAllStringSubmatchIndex(format, -1) if len(matches) == 0 || len(matches) > len(args) { // 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 - fields := make([]interface{}, 0, len(matches)*2+2) + fields := make([]any, 0, len(matches)*2+2) lastEnd := 0 argIndex := 0 @@ -91,7 +91,7 @@ func NewStructuredGnetAdapter(logger *log.Logger, opts ...GnetOption) *Structure } // 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 { fields := parseFormat(format, args) 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 -func (a *StructuredGnetAdapter) Infof(format string, args ...interface{}) { +func (a *StructuredGnetAdapter) Infof(format string, args ...any) { if a.extractFields { fields := parseFormat(format, args) 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 -func (a *StructuredGnetAdapter) Warnf(format string, args ...interface{}) { +func (a *StructuredGnetAdapter) Warnf(format string, args ...any) { if a.extractFields { fields := parseFormat(format, args) 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 -func (a *StructuredGnetAdapter) Errorf(format string, args ...interface{}) { +func (a *StructuredGnetAdapter) Errorf(format string, args ...any) { if a.extractFields { fields := parseFormat(format, args) a.logger.Error(append(fields, "source", "gnet")...) diff --git a/doc/compatibility-adapters.md b/doc/compatibility-adapters.md index 6106de1..6ef57ad 100644 --- a/doc/compatibility-adapters.md +++ b/doc/compatibility-adapters.md @@ -63,11 +63,11 @@ type GnetAdapter struct { } // Methods implemented: -// - Debugf(format string, args ...interface{}) -// - Infof(format string, args ...interface{}) -// - Warnf(format string, args ...interface{}) -// - Errorf(format string, args ...interface{}) -// - Fatalf(format string, args ...interface{}) +// - Debugf(format string, args ...any) +// - Infof(format string, args ...any) +// - Warnf(format string, args ...any) +// - Errorf(format string, args ...any) +// - Fatalf(format string, args ...any) ``` ### Custom Fatal Behavior diff --git a/format.go b/format.go index 92932fa..15c543f 100644 --- a/format.go +++ b/format.go @@ -4,8 +4,8 @@ package log import ( "bytes" "encoding/hex" + "encoding/json" "fmt" - "reflect" "strconv" "strings" "time" @@ -41,7 +41,12 @@ func (s *serializer) serialize(format string, flags int64, timestamp time.Time, 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" { 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). func (s *serializer) serializeJSON(flags int64, timestamp time.Time, level int64, trace string, args []any) []byte { 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 func levelToString(level int64) string { switch level { diff --git a/interface.go b/interface.go index 47a34b5..4139368 100644 --- a/interface.go +++ b/interface.go @@ -22,10 +22,11 @@ const ( // Record flags for controlling output structure const ( - FlagShowTimestamp int64 = 0b001 - FlagShowLevel int64 = 0b010 - FlagRaw int64 = 0b100 - FlagDefault = FlagShowTimestamp | FlagShowLevel + FlagShowTimestamp int64 = 0b0001 + FlagShowLevel int64 = 0b0010 + FlagRaw int64 = 0b0100 + FlagStructuredJSON int64 = 0b1000 + FlagDefault = FlagShowTimestamp | FlagShowLevel ) // 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...) } +// 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. // This method bypasses all formatting (timestamps, levels, JSON structure) // and writes args as space-separated strings without a trailing newline. diff --git a/logger.go b/logger.go index 91b9a13..2d31852 100644 --- a/logger.go +++ b/logger.go @@ -77,7 +77,7 @@ func (l *Logger) LoadConfig(path string, args []string) error { l.initMu.Lock() defer l.initMu.Unlock() - return l.applyAndReconfigureLocked() + return l.applyConfig() } // SaveConfig saves the current logger configuration to a file @@ -143,9 +143,9 @@ func (l *Logger) updateConfigFromExternal(extCfg *config.Config, basePath string return nil } -// applyAndReconfigureLocked applies the configuration and reconfigures logger components +// applyConfig applies the configuration and reconfigures logger components // Assumes initMu is held -func (l *Logger) applyAndReconfigureLocked() error { +func (l *Logger) applyConfig() error { // Check parameter relationship issues minInterval, _ := l.config.Int64("log.min_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 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 } diff --git a/processor.go b/processor.go index 6362868..09b219c 100644 --- a/processor.go +++ b/processor.go @@ -462,59 +462,22 @@ func (l *Logger) logSysHeartbeat() { 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) { if l.state.LoggerDisabled.Load() || l.state.ShutdownCalled.Load() { return } - // Serialize heartbeat data - format, _ := l.config.String("log.format") - hbData := l.serializer.serialize(format, FlagDefault|FlagShowLevel, time.Now(), level, "", args) - - // Mirror to stdout if enabled - enableStdout, _ := l.config.Bool("log.enable_stdout") - if enableStdout { - if s := l.state.StdoutWriter.Load(); s != nil { - // Assert to concrete type: *sink - if sinkWrapper, ok := s.(*sink); ok && sinkWrapper != nil { - // Use the wrapped writer (sinkWrapper.w) - _, _ = sinkWrapper.w.Write(hbData) - } - } + // Create heartbeat record with appropriate flags + record := logRecord{ + Flags: FlagDefault | FlagShowLevel, + TimeStamp: time.Now(), + Level: level, + Trace: "", + Args: args, + unreportedDrops: 0, } - disableFile, _ := l.config.Bool("log.disable_file") - if disableFile || !l.state.DiskStatusOK.Load() { - 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)) - } + // Send through the main processing channel + l.sendLogRecord(record) } \ No newline at end of file diff --git a/state.go b/state.go index bfc479e..a4bdce4 100644 --- a/state.go +++ b/state.go @@ -64,7 +64,7 @@ func (l *Logger) Init(cfg *config.Config, basePath string) error { return err } - return l.applyAndReconfigureLocked() + return l.applyConfig() } // 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) } - var parsedValue interface{} + var parsedValue any var parseErr error 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