1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
|
package slogmulti
import (
"context"
"errors"
"log/slog"
"slices"
"github.com/samber/lo"
)
// Ensure FanoutHandler implements the slog.Handler interface at compile time
var _ slog.Handler = (*FanoutHandler)(nil)
// FanoutHandler distributes log records to multiple slog.Handler instances in parallel.
// It implements the slog.Handler interface and forwards all logging operations to all
// registered handlers that are enabled for the given log level.
type FanoutHandler struct {
// handlers contains the list of slog.Handler instances to which log records will be distributed
handlers []slog.Handler
}
// Fanout creates a new FanoutHandler that distributes records to multiple slog.Handler instances.
// If exactly one handler is provided, it returns that handler unmodified.
// If you pass a FanoutHandler as an argument, its handlers are flattened into the new FanoutHandler.
// This function is the primary entry point for creating a multi-handler setup.
//
// Example usage:
//
// handler := slogmulti.Fanout(
// slog.NewJSONHandler(os.Stdout, nil),
// slogdatadog.NewDatadogHandler(...),
// )
// logger := slog.New(handler)
//
// Args:
//
// handlers: Variable number of slog.Handler instances to distribute logs to
//
// Returns:
//
// A slog.Handler that forwards all operations to the provided handlers
func Fanout(handlers ...slog.Handler) slog.Handler {
var flat []slog.Handler
for _, handler := range handlers {
if fan, ok := handler.(*FanoutHandler); ok {
flat = append(flat, fan.handlers...)
} else {
flat = append(flat, handler)
}
}
if len(flat) == 1 {
return flat[0]
}
return &FanoutHandler{
handlers: flat,
}
}
// Enabled checks if any of the underlying handlers are enabled for the given log level.
// This method implements the slog.Handler interface requirement.
//
// The handler is considered enabled if at least one of its child handlers
// is enabled for the specified level. This ensures that if any handler
// can process the log, the fanout handler will attempt to distribute it.
//
// Args:
//
// ctx: The context for the logging operation
// l: The log level to check
//
// Returns:
//
// true if at least one handler is enabled for the level, false otherwise
func (h *FanoutHandler) Enabled(ctx context.Context, l slog.Level) bool {
for i := range h.handlers {
if h.handlers[i].Enabled(ctx, l) {
return true
}
}
return false
}
// Handle distributes a log record to all enabled handlers.
// This method implements the slog.Handler interface requirement.
//
// The method:
// 1. Iterates through all registered handlers
// 2. Checks if each handler is enabled for the record's level
// 3. For enabled handlers, calls their Handle method with a cloned record
// 4. Collects any errors that occur during handling
// 5. Returns a combined error if any handlers failed
//
// Note: Each handler receives a cloned record to prevent interference between handlers.
// This ensures that one handler cannot modify the record for other handlers.
//
// Args:
//
// ctx: The context for the logging operation
// r: The log record to distribute
//
// Returns:
//
// An error if any handler failed to process the record, nil otherwise
func (h *FanoutHandler) Handle(ctx context.Context, r slog.Record) error {
var errs []error
for i := range h.handlers {
if h.handlers[i].Enabled(ctx, r.Level) {
err := try(func() error {
return h.handlers[i].Handle(ctx, r.Clone())
})
if err != nil {
errs = append(errs, err)
}
}
}
// If errs is empty, or contains only nil errors, this returns nil
return errors.Join(errs...)
}
// WithAttrs creates a new FanoutHandler with additional attributes added to all child handlers.
// This method implements the slog.Handler interface requirement.
//
// The method creates new handler instances for each child handler with the additional
// attributes, ensuring that the attributes are properly propagated to all handlers
// in the fanout chain.
//
// Args:
//
// attrs: The attributes to add to all handlers
//
// Returns:
//
// A new FanoutHandler with the attributes added to all child handlers
func (h *FanoutHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
handlers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler {
return h.WithAttrs(slices.Clone(attrs))
})
return Fanout(handlers...)
}
// WithGroup creates a new FanoutHandler with a group name applied to all child handlers.
// This method implements the slog.Handler interface requirement.
//
// The method follows the same pattern as the standard slog implementation:
// - If the group name is empty, returns the original handler unchanged
// - Otherwise, creates new handler instances for each child handler with the group name
//
// Args:
//
// name: The group name to apply to all handlers
//
// Returns:
//
// A new FanoutHandler with the group name applied to all child handlers,
// or the original handler if the group name is empty
func (h *FanoutHandler) WithGroup(name string) slog.Handler {
// https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247
if name == "" {
return h
}
handlers := lo.Map(h.handlers, func(h slog.Handler, _ int) slog.Handler {
return h.WithGroup(name)
})
return Fanout(handlers...)
}
|