From 189ffa976447f257ebbd12694102d9d1c80596f0 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Sun, 15 Oct 2023 17:55:31 +0200 Subject: [PATCH] feat: adding support for AddSource+ReplaceAttr - moving some code to samber/slog-common --- .github/workflows/release.yml | 3 + README.md | 12 ++++ converter.go | 115 +++++++++------------------------- go.mod | 7 ++- go.sum | 2 + handler.go | 9 ++- utils.go | 58 ----------------- version.go | 4 ++ 8 files changed, 62 insertions(+), 148 deletions(-) delete mode 100644 utils.go create mode 100644 version.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9fadc8..532584c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,9 @@ jobs: - name: Remove xxx_test.go files run: rm -rf *_test.go ./examples ./images + - name: Set library version in version.go + run: sed -i 's/VERSION/${{ inputs.semver }}/g' version.go + # cleanup test dependencies - name: Cleanup dependencies run: go mod tidy diff --git a/README.md b/README.md index 464f1fc..f857ec3 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,23 @@ type Option struct { // optional: customize json payload builder Converter Converter + + // optional: see slog.HandlerOptions + AddSource bool + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr } ``` Attributes will be injected in log payload. +Other global parameters: + +```go +sloggraylog.SourceKey = "source" +sloggraylog.ContextKey = "extra" +sloggraylog.ErrorKeys = []string{"error", "err"} +``` + ### Example ```go diff --git a/converter.go b/converter.go index ecb158a..2a85fc7 100644 --- a/converter.go +++ b/converter.go @@ -1,106 +1,49 @@ package sloggraylog import ( - "encoding" - "fmt" - "reflect" - "log/slog" -) -type Converter func(loggerAttr []slog.Attr, record *slog.Record) map[string]any + slogcommon "github.com/samber/slog-common" +) -func DefaultConverter(loggerAttr []slog.Attr, record *slog.Record) map[string]any { - log := map[string]any{ - "timestamp": record.Time.UTC(), - "level": record.Level.String(), - "message": record.Message, - } +var SourceKey = "source" +var ContextKey = "extra" +var ErrorKeys = []string{"error", "err"} - extra := attrsToValue(loggerAttr) +type Converter func(addSource bool, replaceAttr func(groups []string, a slog.Attr) slog.Attr, loggerAttr []slog.Attr, groups []string, record *slog.Record) map[string]any - record.Attrs(func(attr slog.Attr) bool { - for k, v := range attrsToValue([]slog.Attr{attr}) { - extra[k] = v - } - return true - }) +func DefaultConverter(addSource bool, replaceAttr func(groups []string, a slog.Attr) slog.Attr, loggerAttr []slog.Attr, groups []string, record *slog.Record) map[string]any { + // aggregate all attributes + attrs := slogcommon.AppendRecordAttrsToAttrs(loggerAttr, groups, record) - if err, ok := extra["error"]; ok { - log["error"] = err - delete(extra, "error") + // developer formatters + if addSource { + attrs = append(attrs, slogcommon.Source(SourceKey, record)) } + attrs = slogcommon.ReplaceAttrs(replaceAttr, []string{}, attrs...) - log["extra"] = extra - - return log -} - -func attrsToValue(attrs []slog.Attr) map[string]any { - log := map[string]any{} - - for i := range attrs { - k, v := attrToValue(attrs[i]) - log[k] = v + // handler formatter + log := map[string]any{ + "logger.name": name, + "logger.version": version, + "timestamp": record.Time.UTC(), + "level": record.Level.String(), + "message": record.Message, } - return log -} - -func attrToValue(attr slog.Attr) (string, any) { - k := attr.Key - v := attr.Value - kind := v.Kind() + extra := slogcommon.AttrsToMap(attrs...) - switch kind { - case slog.KindAny: - if k == "error" { - if err, ok := v.Any().(error); ok { - return k, buildExceptions(err) + for _, errorKey := range ErrorKeys { + if v, ok := extra[errorKey]; ok { + if err, ok := v.(error); ok { + log[errorKey] = slogcommon.FormatError(err) + delete(extra, errorKey) + break } } - - return k, v.Any() - case slog.KindLogValuer: - return k, v.Any() - case slog.KindGroup: - return k, attrsToValue(v.Group()) - case slog.KindInt64: - return k, v.Int64() - case slog.KindUint64: - return k, v.Uint64() - case slog.KindFloat64: - return k, v.Float64() - case slog.KindString: - return k, v.String() - case slog.KindBool: - return k, v.Bool() - case slog.KindDuration: - return k, v.Duration() - case slog.KindTime: - return k, v.Time().UTC() - default: - return k, anyValueToString(v) - } -} - -func anyValueToString(v slog.Value) string { - if tm, ok := v.Any().(encoding.TextMarshaler); ok { - data, err := tm.MarshalText() - if err != nil { - return "" - } - - return string(data) } - return fmt.Sprintf("%+v", v.Any()) -} + log[ContextKey] = extra -func buildExceptions(err error) map[string]any { - return map[string]any{ - "kind": reflect.TypeOf(err).String(), - "error": err.Error(), - "stack": nil, // @TODO - } + return log } diff --git a/go.mod b/go.mod index d67a03c..0b5f1d4 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,11 @@ go 1.21 require ( github.com/Graylog2/go-gelf v0.0.0-20170811154226-7ebf4f536d8f - github.com/samber/lo v1.38.1 + github.com/samber/slog-common v0.11.0 go.uber.org/goleak v1.2.1 ) -require golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect +require ( + github.com/samber/lo v1.38.1 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect +) diff --git a/go.sum b/go.sum index f1754f9..155ca10 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/samber/slog-common v0.11.0 h1:JdESCaXcEwdtoTCYHKQFfHGbWN2vZJq0DDGEE/lwTUQ= +github.com/samber/slog-common v0.11.0/go.mod h1:Qjrfhwk79XiCIhBj8+jTq1Cr0u9rlWbjawh3dWXzaHk= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= diff --git a/handler.go b/handler.go index 2b89f97..3738cf5 100644 --- a/handler.go +++ b/handler.go @@ -7,6 +7,7 @@ import ( "log/slog" "github.com/Graylog2/go-gelf/gelf" + slogcommon "github.com/samber/slog-common" ) type Option struct { @@ -18,6 +19,10 @@ type Option struct { // optional: customize json payload builder Converter Converter + + // optional: see slog.HandlerOptions + AddSource bool + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr } func (o Option) NewGraylogHandler() slog.Handler { @@ -54,7 +59,7 @@ func (h *GraylogHandler) Handle(ctx context.Context, record slog.Record) error { converter = h.option.Converter } - message := converter(h.attrs, &record) + message := converter(h.option.AddSource, h.option.ReplaceAttr, h.attrs, h.groups, &record) bytes, err := json.Marshal(message) if err != nil { @@ -69,7 +74,7 @@ func (h *GraylogHandler) Handle(ctx context.Context, record slog.Record) error { func (h *GraylogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &GraylogHandler{ option: h.option, - attrs: appendAttrsToGroup(h.groups, h.attrs, attrs), + attrs: slogcommon.AppendAttrsToGroup(h.groups, h.attrs, attrs...), groups: h.groups, } } diff --git a/utils.go b/utils.go deleted file mode 100644 index b2c22de..0000000 --- a/utils.go +++ /dev/null @@ -1,58 +0,0 @@ -package sloggraylog - -import ( - "log/slog" - - "github.com/samber/lo" -) - -func appendAttrsToGroup(groups []string, actualAttrs []slog.Attr, newAttrs []slog.Attr) []slog.Attr { - if len(groups) == 0 { - return uniqAttrs(append(actualAttrs, newAttrs...)) - } - - for i := range actualAttrs { - attr := actualAttrs[i] - if attr.Key == groups[0] && attr.Value.Kind() == slog.KindGroup { - actualAttrs[i] = slog.Group(groups[0], lo.ToAnySlice(appendAttrsToGroup(groups[1:], attr.Value.Group(), newAttrs))...) - return actualAttrs - } - } - - return uniqAttrs( - append( - actualAttrs, - slog.Group( - groups[0], - lo.ToAnySlice(appendAttrsToGroup(groups[1:], []slog.Attr{}, newAttrs))..., - ), - ), - ) -} - -func uniqAttrs(attrs []slog.Attr) []slog.Attr { - return uniqByLast(attrs, func(item slog.Attr) string { - return item.Key - }) -} - -func uniqByLast[T any, U comparable](collection []T, iteratee func(item T) U) []T { - result := make([]T, 0, len(collection)) - seen := make(map[U]int, len(collection)) - seenIndex := 0 - - for _, item := range collection { - key := iteratee(item) - - if index, ok := seen[key]; ok { - result[index] = item - continue - } - - seen[key] = seenIndex - seenIndex++ - result = append(result, item) - } - - return result -} diff --git a/version.go b/version.go new file mode 100644 index 0000000..b6d9948 --- /dev/null +++ b/version.go @@ -0,0 +1,4 @@ +package sloggraylog + +const name = "samber/slog-graylog" +const version = "VERSION" // replaced by .github/workflows/release.yml