1- // Package logger provides structured logging utilities with field support
2- // for better debugging and monitoring of webhook sprinkler operations .
1+ // Package logger provides structured logging using slog with hostname tracking
2+ // and short source file paths for better debugging across multiple instances .
33package logger
44
55import (
6+ "context"
67 "fmt"
7- "log"
8- "sort"
9- "strings"
8+ "io"
9+ "log/slog"
10+ "os"
11+ "path/filepath"
12+ "runtime"
13+ "time"
1014)
1115
1216// Fields represents structured log fields.
1317type Fields map [string ]any
1418
15- // WithFieldsf adds structured context to log messages with printf-style formatting.
16- func WithFieldsf (fields Fields , format string , args ... any ) {
17- // Sort keys for consistent output
18- keys := make ([]string , 0 , len (fields ))
19- for k := range fields {
20- keys = append (keys , k )
21- }
22- sort .Strings (keys )
19+ var (
20+ // defaultLogger is the global logger instance.
21+ defaultLogger * slog.Logger
22+ // hostname is cached on init for performance.
23+ hostname string
24+ )
2325
24- var parts []string
25- for _ , k := range keys {
26- parts = append (parts , fmt .Sprintf ("%s=%v" , k , fields [k ]))
26+ func init () {
27+ var err error
28+ hostname , err = os .Hostname ()
29+ if err != nil {
30+ hostname = "unknown"
2731 }
2832
29- msg := fmt .Sprintf (format , args ... )
30- if len (parts ) > 0 {
31- log .Printf ("%s [%s]" , msg , strings .Join (parts , " " ))
32- } else {
33- log .Print (msg )
33+ // Initialize with default text handler
34+ defaultLogger = New (os .Stderr )
35+ }
36+
37+ // New creates a new slog logger with hostname and short source paths.
38+ func New (w io.Writer ) * slog.Logger {
39+ opts := & slog.HandlerOptions {
40+ AddSource : true ,
41+ Level : slog .LevelInfo ,
42+ ReplaceAttr : func (_ []string , a slog.Attr ) slog.Attr {
43+ // Shorten source file paths to just basename:line
44+ if a .Key == slog .SourceKey {
45+ if source , ok := a .Value .Any ().(* slog.Source ); ok {
46+ source .File = filepath .Base (source .File )
47+ // Remove function name to keep it concise
48+ source .Function = ""
49+ }
50+ }
51+ return a
52+ },
3453 }
54+
55+ handler := slog .NewTextHandler (w , opts )
56+ logger := slog .New (handler )
57+
58+ // Add hostname to all log messages
59+ return logger .With ("instance" , hostname )
60+ }
61+
62+ // SetDefault sets the default logger.
63+ func SetDefault (l * slog.Logger ) {
64+ defaultLogger = l
65+ }
66+
67+ // SetLogger sets the default logger (alias for SetDefault).
68+ func SetLogger (l * slog.Logger ) {
69+ defaultLogger = l
70+ }
71+
72+ // Default returns the default logger.
73+ func Default () * slog.Logger {
74+ return defaultLogger
75+ }
76+
77+ // Hostname returns the cached hostname.
78+ func Hostname () string {
79+ return hostname
3580}
3681
3782// Info logs an info message with optional fields.
3883func Info (msg string , fields Fields ) {
39- WithFieldsf (fields , "%s" , msg )
84+ defaultLogger .LogAttrs (context .Background (), slog .LevelInfo , msg , attrsFromFields (fields )... )
85+ }
86+
87+ // Warn logs a warning message with optional fields.
88+ func Warn (msg string , fields Fields ) {
89+ defaultLogger .LogAttrs (context .Background (), slog .LevelWarn , msg , attrsFromFields (fields )... )
4090}
4191
4292// Error logs an error message with optional fields.
@@ -45,10 +95,44 @@ func Error(msg string, err error, fields Fields) {
4595 fields = Fields {}
4696 }
4797 fields ["error" ] = err .Error ()
48- WithFieldsf ( fields , "ERROR: %s" , msg )
98+ defaultLogger . LogAttrs ( context . Background (), slog . LevelError , msg , attrsFromFields ( fields ) ... )
4999}
50100
51- // Warn logs a warning message with optional fields.
52- func Warn (msg string , fields Fields ) {
53- WithFieldsf (fields , "WARNING: %s" , msg )
101+ // Debug logs a debug message with optional fields.
102+ func Debug (msg string , fields Fields ) {
103+ defaultLogger .LogAttrs (context .Background (), slog .LevelDebug , msg , attrsFromFields (fields )... )
104+ }
105+
106+ // attrsFromFields converts Fields to slog.Attr slice.
107+ func attrsFromFields (fields Fields ) []slog.Attr {
108+ if fields == nil {
109+ return nil
110+ }
111+ attrs := make ([]slog.Attr , 0 , len (fields ))
112+ for k , v := range fields {
113+ attrs = append (attrs , slog .Any (k , v ))
114+ }
115+ return attrs
116+ }
117+
118+ // LogAt logs a message at the specified level with source information.
119+ // This is useful when you want to override the default source location.
120+ func LogAt (level slog.Level , skip int , msg string , fields Fields ) {
121+ var pcs [1 ]uintptr
122+ runtime .Callers (skip + 2 , pcs [:])
123+ r := slog .NewRecord (
124+ time .Now (),
125+ level ,
126+ msg ,
127+ pcs [0 ],
128+ )
129+ r .AddAttrs (attrsFromFields (fields )... )
130+ _ = defaultLogger .Handler ().Handle (context .Background (), r ) //nolint:errcheck // Best effort logging
131+ }
132+
133+ // WithFieldsf provides backward compatibility for tests.
134+ // Deprecated: Use Info/Warn/Error with Fields instead.
135+ func WithFieldsf (fields Fields , format string , args ... any ) {
136+ msg := fmt .Sprintf (format , args ... )
137+ Info (msg , fields )
54138}
0 commit comments