Skip to content

Commit

Permalink
Add Slog integration (#865)
Browse files Browse the repository at this point in the history
Co-authored-by: Michi Hoffmann <[email protected]>
  • Loading branch information
ribice and cleptric authored Nov 13, 2024
1 parent eb74cc1 commit fb01995
Show file tree
Hide file tree
Showing 12 changed files with 1,567 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .craft.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ targets:
- name: github
tagPrefix: otel/v
tagOnly: true
- name: github
tagPrefix: slog/v
tagOnly: true
- name: registry
sdks:
github:getsentry/sentry-go:
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

### Features

- Add `sentryslog` integration ([#865](https://github.com/getsentry/sentry-go/pull/865))

- Always set Mechanism Type to generic ([#896](https://github.com/getsentry/sentry-go/pull/897))

### Misc
Expand Down
7 changes: 3 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,21 @@ test-coverage: $(COVERAGE_REPORT_DIR) clean-report-dir ## Test with coverage en
mod-tidy: ## Check go.mod tidiness
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
cd "$${dir}"; \
echo ">>> Running 'go mod tidy' for module: $${dir}"; \
go mod tidy -go=1.21 -compat=1.21; \
(cd "$${dir}" && go mod tidy -go=1.21 -compat=1.21); \
done; \
git diff --exit-code;
.PHONY: mod-tidy

vet: ## Run "go vet"
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
cd "$${dir}"; \
echo ">>> Running 'go vet' for module: $${dir}"; \
go vet ./...; \
(cd "$${dir}" && go vet ./...); \
done;
.PHONY: vet


lint: ## Lint (using "golangci-lint")
golangci-lint run
.PHONY: lint
Expand Down
38 changes: 38 additions & 0 deletions _examples/slog/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"fmt"
"log"
"time"

"log/slog"

"github.com/getsentry/sentry-go"
sentryslog "github.com/getsentry/sentry-go/slog"
)

func main() {
err := sentry.Init(sentry.ClientOptions{
Dsn: "https://[email protected]/4506954545758208",
EnableTracing: false,
})
if err != nil {
log.Fatal(err)
}

defer sentry.Flush(2 * time.Second)

logger := slog.New(sentryslog.Option{Level: slog.LevelDebug}.NewSentryHandler())
logger = logger.With("release", "v1.0.0")

logger.
With(
slog.Group("user",
slog.String("id", "user-123"),
slog.Time("created_at", time.Now()),
),
).
With("environment", "dev").
With("error", fmt.Errorf("an error")).
Error("a message")
}
260 changes: 260 additions & 0 deletions slog/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
package sentryslog

import (
"context"
"encoding"
"fmt"
"log/slog"
"runtime"
"strconv"
)

func source(sourceKey string, r *slog.Record) slog.Attr {
fs := runtime.CallersFrames([]uintptr{r.PC})
f, _ := fs.Next()
var args []any
if f.Function != "" {
args = append(args, slog.String("function", f.Function))
}
if f.File != "" {
args = append(args, slog.String("file", f.File))
}
if f.Line != 0 {
args = append(args, slog.Int("line", f.Line))
}

return slog.Group(sourceKey, args...)
}

type replaceAttrFn = func(groups []string, a slog.Attr) slog.Attr

func replaceAttrs(fn replaceAttrFn, groups []string, attrs ...slog.Attr) []slog.Attr {
for i := range attrs {
attr := attrs[i]
value := attr.Value.Resolve()
if value.Kind() == slog.KindGroup {
attrs[i].Value = slog.GroupValue(replaceAttrs(fn, append(groups, attr.Key), value.Group()...)...)
} else if fn != nil {
attrs[i] = fn(groups, attr)
}
}

return attrs
}

func attrsToMap(attrs ...slog.Attr) map[string]any {
output := make(map[string]any, len(attrs))

attrsByKey := groupValuesByKey(attrs)
for k, values := range attrsByKey {
v := mergeAttrValues(values...)
if v.Kind() == slog.KindGroup {
output[k] = attrsToMap(v.Group()...)
} else {
output[k] = v.Any()
}
}

return output
}

func extractError(attrs []slog.Attr) ([]slog.Attr, error) {
for i := range attrs {
attr := attrs[i]

if _, ok := errorKeys[attr.Key]; !ok {
continue
}

if err, ok := attr.Value.Resolve().Any().(error); ok {
return append(attrs[:i], attrs[i+1:]...), err
}
}

return attrs, nil
}

func mergeAttrValues(values ...slog.Value) slog.Value {
v := values[0]

for i := 1; i < len(values); i++ {
if v.Kind() != slog.KindGroup || values[i].Kind() != slog.KindGroup {
v = values[i]
continue
}

v = slog.GroupValue(append(v.Group(), values[i].Group()...)...)
}

return v
}

func groupValuesByKey(attrs []slog.Attr) map[string][]slog.Value {
result := map[string][]slog.Value{}

for _, item := range attrs {
key := item.Key
result[key] = append(result[key], item.Value)
}

return result
}

func attrsToString(attrs ...slog.Attr) map[string]string {
output := make(map[string]string, len(attrs))

for _, attr := range attrs {
k, v := attr.Key, attr.Value
output[k] = valueToString(v)
}

return output
}

func valueToString(v slog.Value) string {
switch v.Kind() {
case slog.KindAny, slog.KindLogValuer, slog.KindGroup:
return anyValueToString(v)
case slog.KindInt64:
return fmt.Sprintf("%d", v.Int64())
case slog.KindUint64:
return fmt.Sprintf("%d", v.Uint64())
case slog.KindFloat64:
return fmt.Sprintf("%f", v.Float64())
case slog.KindString:
return v.String()
case slog.KindBool:
return strconv.FormatBool(v.Bool())
case slog.KindDuration:
return v.Duration().String()
case slog.KindTime:
return v.Time().UTC().String()
}
return anyValueToString(v)
}

func anyValueToString(v slog.Value) string {
tm, ok := v.Any().(encoding.TextMarshaler)
if !ok {
return fmt.Sprintf("%+v", v.Any())
}

data, err := tm.MarshalText()
if err != nil {
return fmt.Sprintf("%+v", v.Any())
}

return string(data)
}

func appendRecordAttrsToAttrs(attrs []slog.Attr, groups []string, record *slog.Record) []slog.Attr {
output := make([]slog.Attr, len(attrs))
copy(output, attrs)

for i, j := 0, len(groups)-1; i < j; i, j = i+1, j-1 {
groups[i], groups[j] = groups[j], groups[i]
}
record.Attrs(func(attr slog.Attr) bool {
for i := range groups {
attr = slog.Group(groups[i], attr)
}
output = append(output, attr)
return true
})

return output
}

func removeEmptyAttrs(attrs []slog.Attr) []slog.Attr {
result := []slog.Attr{}

for _, attr := range attrs {
if attr.Key == "" {
continue
}

if attr.Value.Kind() == slog.KindGroup {
values := removeEmptyAttrs(attr.Value.Group())
if len(values) == 0 {
continue
}
attr.Value = slog.GroupValue(values...)
result = append(result, attr)
} else if !attr.Value.Equal(slog.Value{}) {
result = append(result, attr)
}
}

return result
}

func contextExtractor(ctx context.Context, fns []func(ctx context.Context) []slog.Attr) []slog.Attr {
attrs := []slog.Attr{}
for _, fn := range fns {
attrs = append(attrs, fn(ctx)...)
}
return attrs
}

func appendAttrsToGroup(groups []string, actualAttrs []slog.Attr, newAttrs ...slog.Attr) []slog.Attr {
actualAttrsCopy := make([]slog.Attr, len(actualAttrs))
copy(actualAttrsCopy, actualAttrs)

if len(groups) == 0 {
return uniqAttrs(append(actualAttrsCopy, newAttrs...))
}

groupKey := groups[0]
for i := range actualAttrsCopy {
attr := actualAttrsCopy[i]
if attr.Key == groupKey && attr.Value.Kind() == slog.KindGroup {
actualAttrsCopy[i] = slog.Group(groupKey, toAnySlice(appendAttrsToGroup(groups[1:], attr.Value.Group(), newAttrs...))...)
return actualAttrsCopy
}
}

return uniqAttrs(
append(
actualAttrsCopy,
slog.Group(
groupKey,
toAnySlice(appendAttrsToGroup(groups[1:], []slog.Attr{}, newAttrs...))...,
),
),
)
}

func toAnySlice(collection []slog.Attr) []any {
result := make([]any, len(collection))
for i := range collection {
result[i] = collection[i]
}
return result
}

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
}
Loading

0 comments on commit fb01995

Please sign in to comment.