Skip to content

Commit e50676c

Browse files
feat(logging): adopt zap as unified logging library (#83)
Signed-off-by: Jintao Zhang <[email protected]>
1 parent 83bf2e4 commit e50676c

File tree

4 files changed

+140
-12
lines changed

4 files changed

+140
-12
lines changed

src/semantic-router/cmd/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/prometheus/client_golang/prometheus/promhttp"
1111
"github.com/vllm-project/semantic-router/semantic-router/pkg/api"
1212
"github.com/vllm-project/semantic-router/semantic-router/pkg/extproc"
13+
"github.com/vllm-project/semantic-router/semantic-router/pkg/observability"
1314
)
1415

1516
func main() {
@@ -23,6 +24,11 @@ func main() {
2324
)
2425
flag.Parse()
2526

27+
// Initialize logging (zap) from environment.
28+
if _, err := observability.InitLoggerFromEnv(); err != nil {
29+
log.Printf("failed to initialize logger, falling back to stdlib: %v", err)
30+
}
31+
2632
// Check if config file exists
2733
if _, err := os.Stat(*configPath); os.IsNotExist(err) {
2834
log.Fatalf("Config file not found: %s", *configPath)

src/semantic-router/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ require (
3939
github.com/tidwall/pretty v1.2.1 // indirect
4040
github.com/tidwall/sjson v1.2.5 // indirect
4141
go.uber.org/automaxprocs v1.6.0 // indirect
42+
go.uber.org/multierr v1.10.0 // indirect
43+
go.uber.org/zap v1.27.0 // indirect
4244
golang.org/x/net v0.41.0 // indirect
4345
golang.org/x/sys v0.33.0 // indirect
4446
golang.org/x/text v0.26.0 // indirect

src/semantic-router/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
8484
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
8585
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
8686
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
87+
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
88+
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
89+
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
90+
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
8791
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
8892
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
8993
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
Lines changed: 128 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,123 @@
11
package observability
22

33
import (
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.
11122
func 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

Comments
 (0)