package log import ( "bytes" "context" "encoding/json" "fmt" "log/slog" "maps" "net/http" "net/url" "time" "github.com/pkg/errors" ) // SeqHandler is a slog.Handler that converts slog.Record instances into Seq events // and submits them to the specified Seq API. type SeqHandler struct { url url.URL apiKey string client http.Client enabledLevel slog.Level attrs []slog.Attr group string } // Enabled returns true if the given level is greater than or equal to the handler's enabled level // set during initialization. func (handler *SeqHandler) Enabled(_ context.Context, level slog.Level) bool { return level >= handler.enabledLevel } // Handle converts the given slog.Record into a Seq event and submits it to the Seq API. func (handler *SeqHandler) Handle(ctx context.Context, record slog.Record) error { event, err := handler.recordToSeqEvent(record) if err != nil { return err } return handler.postEvent(event) } func (handler *SeqHandler) recordToSeqEvent(record slog.Record) (map[string]any, error) { seqLevel, err := slogLevelToSeqLevel(record.Level) if err != nil { return map[string]any{}, err } event := map[string]any{ "@t": record.Time.Format(time.RFC3339Nano), "@l": seqLevel, "@m": record.Message, } eventAttrs := map[string]any{} record.Attrs(func(attr slog.Attr) bool { addAttr(eventAttrs, attr) return true }) for _, attr := range handler.attrs { addAttr(eventAttrs, attr) } if handler.group != "" { event[handler.group] = eventAttrs } else { maps.Copy(event, eventAttrs) } return event, nil } func (handler *SeqHandler) postEvent(event map[string]any) error { body, err := json.Marshal(event) if err != nil { return errors.Wrap(err, "failed to marshal event") } req, err := http.NewRequest("POST", handler.url.String(), bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") if handler.apiKey != "" { req.Header.Set("X-Seq-ApiKey", handler.apiKey) } resp, err := handler.client.Do(req) if resp != nil { defer resp.Body.Close() } return err } // WithAttrs returns a new SeqHandler with the given attributes appended to the handler's existing attributes. func (handler *SeqHandler) WithAttrs(attrs []slog.Attr) *SeqHandler { return &SeqHandler{ url: handler.url, apiKey: handler.apiKey, client: handler.client, enabledLevel: handler.enabledLevel, attrs: append(handler.attrs, attrs...), group: handler.group, } } // WithGroup returns a new SeqHandler with the given log group name set. func (handler *SeqHandler) WithGroup(name string) *SeqHandler { if name == "" { return handler } return &SeqHandler{ url: handler.url, apiKey: handler.apiKey, client: handler.client, enabledLevel: handler.enabledLevel, attrs: handler.attrs, group: name, } } func slogLevelToSeqLevel(level slog.Level) (string, error) { switch level { case slog.LevelDebug: return "Debug", nil case slog.LevelInfo: return "Information", nil case slog.LevelWarn: return "Warning", nil case slog.LevelError: return "Error", nil default: return "", errors.Errorf("unknown slog level %s", level) } } func addAttr(event map[string]any, attr slog.Attr) { if attr.Key == "err" { if err, ok := attr.Value.Any().(error); ok { addErrorAttrs(event, err) return } } event[attr.Key] = attr.Value.Any() } func addErrorAttrs(event map[string]any, err error) { event["@x"] = fmt.Sprintf("%+v", err) httpError, ok := errors.Cause(err).(HTTPError) if ok { if responseBody, err := httpError.ReadBody(); err == nil { event["respBody"] = responseBody } } }