e1.9.0 Structured JSON log method added, refactored.
This commit is contained in:
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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")...)
|
||||
|
||||
@ -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
174
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 {
|
||||
|
||||
12
interface.go
12
interface.go
@ -22,9 +22,10 @@ const (
|
||||
|
||||
// Record flags for controlling output structure
|
||||
const (
|
||||
FlagShowTimestamp int64 = 0b001
|
||||
FlagShowLevel int64 = 0b010
|
||||
FlagRaw int64 = 0b100
|
||||
FlagShowTimestamp int64 = 0b0001
|
||||
FlagShowLevel int64 = 0b0010
|
||||
FlagRaw int64 = 0b0100
|
||||
FlagStructuredJSON int64 = 0b1000
|
||||
FlagDefault = FlagShowTimestamp | FlagShowLevel
|
||||
)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
59
processor.go
59
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)
|
||||
}
|
||||
6
state.go
6
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
|
||||
|
||||
Reference in New Issue
Block a user