Skip to content

Conversation

giortzisg
Copy link
Contributor

@giortzisg giortzisg commented Jun 10, 2025

Description

Closes #1027.
Changing the current sentryslog implementation to add support for creating sentry Logs, using different option Levels:

        ctx := context.Background()
	logger := slog.New(sentryslog.Option{
		LogLevel:   []slog.Level{slog.LevelWarn, slog.LevelInfo},
		EventLevel: []slog.Level{slog.LevelError, sentryslog.LevelFatal},
	}.NewSentryHandler(ctx))
	logger = logger.With("release", "v1.0.0")

	// message level is Error and will be handled by both the event & log handler.
	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")

Somes notes:

  • It would be good to add context.Context while creating the NewSentryHandler, but that would be a breaking change.

@giortzisg giortzisg requested a review from cleptric June 10, 2025 13:54
Copy link

codecov bot commented Jun 10, 2025

Codecov Report

Attention: Patch coverage is 95.09202% with 8 lines in your changes missing coverage. Please review.

Project coverage is 86.78%. Comparing base (f43c181) to head (7f9c43d).
Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
slog/sentryslog.go 95.16% 5 Missing and 1 partial ⚠️
slog/converter.go 94.87% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1033      +/-   ##
==========================================
+ Coverage   86.27%   86.78%   +0.50%     
==========================================
  Files          55       55              
  Lines        5670     5819     +149     
==========================================
+ Hits         4892     5050     +158     
+ Misses        637      627      -10     
- Partials      141      142       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cleptric cleptric added the Feature Issue type label Jun 10, 2025
@cleptric cleptric requested a review from Copilot June 13, 2025 06:26
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Adds support for capturing Sentry logs (in addition to events) by introducing a CaptureType enum and a new log-based handler path.

  • Introduce CaptureType enum (EventType vs. LogType) and make NewSentryHandler accept a context.Context
  • Implement handleAsLog in SentryHandler to send structured logs via sentry-go/attribute
  • Expand unit tests to cover both event- and log-based captures

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
slog/sentryslog.go Added CaptureType, context-aware handler, and handleAsLog
slog/converter.go New attrToSentryLog helper to map slog.Attr to Sentry attributes
slog/sentryslog_test.go Updated tests for new signature and added coverage for log capture
log.go Preserve existing sentry.origin attribute when logging
Comments suppressed due to low confidence (1)

slog/sentryslog_test.go:203

  • newMockTransport mutates the global Sentry hub via CurrentHub(), which can leak state across tests. Consider cloning the hub (e.g. sentry.CurrentHub().Clone()) before binding your mock client.
