Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions zerolog/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package sentryzerolog_test

import (
"context"
"os"

"github.com/getsentry/sentry-go"
sentryzerolog "github.com/getsentry/sentry-go/zerolog"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

func ExampleNewSentryLogger() {
// Assuming you're using the zerolog/log package:
// import "github.com/rs/zerolog/log"

log.Logger = zerolog.New(
zerolog.MultiLevelWriter(zerolog.ConsoleWriter{Out: os.Stdout}, sentryzerolog.NewSentryLogger()),
).With().Timestamp().Logger()

log.Info().Msg("This is an info message")

// You can populate context from *(net/http).Request.Context()
// or any other context that has Sentry Hub on it.
// The parent context will be respected and the event will be
// linked to the parent event.
ctx := context.Background()
ctx = sentry.SetHubOnContext(ctx, sentry.CurrentHub().Clone())
log.Error().
Ctx(ctx).
Err(os.ErrClosed).
Str("file_name", "foo.txt").
Msg("File does not exists")
}
1 change: 1 addition & 0 deletions zerolog/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ replace github.com/getsentry/sentry-go => ../
require (
github.com/buger/jsonparser v1.1.1
github.com/getsentry/sentry-go v0.33.0
github.com/google/go-cmp v0.5.9
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0
)
Expand Down
129 changes: 129 additions & 0 deletions zerolog/sentrylogger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package sentryzerolog

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"time"

"github.com/getsentry/sentry-go"
"github.com/getsentry/sentry-go/attribute"
"github.com/rs/zerolog"
)

// NewSentryLogger creates a new instance of SentryLogger that implements
// zerolog.LevelWriter interface. This should be used for sending zerolog
// events to Sentry as logs as opposed to events/errors.
//
// If you want to send events/errors to Sentry, use the New() function instead.
func NewSentryLogger() SentryLogger {
return SentryLogger{}
}

// SentryLogger implements zerolog.LevelWriter.
// This should be used for sending zerolog events to Sentry as logs as opposed
// to events/errors.
type SentryLogger struct{}

var _ zerolog.LevelWriter = (*SentryLogger)(nil)
var _ io.Closer = (*SentryLogger)(nil)

func (s SentryLogger) Write(p []byte) (n int, err error) {
return s.WriteLevel(zerolog.DebugLevel, p)
}

func (s SentryLogger) WriteLevel(level zerolog.Level, p []byte) (n int, err error) {
return s.runContext(context.Background(), level, p)
}

func (s SentryLogger) runContext(ctx context.Context, level zerolog.Level, p []byte) (n int, err error) {
if !sentry.HasHubOnContext(ctx) {
hub := sentry.CurrentHub()
if hub == nil {
hub = sentry.NewHub(nil, sentry.NewScope())
}

Check warning on line 46 in zerolog/sentrylogger.go

View check run for this annotation

Codecov / codecov/patch

zerolog/sentrylogger.go#L45-L46

Added lines #L45 - L46 were not covered by tests

ctx = sentry.SetHubOnContext(context.Background(), hub)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
ctx = sentry.SetHubOnContext(context.Background(), hub)
ctx = sentry.SetHubOnContext(ctx, hub)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No can't do. You must assume that the previous ctx is nil.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

really? do you have links to code/docs about this?

}

var evt map[string]any
d := json.NewDecoder(bytes.NewReader(p))
err = d.Decode(&evt)
if err != nil {
return 0, fmt.Errorf("cannot decode event: %s", err.Error())
}

l := sentry.NewLogger(ctx)
var message string
for field, value := range evt {
switch field {
case zerolog.LevelFieldName:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

these names can be customized: https://github.com/rs/zerolog/blob/master/README.md#customize-automatic-field-names

we prob need a helper

func (s SentryLogger) getFieldNames() (level, message, timestamp string) {
	return zerolog.LevelFieldName,
		zerolog.MessageFieldName,
		zerolog.TimestampFieldName
}

levelString, _ := value.(string)
level, err = zerolog.ParseLevel(levelString)
if err != nil {
level = zerolog.DebugLevel
}
case zerolog.MessageFieldName:
message, _ = value.(string)
case zerolog.TimestampFieldName:
continue
default:
switch valueType := value.(type) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't we have a case for int here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did test it with every possible Go types. Yet everything falls down to just 3 types (plus their array variant): string, boolean, float64

Copy link
Copy Markdown
Contributor

@AbhiPrasad AbhiPrasad Jun 9, 2025

Choose a reason for hiding this comment

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

We'll want to still explicitly use attribute.Int because we treat these differently in the backend. In the future there will be operations you can only do on integers you can can't use on floats.

case string:
l.SetAttributes(attribute.String(field, valueType))
case bool:
l.SetAttributes(attribute.Bool(field, valueType))
case float64:
l.SetAttributes(attribute.Float64(field, float64(valueType)))
case []any:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we should have a default case here that just serializes the value and sends as a string type.

for i, v := range valueType {
switch vv := v.(type) {
case string:
l.SetAttributes(attribute.String(fmt.Sprintf("%s.%d", field, i), vv))
case bool:
l.SetAttributes(attribute.Bool(fmt.Sprintf("%s.%d", field, i), vv))
case float64:
l.SetAttributes(attribute.Float64(fmt.Sprintf("%s.%d", field, i), float64(vv)))
}
}
}
}
}

if message == "" {
message = string(p)
}

switch level {
case zerolog.TraceLevel:
l.Trace(ctx, message)
case zerolog.DebugLevel:
l.Debug(ctx, message)
case zerolog.InfoLevel:
l.Info(ctx, message)
case zerolog.WarnLevel:
l.Warn(ctx, message)
case zerolog.ErrorLevel:
l.Error(ctx, message)
case zerolog.FatalLevel:
l.Fatal(ctx, message)

Check warning on line 111 in zerolog/sentrylogger.go

View check run for this annotation

Codecov / codecov/patch

zerolog/sentrylogger.go#L110-L111

Added lines #L110 - L111 were not covered by tests
case zerolog.PanicLevel:
l.Panic(ctx, message)
default:
// for disabled level
break
}

// Zerolog requires that the original number of bytes is returned.
// Otherwise, it will return "short write" error.
return len(p), nil
}

// Close should not be called directly.
// It should be called internally by zerolog.
func (s SentryLogger) Close() error {
sentry.Flush(time.Second)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we make this configurable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Perhaps, yes.

return nil
}
Loading
Loading