Skip to content

Commit 5bc6aad

Browse files
authored
Structured logging support for slog (#1033)
1 parent f43c181 commit 5bc6aad

5 files changed

Lines changed: 946 additions & 76 deletions

File tree

_examples/slog/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"fmt"
56
"log"
67
"time"
@@ -22,9 +23,15 @@ func main() {
2223

2324
defer sentry.Flush(2 * time.Second)
2425

25-
logger := slog.New(sentryslog.Option{Level: slog.LevelDebug}.NewSentryHandler())
26+
ctx := context.Background()
27+
handler := sentryslog.Option{
28+
EventLevel: []slog.Level{slog.LevelError, sentryslog.LevelFatal}, // Only Error and Fatal as events
29+
LogLevel: []slog.Level{slog.LevelWarn, slog.LevelInfo}, // Only Warn and Info as logs
30+
}.NewSentryHandler(ctx)
31+
logger := slog.New(handler)
2632
logger = logger.With("release", "v1.0.0")
2733

34+
// message level is Error and will be handled only as Event.
2835
logger.
2936
With(
3037
slog.Group("user",

slog/README.MD

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,38 +29,43 @@ import (
2929
"log/slog"
3030

3131
"github.com/getsentry/sentry-go"
32-
slogSentry "github.com/getsentry/sentry-go/slog"
32+
sentryslog "github.com/getsentry/sentry-go/slog"
3333
)
3434

3535
func main() {
3636
// Initialize Sentry
3737
err := sentry.Init(sentry.ClientOptions{
3838
Dsn: "your-public-dsn",
3939
Debug: true,
40+
EnableLogs: true,
4041
})
4142
if err != nil {
4243
panic(err)
4344
}
4445
defer sentry.Flush(5 * time.Second)
4546

46-
// Set up slog with Sentry handler
47-
handler := slogSentry.Option{
48-
Level: slog.LevelError, // Minimum log level
49-
AddSource: true, // Include file/line source info
50-
}.NewSentryHandler()
51-
logger := slog.New(handler)
47+
ctx := context.Background()
48+
handler := sentryslog.Option{
49+
EventLevel: []slog.Level{slog.LevelError, sentryslog.LevelFatal}, // Only Error and Fatal as events
50+
LogLevel: []slog.Level{slog.LevelWarn, slog.LevelInfo}, // Only Warn and Info as logs
51+
}.NewSentryHandler(ctx)
52+
logger := slog.New(handler)
5253

5354
// Example logging
54-
logger.Info("This will not be sent to Sentry")
55-
logger.Error("An error occurred", "user", "test-user")
55+
logger.Info("This will be sent to sentry as a Log entry")
56+
logger.Error("An error occurred", "user", "test-user") // this will be sent as an Event
57+
// These will be ignored
58+
logger.Debug("This will be ignored")
5659
}
5760
```
5861

5962
## Configuration
6063

6164
The slog-sentry package offers several options to customize how logs are handled and sent to Sentry. These are specified through the Option struct:
6265

63-
- `Level`: Minimum log level to send to Sentry. Defaults to `slog.LevelDebug`.
66+
- `EventLevel`: Slice of specific levels to send `Events` to Sentry. Defaults to `[]slog.Level{slog.LevelError, LevelFatal}`.
67+
68+
- `LogLevel`: Slice of specific levels to send `Log` entries to Sentry. Defaults to `[]slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError, LevelFatal}`.
6469

6570
- `Hub`: Custom Sentry hub to use; defaults to the current Sentry hub if not set.
6671

@@ -77,7 +82,7 @@ The slog-sentry package offers several options to customize how logs are handled
7782

7883
```go
7984
handler := slogSentry.Option{
80-
Level: slog.LevelWarn,
85+
EventLevel: slog.LevelWarn,
8186
Converter: func(addSource bool, replaceAttr func([]string, slog.Attr) slog.Attr, attrs []slog.Attr, groups []string, record *slog.Record, hub *sentry.Hub) *sentry.Event {
8287
// Custom conversion logic
8388
return &sentry.Event{
@@ -88,6 +93,22 @@ handler := slogSentry.Option{
8893
}.NewSentryHandler()
8994
```
9095

96+
### Backwards Compatibility
97+
98+
The old `Level` field is Deprecated but still works and will be converted to a slice of all levels starting from the minimum level:
99+
100+
```go
101+
// Old way (still works)
102+
handler := sentryslog.Option{
103+
Level: slog.LevelWarn, // Will be converted to EventLevel: [Warn, Error, Fatal]
104+
}.NewSentryHandler(ctx)
105+
106+
// New way (preferred)
107+
handler := sentryslog.Option{
108+
EventLevel: []slog.Level{slog.LevelWarn, slog.LevelError, sentryslog.LevelFatal},
109+
LogLevel: []slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError, sentryslog.LevelFatal},
110+
}.NewSentryHandler(ctx)
111+
```
91112
## Notes
92113

93114
- Always call `Flush` or `FlushWithContext` to ensure all events are sent to Sentry before program termination

slog/converter.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ import (
44
"encoding"
55
"fmt"
66
"log/slog"
7+
"math"
78
"net/http"
9+
"strconv"
10+
"time"
811

912
"github.com/getsentry/sentry-go"
13+
"github.com/getsentry/sentry-go/attribute"
1014
)
1115

1216
var (
@@ -129,3 +133,46 @@ func handleFingerprint(v slog.Value, event *sentry.Event) {
129133
event.Fingerprint = fingerprint
130134
}
131135
}
136+
137+
func attrToSentryLog(group string, a slog.Attr) []attribute.Builder {
138+
key := group + a.Key
139+
switch a.Value.Kind() {
140+
case slog.KindAny:
141+
return []attribute.Builder{attribute.String(key, fmt.Sprintf("%+v", a.Value.Any()))}
142+
case slog.KindBool:
143+
return []attribute.Builder{attribute.Bool(key, a.Value.Bool())}
144+
case slog.KindDuration:
145+
return []attribute.Builder{attribute.String(key, a.Value.Duration().String())}
146+
case slog.KindFloat64:
147+
return []attribute.Builder{attribute.Float64(key, a.Value.Float64())}
148+
case slog.KindInt64:
149+
return []attribute.Builder{attribute.Int64(key, a.Value.Int64())}
150+
case slog.KindString:
151+
return []attribute.Builder{attribute.String(key, a.Value.String())}
152+
case slog.KindTime:
153+
return []attribute.Builder{attribute.String(key, a.Value.Time().Format(time.RFC3339))}
154+
case slog.KindUint64:
155+
val := a.Value.Uint64()
156+
if val <= math.MaxInt64 {
157+
return []attribute.Builder{attribute.Int64(key, int64(val))}
158+
} else {
159+
return []attribute.Builder{attribute.String(key, strconv.FormatUint(val, 10))}
160+
}
161+
case slog.KindLogValuer:
162+
return []attribute.Builder{attribute.String(key, a.Value.LogValuer().LogValue().String())}
163+
case slog.KindGroup:
164+
// Handle nested group attributes
165+
var attrs []attribute.Builder
166+
groupPrefix := key
167+
if groupPrefix != "" {
168+
groupPrefix += "."
169+
}
170+
for _, subAttr := range a.Value.Group() {
171+
attrs = append(attrs, attrToSentryLog(groupPrefix, subAttr)...)
172+
}
173+
return attrs
174+
}
175+
176+
sentry.DebugLogger.Printf("Invalid type: dropping attribute with key: %v and value: %v", a.Key, a.Value)
177+
return []attribute.Builder{}
178+
}

0 commit comments

Comments
 (0)