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))