438 lines
10 KiB
Go
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,
|
|
}
|
|
}
|