Emit slog attributes from every handler (closes #19, closes #24)
check / check (push) Successful in 34s
check / check (pull_request) Successful in 32s

Handle never read the record's attributes, and WithAttrs and WithGroup
returned the receiver unchanged. The JSON and webhook handlers also marshaled
slog.Record itself, whose attributes are unexported.

Each handler now carries a handlerAttrs value, copied rather than mutated, so
sibling loggers cannot leak attributes into each other. JSON and webhook
output nest groups as objects; the console appends key=value pairs with dotted
group keys, quoted as slog.NewTextHandler quotes them, invalid UTF-8
included. Logged values are never written to. In JSON the record's own fields
win a key collision, and a repeated key keeps its last value unless both are
groups, which merge; the README documents both.

Deviation: DEL is quoted on the console; the stdlib leaves it bare.

Model: opus-5-5
This commit is contained in:
2026-09-28 10:49:46 +00:00
parent a5fdadba76
commit 86436449c5
6 changed files with 407 additions and 26 deletions
+26 -8
View File
@@ -14,6 +14,7 @@ import (
// URL.
type WebhookHandler struct {
webhookURL string
attrs handlerAttrs
}
// NewWebhookHandler returns a WebhookHandler that delivers records to
@@ -33,19 +34,36 @@ func (w *WebhookHandler) Enabled(_ context.Context, _ slog.Level) bool {
return true
}
// WithAttrs returns the handler unchanged; attributes are not rendered.
func (w *WebhookHandler) WithAttrs(_ []slog.Attr) slog.Handler {
return w
// WithAttrs returns a new handler that also emits attrs, qualified by the
// groups open now. The receiver is not modified.
func (w *WebhookHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(attrs) == 0 {
return w
}
return &WebhookHandler{
webhookURL: w.webhookURL,
attrs: w.attrs.withAttrs(attrs),
}
}
// WithGroup returns the handler unchanged; groups are not rendered.
func (w *WebhookHandler) WithGroup(_ string) slog.Handler {
return w
// WithGroup returns a new handler that nests later attributes in an
// object named after the group. An empty name returns the receiver.
func (w *WebhookHandler) WithGroup(name string) slog.Handler {
if name == "" {
return w
}
return &WebhookHandler{
webhookURL: w.webhookURL,
attrs: w.attrs.withGroup(name),
}
}
// Handle marshals the record to JSON and POSTs it to the webhook URL.
// Handle marshals the record, with its attributes, to one JSON object and
// POSTs it to the webhook URL.
func (w *WebhookHandler) Handle(ctx context.Context, record slog.Record) error {
jsonData, err := json.Marshal(record)
jsonData, err := json.Marshal(recordToMap(record, w.attrs))
if err != nil {
return fmt.Errorf("error marshaling event: %w", err)
}