-
Notifications
You must be signed in to change notification settings - Fork 245
feat: structured logging support for zerolog #1031
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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") | ||
| } |
| 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()) | ||
| } | ||
|
|
||
| ctx = sentry.SetHubOnContext(context.Background(), hub) | ||
| } | ||
|
|
||
| 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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't we have a case for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll want to still explicitly use |
||
| 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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we make this configurable?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps, yes. |
||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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
ctxisnil.There was a problem hiding this comment.
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?