Naposledy aktivní 4 weeks ago

Revize 8012b0d932d3a0f077c0c19ba151976a8b127ef5

http_log.go Raw
1package log
2
3// HTTPError is used when the response of a HTTP request is unexpected or otherwise incorrect.
4type HTTPError interface {
5 Error() string
6 ReadBody() (string, error)
7 StatusCode() int
8 Close() error
9}
seq_log.go Raw
1package log
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "log/slog"
9 "maps"
10 "net/http"
11 "net/url"
12 "time"
13
14 "github.com/pkg/errors"
15)
16
17// SeqHandler is a slog.Handler that converts slog.Record instances into Seq events
18// and submits them to the specified Seq API.
19type SeqHandler struct {
20 url url.URL
21 apiKey string
22 client http.Client
23 enabledLevel slog.Level
24 attrs []slog.Attr
25 group string
26}
27
28// Enabled returns true if the given level is greater than or equal to the handler's enabled level
29// set during initialization.
30func (handler *SeqHandler) Enabled(_ context.Context, level slog.Level) bool {
31 return level >= handler.enabledLevel
32}
33
34// Handle converts the given slog.Record into a Seq event and submits it to the Seq API.
35func (handler *SeqHandler) Handle(ctx context.Context, record slog.Record) error {
36 event, err := handler.recordToSeqEvent(record)
37 if err != nil {
38 return err
39 }
40
41 return handler.postEvent(event)
42}
43
44func (handler *SeqHandler) recordToSeqEvent(record slog.Record) (map[string]any, error) {
45 seqLevel, err := slogLevelToSeqLevel(record.Level)
46 if err != nil {
47 return map[string]any{}, err
48 }
49
50 event := map[string]any{
51 "@t": record.Time.Format(time.RFC3339Nano),
52 "@l": seqLevel,
53 "@m": record.Message,
54 }
55
56 eventAttrs := map[string]any{}
57
58 record.Attrs(func(attr slog.Attr) bool {
59 addAttr(eventAttrs, attr)
60 return true
61 })
62
63 for _, attr := range handler.attrs {
64 addAttr(eventAttrs, attr)
65 }
66
67 if handler.group != "" {
68 event[handler.group] = eventAttrs
69 } else {
70 maps.Copy(event, eventAttrs)
71 }
72
73 return event, nil
74}
75
76func (handler *SeqHandler) postEvent(event map[string]any) error {
77 body, err := json.Marshal(event)
78 if err != nil {
79 return errors.Wrap(err, "failed to marshal event")
80 }
81
82 req, err := http.NewRequest("POST", handler.url.String(), bytes.NewReader(body))
83 if err != nil {
84 return err
85 }
86
87 req.Header.Set("Content-Type", "application/json")
88 if handler.apiKey != "" {
89 req.Header.Set("X-Seq-ApiKey", handler.apiKey)
90 }
91
92 resp, err := handler.client.Do(req)
93 if resp != nil {
94 defer resp.Body.Close()
95 }
96
97 return err
98}
99
100// WithAttrs returns a new SeqHandler with the given attributes appended to the handler's existing attributes.
101func (handler *SeqHandler) WithAttrs(attrs []slog.Attr) *SeqHandler {
102 return &SeqHandler{
103 url: handler.url,
104 apiKey: handler.apiKey,
105 client: handler.client,
106 enabledLevel: handler.enabledLevel,
107 attrs: append(handler.attrs, attrs...),
108 group: handler.group,
109 }
110}
111
112// WithGroup returns a new SeqHandler with the given log group name set.
113func (handler *SeqHandler) WithGroup(name string) *SeqHandler {
114 if name == "" {
115 return handler
116 }
117 return &SeqHandler{
118 url: handler.url,
119 apiKey: handler.apiKey,
120 client: handler.client,
121 enabledLevel: handler.enabledLevel,
122 attrs: handler.attrs,
123 group: name,
124 }
125}
126
127func slogLevelToSeqLevel(level slog.Level) (string, error) {
128 switch level {
129 case slog.LevelDebug:
130 return "Debug", nil
131 case slog.LevelInfo:
132 return "Information", nil
133 case slog.LevelWarn:
134 return "Warning", nil
135 case slog.LevelError:
136 return "Error", nil
137 default:
138 return "", errors.Errorf("unknown slog level %s", level)
139 }
140}
141
142func addAttr(event map[string]any, attr slog.Attr) {
143 if attr.Key == "err" {
144 if err, ok := attr.Value.Any().(error); ok {
145 addErrorAttrs(event, err)
146 return
147 }
148 }
149 event[attr.Key] = attr.Value.Any()
150}
151
152func addErrorAttrs(event map[string]any, err error) {
153 event["@x"] = fmt.Sprintf("%+v", err)
154 httpError, ok := errors.Cause(err).(HTTPError)
155 if ok {
156 if responseBody, err := httpError.ReadBody(); err == nil {
157 event["respBody"] = responseBody
158 }
159 }
160}
text_log.go Raw
1package log
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "strings"
8
9 "github.com/pkg/errors"
10)
11
12const tab = "\t"
13const fourSpaces = " "
14
15// TextHandler is a slog.Handler that replaces all \t instances with four spaces and replaces errors with their
16// detailed string representation.
17type TextHandler struct {
18 slog.Handler
19}
20
21// Handle replaces all \t instances with four spaces and replaces errors with their detailed string representation.
22func (handler *TextHandler) Handle(ctx context.Context, record slog.Record) error {
23 record.Message = strings.ReplaceAll(record.Message, tab, fourSpaces)
24 record = handler.parseRecordAttrs(record)
25 return handler.Handler.Handle(ctx, record)
26}
27
28func (handler *TextHandler) parseRecordAttrs(record slog.Record) slog.Record {
29 var attrs []slog.Attr
30 record.Attrs(func(attr slog.Attr) bool {
31 newAttrs := handler.parseAttr(attr)
32 for _, newAttr := range newAttrs {
33 attrs = append(attrs, *newAttr)
34 }
35 return true
36 })
37
38 newRecord := slog.NewRecord(record.Time, record.Level, record.Message, record.PC)
39 newRecord.AddAttrs(attrs...)
40 return newRecord
41}
42
43func (handler *TextHandler) parseAttr(attr slog.Attr) []*slog.Attr {
44 newAttrs := []*slog.Attr{&attr}
45 attr.Value = attr.Value.Resolve()
46 switch attr.Value.Kind() {
47 case slog.KindString:
48 attr.Value = slog.StringValue(strings.ReplaceAll(attr.Value.String(), tab, fourSpaces))
49 case slog.KindAny:
50 if err, ok := attr.Value.Any().(error); ok {
51 newAttrs = handler.parseErrorAttr(attr, err)
52 }
53 case slog.KindGroup:
54 var groupAttrs []slog.Attr
55 for _, groupAttr := range attr.Value.Group() {
56 newGroupAttrs := handler.parseAttr(groupAttr)
57 for _, newGroupAttr := range newGroupAttrs {
58 groupAttrs = append(groupAttrs, *newGroupAttr)
59 }
60 }
61 attr.Value = slog.GroupValue(groupAttrs...)
62 }
63 return newAttrs
64}
65
66func (handler *TextHandler) parseErrorAttr(attr slog.Attr, err error) []*slog.Attr {
67 attr.Value = slog.StringValue(fmt.Sprintf("%+v", err))
68 newAttrs := handler.parseAttr(attr)
69 httpError, ok := errors.Cause(err).(HTTPError)
70 if ok {
71 if responseBody, err := httpError.ReadBody(); err == nil {
72 responseBodyAttr := slog.String("response body", responseBody)
73 newAttrs = append(newAttrs, &responseBodyAttr)
74 }
75 }
76
77 return newAttrs
78}
79
80// WithAttrs returns a new TextHandler with the given attributes appended to the handler's existing attributes.
81func (handler *TextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
82 return &TextHandler{handler.Handler.WithAttrs(attrs)}
83}
84
85// WithGroup returns a new TextHandler with the given group name set.
86func (handler *TextHandler) WithGroup(name string) slog.Handler {
87 return &TextHandler{handler.Handler.WithGroup(name)}
88}