github.com/phsym/zeroslog@v0.1.1-0.20240224183259-0b7a5ea94339/zerolog.go (about) 1 package zeroslog 2 3 import ( 4 "context" 5 "encoding" 6 "encoding/json" 7 "fmt" 8 "io" 9 "log/slog" 10 "net" 11 "runtime" 12 "strings" 13 "time" 14 15 "github.com/rs/zerolog" 16 ) 17 18 // HandlerOptions are options for a ZerologHandler. 19 // A zero HandlerOptions consists entirely of default values. 20 type HandlerOptions struct { 21 // AddSource causes the handler to compute the source code position 22 // of the log statement and add a SourceKey attribute to the output. 23 AddSource bool 24 25 // Level reports the minimum record level that will be logged. 26 // The handler discards records with lower levels. 27 // If Level is nil, the handler assumes the level set in the logger. 28 // The handler calls Level.Level if it's not nil for each record processed; 29 // to adjust the minimum level dynamically, use a LevelVar. 30 Level slog.Leveler 31 } 32 33 // zerologHandler is an internal interface used to expose additional methods 34 // between handlers. 35 type zerologHandler interface { 36 slog.Handler 37 // handleGroup handles records comming from the child group. 38 handleGroup(group string, rec *slog.Record, e *zerolog.Event) 39 } 40 41 // Handler is an slog.Handler implementation that uses zerolog to process slog.Record. 42 type Handler struct { 43 opts *HandlerOptions 44 logger zerolog.Logger 45 } 46 47 var _ zerologHandler = (*Handler)(nil) 48 49 // NewHandler creates a *ZerologHandler implementing slog.Handler. 50 // It wraps a zerolog.Logger to which log records will be sent. 51 // 52 // Unlesse opts.Level is not nil, the logger level is used to filter out records, otherwise 53 // opts.Level is used. 54 // 55 // The provided logger instance must be configured to not send timestamps or caller information. 56 // 57 // If opts is nil, it assumes default options values. 58 func NewHandler(logger zerolog.Logger, opts *HandlerOptions) *Handler { 59 if opts == nil { 60 opts = new(HandlerOptions) 61 } 62 opt := *opts // Copy 63 return &Handler{ 64 opts: &opt, 65 logger: logger, 66 } 67 } 68 69 // NewJsonHandler is a shortcut to calling 70 // 71 // NewHandler(zerolog.New(out).Level(zerolog.InfoLevel), opts) 72 func NewJsonHandler(out io.Writer, opts *HandlerOptions) *Handler { 73 return NewHandler(zerolog.New(out).Level(zerolog.InfoLevel), opts) 74 } 75 76 // NewConsoleHandler creates a new zerolog handler, wrapping out into a zerolog.ConsoleWriter. 77 // It's a shortcut to calling 78 // 79 // NewHandler(zerolog.New(&zerolog.ConsoleWriter{Out: out, TimeFormat: time.DateTime}).Level(zerolog.InfoLevel), opts) 80 func NewConsoleHandler(out io.Writer, opts *HandlerOptions) *Handler { 81 return NewJsonHandler(&zerolog.ConsoleWriter{Out: out, TimeFormat: time.DateTime}, opts) 82 } 83 84 // Enabled implements slog.Handler. 85 func (h *Handler) Enabled(_ context.Context, lvl slog.Level) bool { 86 if h.opts.Level != nil { 87 return lvl >= h.opts.Level.Level() 88 } 89 return zerologLevel(lvl) >= h.logger.GetLevel() 90 } 91 92 // startLog creates a new logging event at the given level. 93 func (h *Handler) startLog(lvl slog.Level) *zerolog.Event { 94 logger := h.logger 95 if h.opts.Level != nil { 96 logger = h.logger.Level(zerologLevel(h.opts.Level.Level())) 97 } 98 return logger.WithLevel(zerologLevel(lvl)) 99 } 100 101 // endLog finalize the log event by appending record source, timestamp and message before sending it. 102 func (h *Handler) endLog(rec *slog.Record, evt *zerolog.Event) { 103 if h.opts.AddSource && rec.PC > 0 { 104 frame, _ := runtime.CallersFrames([]uintptr{rec.PC}).Next() 105 evt.Str(zerolog.CallerFieldName, fmt.Sprintf("%s:%d", frame.File, frame.Line)) 106 } 107 108 evt.Time(zerolog.TimestampFieldName, rec.Time) 109 evt.Msg(rec.Message) 110 } 111 112 // handleGroup handles records comming from a child group. 113 func (h *Handler) handleGroup(group string, rec *slog.Record, dict *zerolog.Event) { 114 evt := h.startLog(rec.Level) 115 evt.Dict(group, dict) 116 h.endLog(rec, evt) 117 } 118 119 // Handle implements slog.Handler. 120 func (h *Handler) Handle(_ context.Context, rec slog.Record) error { 121 evt := h.startLog(rec.Level) 122 rec.Attrs(func(a slog.Attr) bool { 123 mapAttr(evt, a) 124 return true 125 }) 126 h.endLog(&rec, evt) 127 return nil 128 } 129 130 // WithAttrs implements slog.Handler. 131 func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { 132 return &Handler{ 133 opts: h.opts, 134 logger: mapAttrs(h.logger.With(), attrs...).Logger(), 135 } 136 } 137 138 // WithGroup implements slog.Handler. 139 func (h *Handler) WithGroup(name string) slog.Handler { 140 return &groupHandler{ 141 parent: h, 142 ctx: zerolog.Context{}, 143 name: strings.TrimSpace(name), 144 } 145 } 146 147 // groupHandler handles groups and subgroups. 148 type groupHandler struct { 149 parent zerologHandler 150 ctx zerolog.Context 151 name string 152 } 153 154 var _ zerologHandler = (*groupHandler)(nil) 155 156 // Enabled implements slog.Handler. 157 func (h *groupHandler) Enabled(ctx context.Context, lvl slog.Level) bool { 158 return h.parent.Enabled(ctx, lvl) 159 } 160 161 // handleGroup handles records comming from a child group. 162 func (h *groupHandler) handleGroup(group string, rec *slog.Record, dict *zerolog.Event) { 163 l := h.ctx.Logger() 164 evt := l.Log() 165 evt.Dict(group, dict) 166 h.parent.handleGroup(h.name, rec, evt) 167 } 168 169 // Handle implements slog.Handler. 170 func (h *groupHandler) Handle(ctx context.Context, rec slog.Record) error { 171 l := h.ctx.Logger() 172 evt := l.Log() 173 rec.Attrs(func(a slog.Attr) bool { 174 mapAttr(evt, a) 175 return true 176 }) 177 h.parent.handleGroup(h.name, &rec, evt) 178 return nil 179 } 180 181 // WithAttrs implements slog.Handler. 182 func (h *groupHandler) WithAttrs(attrs []slog.Attr) slog.Handler { 183 return &groupHandler{ 184 parent: h.parent, 185 ctx: mapAttrs(h.ctx.Logger().With(), attrs...), 186 name: h.name, 187 } 188 } 189 190 // WithGroup implements slog.Handler. 191 func (h *groupHandler) WithGroup(name string) slog.Handler { 192 return &groupHandler{ 193 parent: h, 194 ctx: zerolog.Context{}, 195 name: name, 196 } 197 } 198 199 // zlogWriter is an interface with methods common between 200 // zerolog.Context and *zerolog.Event. This interface is 201 // implemented by both zerolog.Context and *zerolog.Event. 202 type zlogWriter[E any] interface { 203 Bool(string, bool) E 204 Dur(string, time.Duration) E 205 Float64(string, float64) E 206 Int64(string, int64) E 207 Str(string, string) E 208 Time(string, time.Time) E 209 Uint64(string, uint64) E 210 Dict(string, *zerolog.Event) E 211 Interface(string, any) E 212 AnErr(string, error) E 213 Stringer(string, fmt.Stringer) E 214 IPAddr(string, net.IP) E 215 IPPrefix(string, net.IPNet) E 216 MACAddr(string, net.HardwareAddr) E 217 RawJSON(string, []byte) E 218 } 219 220 var ( 221 _ zlogWriter[*zerolog.Event] = (*zerolog.Event)(nil) 222 _ zlogWriter[zerolog.Context] = zerolog.Context{} 223 ) 224 225 // mapAttrs writes multiple slog.Attr into the target which is either a zerolog.Context 226 // or a *zerolog.Event. 227 func mapAttrs[T zlogWriter[T]](target T, a ...slog.Attr) T { 228 for _, attr := range a { 229 target = mapAttr(target, attr) 230 } 231 return target 232 } 233 234 // mapAttr writes slog.Attr into the target which is either a zerolog.Context 235 // or a *zerolog.Event. 236 func mapAttr[T zlogWriter[T]](target T, a slog.Attr) T { 237 value := a.Value.Resolve() 238 switch value.Kind() { 239 case slog.KindGroup: 240 return target.Dict(a.Key, mapAttrs(zerolog.Dict(), value.Group()...)) 241 case slog.KindBool: 242 return target.Bool(a.Key, value.Bool()) 243 case slog.KindDuration: 244 return target.Dur(a.Key, value.Duration()) 245 case slog.KindFloat64: 246 return target.Float64(a.Key, value.Float64()) 247 case slog.KindInt64: 248 return target.Int64(a.Key, value.Int64()) 249 case slog.KindString: 250 return target.Str(a.Key, value.String()) 251 case slog.KindTime: 252 return target.Time(a.Key, value.Time()) 253 case slog.KindUint64: 254 return target.Uint64(a.Key, value.Uint64()) 255 case slog.KindAny: 256 fallthrough 257 default: 258 return mapAttrAny(target, a.Key, value.Any()) 259 } 260 } 261 262 func mapAttrAny[T zlogWriter[T]](target T, key string, value any) T { 263 switch v := value.(type) { 264 case net.IP: 265 return target.IPAddr(key, v) 266 case net.IPNet: 267 return target.IPPrefix(key, v) 268 case net.HardwareAddr: 269 return target.MACAddr(key, v) 270 case error: 271 return target.AnErr(key, v) 272 case fmt.Stringer: 273 return target.Stringer(key, v) 274 case json.Marshaler: 275 txt, err := v.MarshalJSON() 276 if err == nil { 277 return target.RawJSON(key, txt) 278 } 279 return target.Str(key, "!ERROR:"+err.Error()) 280 case encoding.TextMarshaler: 281 txt, err := v.MarshalText() 282 if err == nil { 283 return target.Str(key, string(txt)) 284 } 285 return target.Str(key, "!ERROR:"+err.Error()) 286 default: 287 return target.Interface(key, value) 288 } 289 } 290 291 // zerologLevel maps slog.Level into zerolog.Level. 292 func zerologLevel(lvl slog.Level) zerolog.Level { 293 switch { 294 case lvl < slog.LevelDebug: 295 return zerolog.TraceLevel 296 case lvl < slog.LevelInfo: 297 return zerolog.DebugLevel 298 case lvl < slog.LevelWarn: 299 return zerolog.InfoLevel 300 case lvl < slog.LevelError: 301 return zerolog.WarnLevel 302 default: 303 return zerolog.ErrorLevel 304 } 305 }