func newMockTransport() (context.Context, *sentry.MockTransport) {

@giortzisg giortzisg changed the title Add slog integration Add slog logs integration Jun 13, 2025
Comment on lines 90 to 91
if o.LogLevel == nil {
o.LogLevel = slog.LevelDebug
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on defaults, if a user upgrades, log messages will be sent. Should we handle the default level differently for now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still gated behind EnableLogs right? I think thats fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeap, still gated behind it.

func attrToSentryLog(group string, a slog.Attr) []attribute.Builder {
switch a.Value.Kind() {
case slog.KindAny:
return []attribute.Builder{attribute.String(group+a.Key, fmt.Sprintf("%+v", a.Value.Any()))}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Let's move group+a.Key to the top key := group + a.Key and pass in key everywhere.

log.go Outdated
@@ -144,7 +144,9 @@ func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, me
if sdkVersion := l.client.sdkVersion; sdkVersion != "" {
attrs["sentry.sdk.version"] = Attribute{Value: sdkVersion, Type: "string"}
}
attrs["sentry.origin"] = Attribute{Value: "auto.logger.log", Type: "string"}
if _, ok := attrs["sentry.origin"]; !ok {
attrs["sentry.origin"] = Attribute{Value: "auto.logger.log", Type: "string"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h: Origin should not be set by default, only for logs generated by logging integrations.

This is because explicit calls to the logging API are assumed to be manual, so no applicable origin.

@@ -42,9 +43,17 @@ var (
)

type Option struct {
// Level sets the minimum log level to capture and send to Sentry.
// Logs at this level and above will be processed. The default level is debug.
// Deprecated: Level is kept for backwards compatibility and defaults to EventLevel.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to deprecate, we need to provide the alternative (don't use it, use X instead, etc.).

It would also be nice to throw deprecations in https://github.com/getsentry/sentry-go/blob/master/MIGRATION.md so that they can be easily searched for and linked in the changelog, but I'll leave the exact process up to you.

Comment on lines 90 to 91
if o.LogLevel == nil {
o.LogLevel = slog.LevelDebug
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still gated behind EnableLogs right? I think thats fine.


if h.eventHandler.Enabled(ctx, record.Level) {
if handleErr := h.eventHandler.Handle(ctx, record); handleErr != nil {
err = handleErr
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: shouldn't we return the first error that we get? Otherwise I'd prefer if we construct an error that is the combo of of both if we hit both eventHandler and logHandler errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to handle both events and logs so can't return on first error. But it would be probably nicer to wrap them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, let's wrap them.

@@ -82,24 +99,80 @@ func (o Option) NewSentryHandler() slog.Handler {
o.AttrFromContext = []func(ctx context.Context) []slog.Attr{}
}

return &SentryHandler{
logger := sentry.NewLogger(ctx)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this will lead to stale contexts.

Should we just create a new logger everytime on Handle? Overhead might be more, so perhaps we can just set new contexts if it changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are already using the ctx passed on sentryLogger.log to update the hub if possible, so we don't need to create a new logger.

Comment on lines 152 to 153
case slog.KindLogValuer:
return []attribute.Builder{attribute.String(group+a.Key, a.Value.LogValuer().LogValue().String())}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This risks infinite recursion right? I wonder if we need to just special case this somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the comment meant for slog.KindGroup? Valuer should be fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sorry - slog.KindGroup.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably can, but not while using WithGroup. You can possibly create :

attr1 := slog.Attr{
	Key:   "group1",
	Value: slog.GroupValue(),
}

attr2 := slog.Attr{
	Key:   "group2",
	Value: slog.GroupValue(attr1),
}

and then create another group reference to the second one attr1.Value = slog.GroupValue(attr2). Should we guard against that? Seems like a pretty wacky case to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delved a bit deeper into it, slog.Attr and slog.Groups are values, so we can't really have a cycle. If a user manually creates a slog.Attr with its' Value being a pointer to a GroupValue it should be handled by slog.KindAny not slog.KindGroup.

@giortzisg giortzisg requested a review from AbhiPrasad June 17, 2025 09:18
Comment on lines 54 to 57
// LogLevel sets the minimum log level to capture and send to Sentry as a Log entry.
// Logs at this level and above will be processed as log entries.
// Defaults to slog.LevelDebug.
LogLevel slog.Leveler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know I said previously that EnableLogs gating this is fine, but I tried running this on a local go app and now I'm now debating if there should be a way to turn this off.

I can see users wanting to disable EventLevel completely and only rely on LogLevel for example.

Maybe we expose separate boolean options? What do you think?

Copy link
Contributor Author

@giortzisg giortzisg Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know if i like adding also boolean options.

It might be nicer to specify an array of log levels so the user directly specifies all the levels that wants to be captured. This also provides more fine grained control, since maybe you don't want to catch Error level for logs and want only events.

But then I don't know how we can handle and compare custom slog levels.

EDIT: we actually don't support them with the current implementation, so let me change that as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the array suggestion.

@giortzisg giortzisg requested a review from AbhiPrasad June 18, 2025 09:21
Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One note on the README, otherwise we are good

slog/README.MD Outdated

- `LogLevel`: Minimum level to send logs to Sentry. Defaults to `slog.LevelDebug`.
- `LogLevel`: Slice of specific levels to send `Log` entries to Sentry. Defaults to `[]slog.Level{slog.LevelError, LevelFatal}`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right? From below I can see o.LogLevel = []slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError, LevelFatal}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that was missed. Nice catch Abhi!

@giortzisg giortzisg merged commit 5bc6aad into master Jun 23, 2025
17 checks passed
@giortzisg giortzisg deleted the slog-integration branch June 23, 2025 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature Issue type
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Structured logging support for slog
3 participants