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
92 lines
2.2 KiB
Go
92 lines
2.2 KiB
Go
package simplelog
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
)
|
|
|
|
// WebhookHandler POSTs each log record as JSON to a configured webhook
|
|
// URL.
|
|
type WebhookHandler struct {
|
|
webhookURL string
|
|
attrs handlerAttrs
|
|
}
|
|
|
|
// NewWebhookHandler returns a WebhookHandler that delivers records to
|
|
// the given URL, validating the URL first.
|
|
func NewWebhookHandler(webhookURL string) (*WebhookHandler, error) {
|
|
_, err := url.ParseRequestURI(webhookURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid webhook URL: %w", err)
|
|
}
|
|
|
|
return &WebhookHandler{webhookURL: webhookURL}, nil
|
|
}
|
|
|
|
// Enabled reports whether the handler processes records at the given
|
|
// level; it always returns true.
|
|
func (w *WebhookHandler) Enabled(_ context.Context, _ slog.Level) bool {
|
|
return true
|
|
}
|
|
|
|
// 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 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, 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(recordToMap(record, w.attrs))
|
|
if err != nil {
|
|
return fmt.Errorf("error marshaling event: %w", err)
|
|
}
|
|
|
|
request, err := http.NewRequestWithContext(
|
|
ctx,
|
|
http.MethodPost,
|
|
w.webhookURL,
|
|
bytes.NewReader(jsonData),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating webhook request: %w", err)
|
|
}
|
|
|
|
request.Header.Set("Content-Type", "application/json")
|
|
|
|
response, err := http.DefaultClient.Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = response.Body.Close() }()
|
|
|
|
return nil
|
|
}
|