Skip to content
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

Add zap integration #958

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .craft.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ targets:
- name: github
tagPrefix: zerolog/v
tagOnly: true
- name: github
tagPrefix: zap/v
tagOnly: true
- name: registry
sdks:
github:getsentry/sentry-go:
43 changes: 43 additions & 0 deletions _examples/zap/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"time"

"github.com/getsentry/sentry-go"
sentryzap "github.com/getsentry/sentry-go/zap"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func main() {
// Initialize Sentry
client, err := sentry.NewClient(sentry.ClientOptions{
Dsn: "your-public-dsn",
})
if err != nil {
panic(err)
}
defer sentry.Flush(2 * time.Second)

// Configure Sentry Zap Core
sentryCore, err := sentryzap.NewCore(
sentryzap.Configuration{
Level: zapcore.ErrorLevel,
BreadcrumbLevel: zapcore.InfoLevel,
EnableBreadcrumbs: true,
FlushTimeout: 3 * time.Second,
},
sentryzap.NewSentryClientFromClient(client),
)
if err != nil {
panic(err)
}

// Create a logger with Sentry Core
logger := sentryzap.AttachCoreToLogger(sentryCore, zap.NewExample())

// Example Logs
logger.Info("This is an info message") // Breadcrumb
logger.Error("This is an error message") // Captured as an event
logger.Fatal("This is a fatal message") // Captured as an event and flushes
}
90 changes: 90 additions & 0 deletions zap/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<p align="center">
<a href="https://sentry.io" target="_blank" align="center">
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280">
</a>
<br />
</p>

# Official Sentry Zap Core for Sentry-Go SDK

