@marcel91200 / caddy Mirror · read-only / commits / b3f2db23
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 18:30 UTC

core: custom slog handlers for modules (log contextual data) (#7346)

Kévin Dunglas committed Nov 12, 2025 at 21:29 UTC b3f2db233b5a71df6680b43d025bdb62ee2a999c
3 files changed +125 -11
context.go
+42 -7
@@ -21,12 +21,14 @@ import (
21 "log"
22 "log/slog"
23 "reflect"
24 + "sync"
25
26 "github.com/caddyserver/certmagic"
27 "github.com/prometheus/client_golang/prometheus"
28 "github.com/prometheus/client_golang/prometheus/collectors"
29 "go.uber.org/zap"
30 "go.uber.org/zap/exp/zapslog"
31 + "go.uber.org/zap/zapcore"
32
33 "github.com/caddyserver/caddy/v2/internal/filesystems"
34 )
@@ -583,24 +585,57 @@ func (ctx Context) Logger(module ...Module) *zap.Logger {
585 return ctx.cfg.Logging.Logger(mod)
586 }
587
588 +type slogHandlerFactory func(handler slog.Handler, core zapcore.Core, moduleID string) slog.Handler
589 +
590 +var (
591 + slogHandlerFactories []slogHandlerFactory
592 + slogHandlerFactoriesMu sync.RWMutex
593 +)
594 +
595 +// RegisterSlogHandlerFactory allows modules to register custom log/slog.Handler,
596 +// for instance, to add contextual data to the logs.
597 +func RegisterSlogHandlerFactory(factory slogHandlerFactory) {
598 + slogHandlerFactoriesMu.Lock()
599 + slogHandlerFactories = append(slogHandlerFactories, factory)
600 + slogHandlerFactoriesMu.Unlock()
601 +}
602 +
603 // Slogger returns a slog logger that is intended for use by
604 // the most recent module associated with the context.
605 func (ctx Context) Slogger() *slog.Logger {
606 + var (
607 + handler slog.Handler
608 + core zapcore.Core
609 + moduleID string
610 + )
611 if ctx.cfg == nil {
612 // often the case in tests; just use a dev logger
613 l, err := zap.NewDevelopment()
614 if err != nil {
615 panic("config missing, unable to create dev logger: " + err.Error())
616 }
595 - return slog.New(zapslog.NewHandler(l.Core()))
617 +
618 + core = l.Core()
619 + handler = zapslog.NewHandler(core)
620 + } else {
621 + mod := ctx.Module()
622 + if mod == nil {
623 + core = Log().Core()
624 + handler = zapslog.NewHandler(core)
625 + } else {
626 + moduleID = string(mod.CaddyModule().ID)
627 + core = ctx.cfg.Logging.Logger(mod).Core()
628 + handler = zapslog.NewHandler(core, zapslog.WithName(moduleID))
629 + }
630 }
597 - mod := ctx.Module()
598 - if mod == nil {
599 - return slog.New(zapslog.NewHandler(Log().Core()))
631 +
632 + slogHandlerFactoriesMu.RLock()
633 + for _, f := range slogHandlerFactories {
634 + handler = f(handler, core, moduleID)
635 }
601 - return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(),
602 - zapslog.WithName(string(mod.CaddyModule().ID)),
603 - ))
636 + slogHandlerFactoriesMu.RUnlock()
637 +
638 + return slog.New(handler)
639 }
640
641 // Modules returns the lineage of modules that this context provisioned,
modules/caddyhttp/logging.go
+78 -1
@@ -15,18 +15,28 @@
15 package caddyhttp
16
17 import (
18 + "context"
19 "encoding/json"
20 "errors"
21 + "log/slog"
22 "net"
23 "net/http"
24 "strings"
25 + "sync"
26
27 "go.uber.org/zap"
28 + "go.uber.org/zap/exp/zapslog"
29 "go.uber.org/zap/zapcore"
30
31 "github.com/caddyserver/caddy/v2"
32 )
33
34 +func init() {
35 + caddy.RegisterSlogHandlerFactory(func(handler slog.Handler, core zapcore.Core, moduleID string) slog.Handler {
36 + return &extraFieldsSlogHandler{defaultHandler: handler, core: core, moduleID: moduleID}
37 + })
38 +}
39 +
40 // ServerLogConfig describes a server's logging configuration. If
41 // enabled without customization, all requests to this server are
42 // logged to the default logger; logger destinations may be
@@ -223,17 +233,21 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi
233
234 // ExtraLogFields is a list of extra fields to log with every request.
235 type ExtraLogFields struct {
226 - fields []zapcore.Field
236 + fields []zapcore.Field
237 + handlers sync.Map
238 }
239
240 // Add adds a field to the list of extra fields to log.
241 func (e *ExtraLogFields) Add(field zap.Field) {
242 + e.handlers.Clear()
243 e.fields = append(e.fields, field)
244 }
245
246 // Set sets a field in the list of extra fields to log.
247 // If the field already exists, it is replaced.
248 func (e *ExtraLogFields) Set(field zap.Field) {
249 + e.handlers.Clear()
250 +
251 for i := range e.fields {
252 if e.fields[i].Key == field.Key {
253 e.fields[i] = field
@@ -243,6 +257,29 @@ func (e *ExtraLogFields) Set(field zap.Field) {
257 e.fields = append(e.fields, field)
258 }
259
260 +func (e *ExtraLogFields) getSloggerHandler(handler *extraFieldsSlogHandler) (h slog.Handler) {
261 + if existing, ok := e.handlers.Load(handler); ok {
262 + return existing.(slog.Handler)
263 + }
264 +
265 + if handler.moduleID == "" {
266 + h = zapslog.NewHandler(handler.core.With(e.fields))
267 + } else {
268 + h = zapslog.NewHandler(handler.core.With(e.fields), zapslog.WithName(handler.moduleID))
269 + }
270 +
271 + if handler.group != "" {
272 + h = h.WithGroup(handler.group)
273 + }
274 + if handler.attrs != nil {
275 + h = h.WithAttrs(handler.attrs)
276 + }
277 +
278 + e.handlers.Store(handler, h)
279 +
280 + return h
281 +}
282 +
283 const (
284 // Variable name used to indicate that this request
285 // should be omitted from the access logs
@@ -254,3 +291,43 @@ const (
291 // Variable name used to indicate the logger to be used
292 AccessLoggerNameVarKey string = "access_logger_names"
293 )
294 +
295 +type extraFieldsSlogHandler struct {
296 + defaultHandler slog.Handler
297 + core zapcore.Core
298 + moduleID string
299 + group string
300 + attrs []slog.Attr
301 +}
302 +
303 +func (e *extraFieldsSlogHandler) Enabled(ctx context.Context, level slog.Level) bool {
304 + return e.defaultHandler.Enabled(ctx, level)
305 +}
306 +
307 +func (e *extraFieldsSlogHandler) Handle(ctx context.Context, record slog.Record) error {
308 + if elf, ok := ctx.Value(ExtraLogFieldsCtxKey).(*ExtraLogFields); ok {
309 + return elf.getSloggerHandler(e).Handle(ctx, record)
310 + }
311 +
312 + return e.defaultHandler.Handle(ctx, record)
313 +}
314 +
315 +func (e *extraFieldsSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
316 + return &extraFieldsSlogHandler{
317 + e.defaultHandler.WithAttrs(attrs),
318 + e.core,
319 + e.moduleID,
320 + e.group,
321 + append(e.attrs, attrs...),
322 + }
323 +}
324 +
325 +func (e *extraFieldsSlogHandler) WithGroup(name string) slog.Handler {
326 + return &extraFieldsSlogHandler{
327 + e.defaultHandler.WithGroup(name),
328 + e.core,
329 + e.moduleID,
330 + name,
331 + e.attrs,
332 + }
333 +}
modules/caddyhttp/server.go
+5 -3
@@ -793,8 +793,10 @@ func (s *Server) logRequest(
793 accLog *zap.Logger, r *http.Request, wrec ResponseRecorder, duration *time.Duration,
794 repl *caddy.Replacer, bodyReader *lengthReader, shouldLogCredentials bool,
795 ) {
796 + ctx := r.Context()
797 +
798 // this request may be flagged as omitted from the logs
797 - if skip, ok := GetVar(r.Context(), LogSkipVar).(bool); ok && skip {
799 + if skip, ok := GetVar(ctx, LogSkipVar).(bool); ok && skip {
800 return
801 }
802
@@ -812,7 +814,7 @@ func (s *Server) logRequest(
814 }
815
816 message := "handled request"
815 - if nop, ok := GetVar(r.Context(), "unhandled").(bool); ok && nop {
817 + if nop, ok := GetVar(ctx, "unhandled").(bool); ok && nop {
818 message = "NOP"
819 }
820
@@ -836,7 +838,7 @@ func (s *Server) logRequest(
838 reqBodyLength = bodyReader.Length
839 }
840
839 - extra := r.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields)
841 + extra := ctx.Value(ExtraLogFieldsCtxKey).(*ExtraLogFields)
842
843 fieldCount := 6
844 fields = make([]zapcore.Field, 0, fieldCount+len(extra.fields))