11package observability
22
33import (
4- "encoding/json "
5- "log "
4+ "os "
5+ "strings "
66 "time"
7+
8+ "go.uber.org/zap"
9+ "go.uber.org/zap/zapcore"
710)
811
9- // LogEvent emits a structured JSON log line with a standard envelope
12+ // Config holds logger configuration.
13+ type Config struct {
14+ // Level is one of: debug, info, warn, error, dpanic, panic, fatal
15+ Level string
16+ // Encoding is one of: json, console
17+ Encoding string
18+ // Development enables dev-friendly logging (stacktraces on error, etc.)
19+ Development bool
20+ // AddCaller enables caller annotations.
21+ AddCaller bool
22+ }
23+
24+ // InitLogger initializes a global zap logger using the provided config.
25+ // It also redirects the standard library logger to zap and returns the logger.
26+ func InitLogger (cfg Config ) (* zap.Logger , error ) {
27+ zcfg := zap .NewProductionConfig ()
28+
29+ // Level
30+ lvl := strings .ToLower (strings .TrimSpace (cfg .Level ))
31+ switch lvl {
32+ case "" , "info" :
33+ zcfg .Level = zap .NewAtomicLevelAt (zapcore .InfoLevel )
34+ case "debug" :
35+ zcfg .Level = zap .NewAtomicLevelAt (zapcore .DebugLevel )
36+ case "warn" , "warning" :
37+ zcfg .Level = zap .NewAtomicLevelAt (zapcore .WarnLevel )
38+ case "error" :
39+ zcfg .Level = zap .NewAtomicLevelAt (zapcore .ErrorLevel )
40+ case "dpanic" :
41+ zcfg .Level = zap .NewAtomicLevelAt (zapcore .DPanicLevel )
42+ case "panic" :
43+ zcfg .Level = zap .NewAtomicLevelAt (zapcore .PanicLevel )
44+ case "fatal" :
45+ zcfg .Level = zap .NewAtomicLevelAt (zapcore .FatalLevel )
46+ default :
47+ zcfg .Level = zap .NewAtomicLevelAt (zapcore .InfoLevel )
48+ }
49+
50+ // Encoding
51+ enc := strings .ToLower (strings .TrimSpace (cfg .Encoding ))
52+ if enc == "console" {
53+ zcfg .Encoding = "console"
54+ } else {
55+ zcfg .Encoding = "json"
56+ }
57+
58+ if cfg .Development {
59+ zcfg = zap .NewDevelopmentConfig ()
60+ // Apply encoding override if specified
61+ if enc != "" {
62+ zcfg .Encoding = enc
63+ }
64+ }
65+
66+ // Common fields
67+ zcfg .EncoderConfig .TimeKey = "ts"
68+ zcfg .EncoderConfig .EncodeTime = zapcore .TimeEncoderOfLayout (time .RFC3339Nano )
69+ zcfg .EncoderConfig .MessageKey = "msg"
70+ zcfg .EncoderConfig .LevelKey = "level"
71+ zcfg .EncoderConfig .EncodeLevel = zapcore .LowercaseLevelEncoder
72+ zcfg .EncoderConfig .CallerKey = "caller"
73+
74+ // Build logger
75+ logger , err := zcfg .Build ()
76+ if err != nil {
77+ return nil , err
78+ }
79+
80+ if cfg .AddCaller {
81+ logger = logger .WithOptions (zap .AddCaller ())
82+ }
83+
84+ // Replace globals and redirect stdlib log
85+ zap .ReplaceGlobals (logger )
86+ _ = zap .RedirectStdLog (logger )
87+
88+ return logger , nil
89+ }
90+
91+ // InitLoggerFromEnv builds a logger from environment variables and initializes it.
92+ // Supported env vars:
93+ //
94+ // SR_LOG_LEVEL (debug|info|warn|error|dpanic|panic|fatal) default: info
95+ // SR_LOG_ENCODING (json|console) default: json
96+ // SR_LOG_DEVELOPMENT (true|false) default: false
97+ // SR_LOG_ADD_CALLER (true|false) default: true
98+ func InitLoggerFromEnv () (* zap.Logger , error ) {
99+ cfg := Config {
100+ Level : getenvDefault ("SR_LOG_LEVEL" , "info" ),
101+ Encoding : getenvDefault ("SR_LOG_ENCODING" , "json" ),
102+ Development : parseBool (getenvDefault ("SR_LOG_DEVELOPMENT" , "false" )),
103+ AddCaller : parseBool (getenvDefault ("SR_LOG_ADD_CALLER" , "true" )),
104+ }
105+ return InitLogger (cfg )
106+ }
107+
108+ func getenvDefault (k , d string ) string {
109+ v := os .Getenv (k )
110+ if v == "" {
111+ return d
112+ }
113+ return v
114+ }
115+ func parseBool (s string ) bool {
116+ s = strings .TrimSpace (strings .ToLower (s ))
117+ return s == "1" || s == "true" || s == "yes" || s == "on"
118+ }
119+
120+ // LogEvent emits a structured log at info level with a standard envelope.
10121// Fields provided by callers take precedence and will not be overwritten.
11122func LogEvent (event string , fields map [string ]interface {}) {
12123 if fields == nil {
@@ -15,14 +126,19 @@ func LogEvent(event string, fields map[string]interface{}) {
15126 if _ , ok := fields ["event" ]; ! ok {
16127 fields ["event" ] = event
17128 }
18- if _ , ok := fields ["ts" ]; ! ok {
19- fields ["ts" ] = time .Now ().UTC ().Format (time .RFC3339Nano )
20- }
21- b , err := json .Marshal (fields )
22- if err != nil {
23- // Fallback to regular log on marshal error
24- log .Printf ("event=%s marshal_error=%v fields_len=%d" , event , err , len (fields ))
25- return
129+ // Zap already includes a timestamp; preserve provided ts if any
130+
131+ // Convert the map to zap fields
132+ zfields := make ([]zap.Field , 0 , len (fields ))
133+ for k , v := range fields {
134+ zfields = append (zfields , zap .Any (k , v ))
26135 }
27- log . Println ( string ( b ) )
136+ zap . L (). With ( zfields ... ). Info ( event )
28137}
138+
139+ // Helper printf-style wrappers to ease migration from log.Printf.
140+ func Infof (format string , args ... interface {}) { zap .S ().Infof (format , args ... ) }
141+ func Warnf (format string , args ... interface {}) { zap .S ().Warnf (format , args ... ) }
142+ func Errorf (format string , args ... interface {}) { zap .S ().Errorf (format , args ... ) }
143+ func Debugf (format string , args ... interface {}) { zap .S ().Debugf (format , args ... ) }
144+ func Fatalf (format string , args ... interface {}) { zap .S ().Fatalf (format , args ... ) }
0 commit comments