Files
postmoogle/vendor/github.com/archdx/zerolog-sentry/writer.go
2024-02-19 22:55:14 +02:00

438 lines
10 KiB
Go

package zlogsentry
import (
"crypto/x509"
"errors"
"io"
"net/http"
"time"
"unsafe"
"github.com/buger/jsonparser"
"github.com/getsentry/sentry-go"
"github.com/rs/zerolog"
)
var levelsMapping = map[zerolog.Level]sentry.Level{
zerolog.DebugLevel: sentry.LevelDebug,
zerolog.InfoLevel: sentry.LevelInfo,
zerolog.WarnLevel: sentry.LevelWarning,
zerolog.ErrorLevel: sentry.LevelError,
zerolog.FatalLevel: sentry.LevelFatal,
zerolog.PanicLevel: sentry.LevelFatal,
}
var _ = io.WriteCloser(new(Writer))
var now = time.Now
// Writer is a sentry events writer with std io.Writer iface.
type Writer struct {
hub *sentry.Hub
levels map[zerolog.Level]struct{}
flushTimeout time.Duration
withBreadcrumbs bool
}
// addBreadcrumb adds event as a breadcrumb
func (w *Writer) addBreadcrumb(event *sentry.Event) {
if !w.withBreadcrumbs {
return
}
// category is totally optional, but it's nice to have
var category string
if _, ok := event.Extra["category"]; ok {
if v, ok := event.Extra["category"].(string); ok {
category = v
}
}
w.hub.AddBreadcrumb(&sentry.Breadcrumb{
Category: category,
Message: event.Message,
Level: event.Level,
Data: event.Extra,
}, nil)
}
// Write handles zerolog's json and sends events to sentry.
func (w *Writer) Write(data []byte) (n int, err error) {
n = len(data)
lvl, err := w.parseLogLevel(data)
if err != nil {
return n, nil
}
event, ok := w.parseLogEvent(data)
if !ok {
return
}
event.Level, ok = levelsMapping[lvl]
if !ok {
return
}
if _, enabled := w.levels[lvl]; !enabled {
// if the level is not enabled, add event as a breadcrumb
w.addBreadcrumb(event)
return
}
w.hub.CaptureEvent(event)
// should flush before os.Exit
if event.Level == sentry.LevelFatal {
w.hub.Flush(w.flushTimeout)
}
return
}
// implements zerolog.LevelWriter
func (w *Writer) WriteLevel(level zerolog.Level, p []byte) (n int, err error) {
n = len(p)
event, ok := w.parseLogEvent(p)
if !ok {
return
}
event.Level, ok = levelsMapping[level]
if !ok {
return
}
if _, enabled := w.levels[level]; !enabled {
// if the level is not enabled, add event as a breadcrumb
w.addBreadcrumb(event)
return
}
w.hub.CaptureEvent(event)
// should flush before os.Exit
if event.Level == sentry.LevelFatal {
w.hub.Flush(w.flushTimeout)
}
return
}
// Close forces client to flush all pending events.
// Can be useful before application exits.
func (w *Writer) Close() error {
if ok := w.hub.Flush(w.flushTimeout); !ok {
return ErrFlushTimeout
}
return nil
}
// parses the log level from the encoded log
func (w *Writer) parseLogLevel(data []byte) (zerolog.Level, error) {
lvlStr, err := jsonparser.GetUnsafeString(data, zerolog.LevelFieldName)
if err != nil {
return zerolog.Disabled, nil
}
return zerolog.ParseLevel(lvlStr)
}
// parses the event except the log level
func (w *Writer) parseLogEvent(data []byte) (*sentry.Event, bool) {
const logger = "zerolog"
event := sentry.Event{
Timestamp: now(),
Logger: logger,
Extra: map[string]interface{}{},
}
err := jsonparser.ObjectEach(data, func(key, value []byte, vt jsonparser.ValueType, offset int) error {
switch string(key) {
case zerolog.MessageFieldName:
event.Message = bytesToStrUnsafe(value)
case zerolog.ErrorFieldName:
event.Exception = append(event.Exception, sentry.Exception{
Value: bytesToStrUnsafe(value),
Stacktrace: newStacktrace(),
})
case zerolog.LevelFieldName, zerolog.TimestampFieldName:
default:
event.Extra[string(key)] = bytesToStrUnsafe(value)
}
return nil
})
if err != nil {
return nil, false
}
return &event, true
}
func newStacktrace() *sentry.Stacktrace {
const (
module = "github.com/archdx/zerolog-sentry"
loggerModule = "github.com/rs/zerolog"
)
st := sentry.NewStacktrace()
threshold := len(st.Frames) - 1
// drop current module frames
for ; threshold > 0 && st.Frames[threshold].Module == module; threshold-- {
}
outer:
// try to drop zerolog module frames after logger call point
for i := threshold; i > 0; i-- {
if st.Frames[i].Module == loggerModule {
for j := i - 1; j >= 0; j-- {
if st.Frames[j].Module != loggerModule {
threshold = j
break outer
}
}
break
}
}
st.Frames = st.Frames[:threshold+1]
return st
}
func bytesToStrUnsafe(data []byte) string {
return *(*string)(unsafe.Pointer(&data))
}
// WriterOption configures sentry events writer.
type WriterOption interface {
apply(*config)
}
type optionFunc func(*config)
func (fn optionFunc) apply(c *config) { fn(c) }
type EventHintCallback func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event
type config struct {
levels []zerolog.Level
sampleRate float64
release string
environment string
serverName string
ignoreErrors []string
breadcrumbs bool
debug bool
tracing bool
debugWriter io.Writer
httpClient *http.Client
httpProxy string
httpsProxy string
caCerts *x509.CertPool
maxErrorDepth int
flushTimeout time.Duration
beforeSend sentry.EventProcessor
tracesSampleRate float64
}
// WithLevels configures zerolog levels that have to be sent to Sentry.
// Default levels are: error, fatal, panic.
func WithLevels(levels ...zerolog.Level) WriterOption {
return optionFunc(func(cfg *config) {
cfg.levels = levels
})
}
// WithSampleRate configures the sample rate as a percentage of events to be sent in the range of 0.0 to 1.0.
func WithSampleRate(rate float64) WriterOption {
return optionFunc(func(cfg *config) {
cfg.sampleRate = rate
})
}
// WithRelease configures the release to be sent with events.
func WithRelease(release string) WriterOption {
return optionFunc(func(cfg *config) {
cfg.release = release
})
}
// WithEnvironment configures the environment to be sent with events.
func WithEnvironment(environment string) WriterOption {
return optionFunc(func(cfg *config) {
cfg.environment = environment
})
}
// WithServerName configures the server name field for events. Default value is OS hostname.
func WithServerName(serverName string) WriterOption {
return optionFunc(func(cfg *config) {
cfg.serverName = serverName
})
}
// WithIgnoreErrors configures the list of regexp strings that will be used to match against event's message
// and if applicable, caught errors type and value. If the match is found, then a whole event will be dropped.
func WithIgnoreErrors(reList []string) WriterOption {
return optionFunc(func(cfg *config) {
cfg.ignoreErrors = reList
})
}
// WithBreadcrumbs enables sentry client breadcrumbs.
func WithBreadcrumbs() WriterOption {
return optionFunc(func(cfg *config) {
cfg.breadcrumbs = true
})
}
// WithDebug enables sentry client debug logs.
func WithDebug() WriterOption {
return optionFunc(func(cfg *config) {
cfg.debug = true
})
}
// WithTracing enables sentry client tracing.
func WithTracing() WriterOption {
return optionFunc(func(cfg *config) {
cfg.tracing = true
})
}
// WithTracingSampleRate sets tracing sample rate.
func WithTracingSampleRate(tsr float64) WriterOption {
return optionFunc(func(cfg *config) {
cfg.tracesSampleRate = tsr
})
}
// WithBeforeSend sets a callback which is called before event is sent.
func WithBeforeSend(beforeSend sentry.EventProcessor) WriterOption {
return optionFunc(func(cfg *config) {
cfg.beforeSend = beforeSend
})
}
// WithDebugWriter enables sentry client tracing.
func WithDebugWriter(w io.Writer) WriterOption {
return optionFunc(func(cfg *config) {
cfg.debugWriter = w
})
}
// WithHttpClient sets custom http client.
func WithHttpClient(httpClient *http.Client) WriterOption {
return optionFunc(func(cfg *config) {
cfg.httpClient = httpClient
})
}
// WithHttpProxy enables sentry client tracing.
func WithHttpProxy(proxy string) WriterOption {
return optionFunc(func(cfg *config) {
cfg.httpProxy = proxy
})
}
// WithHttpsProxy enables sentry client tracing.
func WithHttpsProxy(proxy string) WriterOption {
return optionFunc(func(cfg *config) {
cfg.httpsProxy = proxy
})
}
// WithCaCerts enables sentry client tracing.
func WithCaCerts(caCerts *x509.CertPool) WriterOption {
return optionFunc(func(cfg *config) {
cfg.caCerts = caCerts
})
}
// WithMaxErrorDepth sets the max depth of error chain.
func WithMaxErrorDepth(maxErrorDepth int) WriterOption {
return optionFunc(func(cfg *config) {
cfg.maxErrorDepth = maxErrorDepth
})
}
// New creates writer with provided DSN and options.
func New(dsn string, opts ...WriterOption) (*Writer, error) {
cfg := newDefaultConfig()
for _, opt := range opts {
opt.apply(&cfg)
}
err := sentry.Init(sentry.ClientOptions{
Dsn: dsn,
SampleRate: cfg.sampleRate,
Release: cfg.release,
Environment: cfg.environment,
ServerName: cfg.serverName,
IgnoreErrors: cfg.ignoreErrors,
Debug: cfg.debug,
EnableTracing: cfg.tracing,
DebugWriter: cfg.debugWriter,
HTTPClient: cfg.httpClient,
HTTPProxy: cfg.httpProxy,
HTTPSProxy: cfg.httpsProxy,
CaCerts: cfg.caCerts,
MaxErrorDepth: cfg.maxErrorDepth,
BeforeSend: cfg.beforeSend,
TracesSampleRate: cfg.tracesSampleRate,
})
if err != nil {
return nil, err
}
levels := make(map[zerolog.Level]struct{}, len(cfg.levels))
for _, lvl := range cfg.levels {
levels[lvl] = struct{}{}
}
return &Writer{
hub: sentry.CurrentHub(),
levels: levels,
flushTimeout: cfg.flushTimeout,
withBreadcrumbs: cfg.breadcrumbs,
}, nil
}
// NewWithHub creates a writer using an existing sentry Hub and options.
func NewWithHub(hub *sentry.Hub, opts ...WriterOption) (*Writer, error) {
if hub == nil {
return nil, errors.New("hub cannot be nil")
}
cfg := newDefaultConfig()
for _, opt := range opts {
opt.apply(&cfg)
}
levels := make(map[zerolog.Level]struct{}, len(cfg.levels))
for _, lvl := range cfg.levels {
levels[lvl] = struct{}{}
}
return &Writer{
hub: hub,
levels: levels,
flushTimeout: cfg.flushTimeout,
}, nil
}
func newDefaultConfig() config {
return config{
levels: []zerolog.Level{
zerolog.ErrorLevel,
zerolog.FatalLevel,
zerolog.PanicLevel,
},
sampleRate: 1.0,
flushTimeout: 3 * time.Second,
}
}