**Go.dev Documentation:** [https://pkg.go.dev/github.com/getsentry/sentryzap](https://pkg.go.dev/github.com/getsentry/sentryzap)
**Example Usage:** [https://github.com/getsentry/sentry-go/tree/master/_examples/zap](https://github.com/getsentry/sentry-go/tree/master/_examples/zap)

## Overview

This package provides a core for the [Zap](https://github.com/uber-go/zap) logger, enabling seamless integration with [Sentry](https://sentry.io). With this core, logs at specific levels can be captured as Sentry events, while others can be added as breadcrumbs for enhanced context.

## Installation

```sh
go get github.com/getsentry/sentry-go/zap
```

## Usage

```go
package main

import (
"time"

"github.com/getsentry/sentry-go"
"github.com/getsentry/sentry-go/zap"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func main() {
// Initialize Sentry
client, err := sentry.NewClient(sentry.ClientOptions{
Dsn: "your-public-dsn",
})
if err != nil {
panic(err)
}
defer sentry.Flush(2 * time.Second)

// Configure Sentry Zap Core
sentryCore, err := sentryzap.NewCore(
sentryzap.Configuration{
Level: zapcore.ErrorLevel,
BreadcrumbLevel: zapcore.InfoLevel,
EnableBreadcrumbs: true,
FlushTimeout: 3 * time.Second,
},
sentryzap.NewSentryClientFromClient(client),
)
if err != nil {
panic(err)
}

// Create a logger with Sentry Core
logger := sentryzap.AttachCoreToLogger(sentryCore, zap.NewExample())

// Example Logs
logger.Info("This is an info message") // Breadcrumb
logger.Error("This is an error message") // Captured as an event
logger.Fatal("This is a fatal message") // Captured as an event and flushes
}
```

## Configuration

The `sentryzap.NewCore` function accepts a `sentryzap.Configuration` struct, which allows for the following configuration options:

- Tags: A map of key-value pairs to add as tags to every event sent to Sentry.
- LoggerNameKey: Specifies the field key used to represent the logger name in the Sentry event. If empty, this feature is disabled.
- DisableStacktrace: If true, stack traces are not included in events sent to Sentry.
- Level: The minimum severity level for logs to be sent to Sentry.
- EnableBreadcrumbs: If true, logs below the event level are added as breadcrumbs to Sentry.
- BreadcrumbLevel: The minimum severity level for breadcrumbs to be recorded.
- Note: BreadcrumbLevel must be lower than or equal to Level.
- MaxBreadcrumbs: The maximum number of breadcrumbs to retain (default: 100).
- FlushTimeout: The maximum duration to wait for events to flush when calling Sync() (default: 3 seconds).
- Hub: Overrides the default Sentry hub.
- FrameMatcher: A function to filter stack trace frames.

## Notes

- Always call sentry.Flush to ensure all events are sent to Sentry before program termination.
- Use sentryzap.AttachCoreToLogger to attach the Sentry core to your existing logger seamlessly.
225 changes: 225 additions & 0 deletions zap/core.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package sentryzap

import (
"slices"
"time"

"github.com/getsentry/sentry-go"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type core struct {
client *sentry.Client
cfg *Configuration
zapcore.LevelEnabler
flushTimeout time.Duration

sentryScope *sentry.Scope
errs []error
fields map[string]any
}

func (c *core) With(fs []zapcore.Field) zapcore.Core {
return c.with(fs)

Check warning on line 24 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L23-L24

Added lines #L23 - L24 were not covered by tests
}

func (c *core) with(fs []zapcore.Field) *core {
fields := make(map[string]interface{}, len(c.fields)+len(fs))
for k, v := range c.fields {
fields[k] = v
}

Check warning on line 31 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L30-L31

Added lines #L30 - L31 were not covered by tests

errs := append([]error{}, c.errs...)
var sentryScope *sentry.Scope

enc := zapcore.NewMapObjectEncoder()
for _, f := range fs {
f.AddTo(enc)
switch f.Type {
case zapcore.ErrorType:
errs = append(errs, f.Interface.(error))
case zapcore.SkipType:
if scope := getScope(f); scope != nil {
sentryScope = scope
}

Check warning on line 45 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L38-L45

Added lines #L38 - L45 were not covered by tests
}
}

for k, v := range enc.Fields {
fields[k] = v
}

Check warning on line 51 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L50-L51

Added lines #L50 - L51 were not covered by tests

return &core{
client: c.client,
cfg: c.cfg,
LevelEnabler: c.LevelEnabler,
flushTimeout: c.flushTimeout,
sentryScope: sentryScope,
errs: errs,
fields: fields,
}
}

func (c *core) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if c.cfg.EnableBreadcrumbs && c.cfg.BreadcrumbLevel.Enabled(ent.Level) {
return ce.AddCore(ent, c)
}

Check warning on line 67 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L66-L67

Added lines #L66 - L67 were not covered by tests
if c.cfg.Level.Enabled(ent.Level) {
return ce.AddCore(ent, c)
}
return ce

Check warning on line 71 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L71

Added line #L71 was not covered by tests
}

func (c *core) Write(ent zapcore.Entry, fs []zapcore.Field) error {
clone := c.with(c.addSpecialFields(ent, fs))

if c.shouldAddBreadcrumb(ent.Level) {
c.addBreadcrumb(ent, clone.fields)
}

Check warning on line 79 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L78-L79

Added lines #L78 - L79 were not covered by tests

if c.shouldLogEvent(ent.Level) {
c.logEvent(ent, fs, clone)
}

if ent.Level > zapcore.ErrorLevel {
return c.Sync()
}

Check warning on line 87 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L86-L87

Added lines #L86 - L87 were not covered by tests
return nil
}

func (c *core) Sync() error {
c.client.Flush(c.flushTimeout)
return nil

Check warning on line 93 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L91-L93

Added lines #L91 - L93 were not covered by tests
}

func (c *core) shouldAddBreadcrumb(level zapcore.Level) bool {
return c.cfg.EnableBreadcrumbs && c.cfg.BreadcrumbLevel.Enabled(level)
}

func (c *core) shouldLogEvent(level zapcore.Level) bool {
return c.cfg.Level.Enabled(level)
}

func (c *core) addBreadcrumb(ent zapcore.Entry, fields map[string]interface{}) {
breadcrumb := sentry.Breadcrumb{
Message: ent.Message,
Data: fields,
Level: levelMap[ent.Level],
Timestamp: ent.Time,
}
c.scope().AddBreadcrumb(&breadcrumb, c.cfg.MaxBreadcrumbs)

Check warning on line 111 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L104-L111

Added lines #L104 - L111 were not covered by tests
}

func (c *core) logEvent(ent zapcore.Entry, fs []zapcore.Field, clone *core) {
event := sentry.NewEvent()
event.Message = ent.Message
event.Timestamp = ent.Time
event.Level = levelMap[ent.Level]
event.Tags = c.collectTags(fs)
event.Extra = clone.fields
event.Exception = clone.createExceptions()

if event.Exception == nil && !c.cfg.DisableStacktrace && c.client.Options().AttachStacktrace {
stacktrace := sentry.NewStacktrace()
if stacktrace != nil {
stacktrace.Frames = c.filterFrames(stacktrace.Frames)
event.Threads = []sentry.Thread{{Stacktrace: stacktrace, Current: true}}
}

Check warning on line 128 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L124-L128

Added lines #L124 - L128 were not covered by tests
}

hint := c.getEventHint(fs)
c.client.CaptureEvent(event, hint, c.scope())
}

func (c *core) addSpecialFields(ent zapcore.Entry, fs []zapcore.Field) []zapcore.Field {
if c.cfg.LoggerNameKey != "" && ent.LoggerName != "" {
fs = append(fs, zap.String(c.cfg.LoggerNameKey, ent.LoggerName))
}

Check warning on line 138 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L137-L138

Added lines #L137 - L138 were not covered by tests
return fs
}

func (c *core) createExceptions() []sentry.Exception {
if len(c.errs) == 0 {
return nil
}

processedErrors := make(map[string]struct{})
exceptions := []sentry.Exception{}

for i := len(c.errs) - 1; i >= 0; i-- {
exceptions = c.addExceptionsFromError(exceptions, processedErrors, c.errs[i])
}

Check warning on line 152 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L147-L152

Added lines #L147 - L152 were not covered by tests

slices.Reverse(exceptions)
return exceptions

Check warning on line 155 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L154-L155

Added lines #L154 - L155 were not covered by tests
}

func (c *core) collectTags(fs []zapcore.Field) map[string]string {
tags := make(map[string]string, len(c.cfg.Tags))
for k, v := range c.cfg.Tags {
tags[k] = v
}

Check warning on line 162 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L161-L162

Added lines #L161 - L162 were not covered by tests
for _, f := range fs {
if f.Type == zapcore.SkipType {
if tag, ok := f.Interface.(tagField); ok {
tags[tag.Key] = tag.Value
}

Check warning on line 167 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L164-L167

Added lines #L164 - L167 were not covered by tests
}
}
return tags
}

func (c *core) addExceptionsFromError(
exceptions []sentry.Exception,
processedErrors map[string]struct{},
err error,
) []sentry.Exception {
for i := 0; i < maxErrorDepth && err != nil; i++ {
key := getTypeOf(err)
if _, seen := processedErrors[key]; seen {
break

Check warning on line 181 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L181

Added line #L181 was not covered by tests
}
processedErrors[key] = struct{}{}

exception := sentry.Exception{Value: err.Error(), Type: getTypeName(err)}
if !c.cfg.DisableStacktrace {
stacktrace := sentry.ExtractStacktrace(err)
if stacktrace != nil {
stacktrace.Frames = c.filterFrames(stacktrace.Frames)
exception.Stacktrace = stacktrace
}

Check warning on line 191 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L189-L191

Added lines #L189 - L191 were not covered by tests
}
exceptions = append(exceptions, exception)

err = unwrapError(err)
}
return exceptions
}

func (c *core) getEventHint(fs []zapcore.Field) *sentry.EventHint {
for _, f := range fs {
if f.Type == zapcore.SkipType {
if ctxField, ok := f.Interface.(ctxField); ok {
return &sentry.EventHint{Context: ctxField.Value}
}

Check warning on line 205 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L202-L205

Added lines #L202 - L205 were not covered by tests
}
}
return nil
}

func (c *core) hub() *sentry.Hub {
if c.cfg.Hub != nil {
return c.cfg.Hub
}

Check warning on line 214 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L213-L214

Added lines #L213 - L214 were not covered by tests

return sentry.CurrentHub()
}

func (c *core) scope() *sentry.Scope {
if c.sentryScope != nil {
return c.sentryScope
}

Check warning on line 222 in zap/core.go

View check run for this annotation

Codecov / codecov/patch

zap/core.go#L221-L222

Added lines #L221 - L222 were not covered by tests

return c.hub().Scope()
}
Loading
Loading