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
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

View File

@ -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)

View File

@ -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")...)

View File

@ -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

174
format.go
View File

@ -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 {

View File

@ -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.

View File

@ -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
}

View File

@ -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)
}

View File

@ -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