Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@asdine
Last active September 17, 2022 23:40
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save asdine/f821abe6189a04250ae61b77a3048bd9 to your computer and use it in GitHub Desktop.
Save asdine/f821abe6189a04250ae61b77a3048bd9 to your computer and use it in GitHub Desktop.
Send zerolog errors to Sentry
package logger
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"runtime"
"time"
"github.com/getsentry/raven-go"
"github.com/mattn/go-isatty"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/tidwall/gjson"
)
var errSkipEvent = errors.New("skip")
// CreateLogger creates a logger that logs everything to stderr.
// If the SENTRY_DSN environment variable is provided, it also sends events reported on error level to Sentry.
// Every event logged on error level will get unmarshaled and transformed into a Raven packet
// to be sent to Sentry.
func CreateLogger(lvl string) (zerolog.Logger, io.Closer, error) {
dsn := os.Getenv("SENTRY_DSN")
if dsn == "" {
return newLogger(lvl, os.Stderr), nil, nil
}
client, err := raven.New(dsn)
if err != nil {
return zerolog.Nop(), nil, err
}
pr, pw := io.Pipe()
go func() {
defer client.Close()
dec := json.NewDecoder(pr)
for {
var e logEvent
err := dec.Decode(&e)
if err == io.EOF {
return
}
if err == errSkipEvent {
continue
}
if err != nil {
fmt.Fprintf(os.Stderr, "unmarshaling log failed with error %v\n", err)
continue
}
packet := raven.Packet{
Message: e.Msg,
Timestamp: raven.Timestamp(e.Time),
Level: raven.ERROR,
Platform: "go",
Project: "foo",
Logger: "zerolog",
Release: "vx.x.x",
Culprit: e.Err.Err,
}
if e.Err.Stacktrace != nil {
packet.Interfaces = append(packet.Interfaces, e.Err.Stacktrace)
}
if e.IP != "" {
packet.Interfaces = append(packet.Interfaces, &raven.User{IP: e.IP})
}
if e.URL != "" {
h := raven.Http{
URL: e.URL,
Method: e.Method,
Headers: make(map[string]string),
}
if e.UserAgent != "" {
h.Headers["User-Agent"] = e.UserAgent
}
packet.Interfaces = append(packet.Interfaces, &h)
}
client.Capture(&packet, nil)
}
}()
// setup a global function that transforms any error passed to
// zerolog to an error with stack strace.
zerolog.ErrorMarshalFunc = func(err error) interface{} {
es := errWithStackTrace{
Err: err.Error(),
}
if _, ok := err.(stackTracer); !ok {
err = errors.WithStack(err)
}
es.Stacktrace = stackTraceToSentry(err.(stackTracer).StackTrace())
return &es
}
return newLogger(lvl, io.MultiWriter(os.Stderr, pw)), pw, nil
}
type errWithStackTrace struct {
Err string `json:"error"`
Stacktrace *raven.Stacktrace `json:"stacktrace"`
}
type stackTracer interface {
StackTrace() errors.StackTrace
}
type logEvent struct {
Level string `json:"level"`
Msg string `json:"message"`
Err errWithStackTrace `json:"error"`
Time time.Time `json:"time"`
Status int `json:"status"`
UserAgent string `json:"user_agent"`
Method string `json:"method"`
URL string `json:"url"`
IP string `json:"ip"`
}
// unmarshal only if the level is error.
func (l *logEvent) UnmarshalJSON(data []byte) error {
res := gjson.Get(string(data), "level")
if !res.Exists() || res.String() != "error" {
return errSkipEvent
}
type event logEvent
return json.Unmarshal(data, (*event)(l))
}
func stackTraceToSentry(st errors.StackTrace) *raven.Stacktrace {
var frames []*raven.StacktraceFrame
for _, f := range st {
pc := uintptr(f) - 1
fn := runtime.FuncForPC(pc)
var funcName, file string
var line int
if fn != nil {
file, line = fn.FileLine(pc)
funcName = fn.Name()
} else {
file = "unknown"
funcName = "unknown"
}
frame := raven.NewStacktraceFrame(pc, funcName, file, line, 3, nil)
if frame != nil {
frames = append([]*raven.StacktraceFrame{frame}, frames...)
}
}
return &raven.Stacktrace{Frames: frames}
}
// newLogger returns a configured logger.
func newLogger(level string, w io.Writer) zerolog.Logger {
logger := zerolog.New(w).With().Timestamp().Logger()
lvl, err := zerolog.ParseLevel(level)
if err != nil {
log.Fatal(err)
}
logger = logger.Level(lvl)
// pretty print during development
if f, ok := w.(*os.File); ok {
if isatty.IsTerminal(f.Fd()) {
logger = logger.Output(zerolog.ConsoleWriter{Out: f})
}
}
// replace standard logger with zerolog
log.SetFlags(0)
log.SetOutput(logger)
return logger
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment