Última atividade 4 weeks ago

robinsiep revisou este gist 4 weeks ago. Ir para a revisão

Sem alterações

Robin Siep revisou este gist 11 months ago. Ir para a revisão

3 files changed, 257 insertions

http_log.go(arquivo criado)

@@ -0,0 +1,9 @@
1 + package log
2 +
3 + // HTTPError is used when the response of a HTTP request is unexpected or otherwise incorrect.
4 + type HTTPError interface {
5 + Error() string
6 + ReadBody() (string, error)
7 + StatusCode() int
8 + Close() error
9 + }

seq_log.go(arquivo criado)

@@ -0,0 +1,160 @@
1 + package log
2 +
3 + import (
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.
19 + type 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.
30 + func (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.
35 + func (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 +
44 + func (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 +
76 + func (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.
101 + func (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.
113 + func (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 +
127 + func 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 +
142 + func 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 +
152 + func 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(arquivo criado)

@@ -0,0 +1,88 @@
1 + package log
2 +
3 + import (
4 + "context"
5 + "fmt"
6 + "log/slog"
7 + "strings"
8 +
9 + "github.com/pkg/errors"
10 + )
11 +
12 + const tab = "\t"
13 + const 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.
17 + type TextHandler struct {
18 + slog.Handler
19 + }
20 +
21 + // Handle replaces all \t instances with four spaces and replaces errors with their detailed string representation.
22 + func (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 +
28 + func (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 +
43 + func (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 +
66 + func (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.
81 + func (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.
86 + func (handler *TextHandler) WithGroup(name string) slog.Handler {
87 + return &TextHandler{handler.Handler.WithGroup(name)}
88 + }
Próximo Anterior