http_log.go
· 213 B · Go
Eredeti
package log
// HTTPError is used when the response of a HTTP request is unexpected or otherwise incorrect.
type HTTPError interface {
Error() string
ReadBody() (string, error)
StatusCode() int
Close() error
}
| 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
· 3.7 KiB · Go
Eredeti
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
}
}
}
| 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
· 2.6 KiB · Go
Eredeti
package log
import (
"context"
"fmt"
"log/slog"
"strings"
"github.com/pkg/errors"
)
const tab = "\t"
const fourSpaces = " "
// TextHandler is a slog.Handler that replaces all \t instances with four spaces and replaces errors with their
// detailed string representation.
type TextHandler struct {
slog.Handler
}
// Handle replaces all \t instances with four spaces and replaces errors with their detailed string representation.
func (handler *TextHandler) Handle(ctx context.Context, record slog.Record) error {
record.Message = strings.ReplaceAll(record.Message, tab, fourSpaces)
record = handler.parseRecordAttrs(record)
return handler.Handler.Handle(ctx, record)
}
func (handler *TextHandler) parseRecordAttrs(record slog.Record) slog.Record {
var attrs []slog.Attr
record.Attrs(func(attr slog.Attr) bool {
newAttrs := handler.parseAttr(attr)
for _, newAttr := range newAttrs {
attrs = append(attrs, *newAttr)
}
return true
})
newRecord := slog.NewRecord(record.Time, record.Level, record.Message, record.PC)
newRecord.AddAttrs(attrs...)
return newRecord
}
func (handler *TextHandler) parseAttr(attr slog.Attr) []*slog.Attr {
newAttrs := []*slog.Attr{&attr}
attr.Value = attr.Value.Resolve()
switch attr.Value.Kind() {
case slog.KindString:
attr.Value = slog.StringValue(strings.ReplaceAll(attr.Value.String(), tab, fourSpaces))
case slog.KindAny:
if err, ok := attr.Value.Any().(error); ok {
newAttrs = handler.parseErrorAttr(attr, err)
}
case slog.KindGroup:
var groupAttrs []slog.Attr
for _, groupAttr := range attr.Value.Group() {
newGroupAttrs := handler.parseAttr(groupAttr)
for _, newGroupAttr := range newGroupAttrs {
groupAttrs = append(groupAttrs, *newGroupAttr)
}
}
attr.Value = slog.GroupValue(groupAttrs...)
}
return newAttrs
}
func (handler *TextHandler) parseErrorAttr(attr slog.Attr, err error) []*slog.Attr {
attr.Value = slog.StringValue(fmt.Sprintf("%+v", err))
newAttrs := handler.parseAttr(attr)
httpError, ok := errors.Cause(err).(HTTPError)
if ok {
if responseBody, err := httpError.ReadBody(); err == nil {
responseBodyAttr := slog.String("response body", responseBody)
newAttrs = append(newAttrs, &responseBodyAttr)
}
}
return newAttrs
}
// WithAttrs returns a new TextHandler with the given attributes appended to the handler's existing attributes.
func (handler *TextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &TextHandler{handler.Handler.WithAttrs(attrs)}
}
// WithGroup returns a new TextHandler with the given group name set.
func (handler *TextHandler) WithGroup(name string) slog.Handler {
return &TextHandler{handler.Handler.WithGroup(name)}
}
| 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 | } |