-
-
Save danesparza/7053c3d4bb8f27715cafaa009bcb83b5 to your computer and use it in GitHub Desktop.
Send zerolog errors to Sentry
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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