Skip to content

Commit

Permalink
Merge pull request #6 from miloconway/add-stacktrace
Browse files Browse the repository at this point in the history
Add stacktrace functionality
  • Loading branch information
evalphobia committed Oct 15, 2015
2 parents 9ae3a97 + 6fdd969 commit e42a087
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 13 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ hook, err := logrus_sentry.NewWithTagsSentryHook(YOUR_DSN, tags, levels)

```

If you wish to initialize a SentryHook with an already initialized raven client, you can use
If you wish to initialize a SentryHook with an already initialized raven client, you can use
the `NewWithClientSentryHook` constructor:

```go
Expand Down Expand Up @@ -114,3 +114,21 @@ with a call to `NewSentryHook`. This can be changed by assigning a value to the
hook, _ := logrus_sentry.NewSentryHook(...)
hook.Timeout = 20*time.Second
```

## Enabling Stacktraces

By default the hook will not send any stacktraces. However, this can be enabled
with:

```go
hook, _ := logrus_sentry.NewSentryHook(...)
hook.StacktraceConfiguration.Enable = true
```

Subsequent calls to `logger.Error` and above will create a stacktrace.

Other configuration options are:
- `StacktraceConfiguration.Level` the logrus level at which to start capturing stacktraces.
- `StacktraceConfiguration.Skip` how many stack frames to skip before stacktrace starts recording.
- `StacktraceConfiguration.Context` the number of lines to include around a stack frame for context.
- `StacktraceConfiguration.InAppPrefixes` the prefixes that will be matched against the stack frame to identify it as in_app
41 changes: 37 additions & 4 deletions sentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,29 @@ type SentryHook struct {
// Timeout sets the time to wait for a delivery error from the sentry server.
// If this is set to zero the server will not wait for any response and will
// consider the message correctly sent
Timeout time.Duration
Timeout time.Duration
StacktraceConfiguration stacktraceConfiguration

client *raven.Client
levels []logrus.Level
}

// stacktraceConfiguration allows for configuring stacktraces
type stacktraceConfiguration struct {
// whether stacktraces should be enabled
Enable bool
// the level at which to start capturing stacktraces
Level logrus.Level
// how many stack frames to skip before stacktrace starts recording
Skip int
// the number of lines to include around a stack frame for context
Context int
// the prefixes that will be matched against the stack frame.
// if the stack frame's package matches one of these prefixes
// sentry will identify the stack frame as "in_app"
InAppPrefixes []string
}

// NewSentryHook creates a hook to be added to an instance of logger
// and initializes the raven client.
// This method sets the timeout to 100 milliseconds.
Expand All @@ -72,7 +89,7 @@ func NewSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) {
if err != nil {
return nil, err
}
return &SentryHook{100 * time.Millisecond, client, levels}, nil
return NewWithClientSentryHook(client, levels)
}

// NewWithTagsSentryHook creates a hook with tags to be added to an instance
Expand All @@ -83,13 +100,24 @@ func NewWithTagsSentryHook(DSN string, tags map[string]string, levels []logrus.L
if err != nil {
return nil, err
}
return &SentryHook{100 * time.Millisecond, client, levels}, nil
return NewWithClientSentryHook(client, levels)
}

// NewWithClientSentryHook creates a hook using an initialized raven client.
// This method sets the timeout to 100 milliseconds.
func NewWithClientSentryHook(client *raven.Client, levels []logrus.Level) (*SentryHook, error) {
return &SentryHook{100 * time.Millisecond, client, levels}, nil
return &SentryHook{
Timeout: 100 * time.Millisecond,
StacktraceConfiguration: stacktraceConfiguration{
Enable: false,
Level: logrus.ErrorLevel,
Skip: 5,
Context: 0,
InAppPrefixes: nil,
},
client: client,
levels: levels,
}, nil
}

// Called when an event should be sent to sentry
Expand All @@ -115,6 +143,11 @@ func (hook *SentryHook) Fire(entry *logrus.Entry) error {
if req, ok := getAndDelRequest(d, "http_request"); ok {
packet.Interfaces = append(packet.Interfaces, raven.NewHttp(req))
}
stConfig := &hook.StacktraceConfiguration
if stConfig.Enable && entry.Level <= stConfig.Level {
currentStacktrace := raven.NewStacktrace(stConfig.Skip, stConfig.Context, stConfig.InAppPrefixes)
packet.Interfaces = append(packet.Interfaces, currentStacktrace)
}
packet.Extra = map[string]interface{}(d)

_, errCh := hook.client.Capture(packet, nil)
Expand Down
96 changes: 88 additions & 8 deletions sentry_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package logrus_sentry

import (
"compress/zlib"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
Expand All @@ -26,12 +29,27 @@ func getTestLogger() *logrus.Logger {
return l
}

func WithTestDSN(t *testing.T, tf func(string, <-chan *raven.Packet)) {
pch := make(chan *raven.Packet, 1)
// raven.Packet does not have a json directive for deserializing stacktrace
// so need to explicitly construct one for purpose of test
type resultPacket struct {
raven.Packet
Stacktrace raven.Stacktrace `json:stacktrace`
}

func WithTestDSN(t *testing.T, tf func(string, <-chan *resultPacket)) {
pch := make(chan *resultPacket, 1)
s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
d := json.NewDecoder(req.Body)
p := &raven.Packet{}
contentType := req.Header.Get("Content-Type")
var bodyReader io.Reader = req.Body
// underlying client will compress and encode payload above certain size
if contentType == "application/octet-stream" {
bodyReader = base64.NewDecoder(base64.StdEncoding, bodyReader)
bodyReader, _ = zlib.NewReader(bodyReader)
}

d := json.NewDecoder(bodyReader)
p := &resultPacket{}
err := d.Decode(p)
if err != nil {
t.Fatal(err.Error())
Expand All @@ -51,7 +69,7 @@ func WithTestDSN(t *testing.T, tf func(string, <-chan *raven.Packet)) {
}

func TestSpecialFields(t *testing.T) {
WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) {
logger := getTestLogger()

hook, err := NewSentryHook(dsn, []logrus.Level{
Expand Down Expand Up @@ -82,7 +100,7 @@ func TestSpecialFields(t *testing.T) {
}

func TestSentryHandler(t *testing.T) {
WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) {
logger := getTestLogger()
hook, err := NewSentryHook(dsn, []logrus.Level{
logrus.ErrorLevel,
Expand All @@ -101,7 +119,7 @@ func TestSentryHandler(t *testing.T) {
}

func TestSentryWithClient(t *testing.T) {
WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) {
logger := getTestLogger()

client, _ := raven.New(dsn)
Expand All @@ -123,7 +141,7 @@ func TestSentryWithClient(t *testing.T) {
}

func TestSentryTags(t *testing.T) {
WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) {
logger := getTestLogger()
tags := map[string]string{
"site": "test",
Expand Down Expand Up @@ -152,3 +170,65 @@ func TestSentryTags(t *testing.T) {
}
})
}

func TestSentryStacktrace(t *testing.T) {
WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) {
logger := getTestLogger()
hook, err := NewSentryHook(dsn, []logrus.Level{
logrus.ErrorLevel,
logrus.InfoLevel,
})
if err != nil {
t.Fatal(err.Error())
}
logger.Hooks.Add(hook)

logger.Error(message)
packet := <-pch
stacktraceSize := len(packet.Stacktrace.Frames)
if stacktraceSize != 0 {
t.Error("Stacktrace should be empty as it is not enabled")
}

hook.StacktraceConfiguration.Enable = true

logger.Error(message) // this is the call that the last frame of stacktrace should capture
expectedLineno := 195 //this should be the line number of the previous line

packet = <-pch
stacktraceSize = len(packet.Stacktrace.Frames)
if stacktraceSize == 0 {
t.Error("Stacktrace should not be empty")
}
lastFrame := packet.Stacktrace.Frames[stacktraceSize-1]
expectedSuffix := "logrus_sentry/sentry_test.go"
if !strings.HasSuffix(lastFrame.Filename, expectedSuffix) {
t.Errorf("File name should have ended with %s, was %s", expectedSuffix, lastFrame.Filename)
}
if lastFrame.Lineno != expectedLineno {
t.Errorf("Line number should have been %s, was %s", expectedLineno, lastFrame.Lineno)
}
if lastFrame.InApp {
t.Error("Frame should not be identified as in_app without prefixes")
}

hook.StacktraceConfiguration.InAppPrefixes = []string{"github.com/Sirupsen/logrus"}
hook.StacktraceConfiguration.Context = 2
hook.StacktraceConfiguration.Skip = 2

logger.Error(message)
packet = <-pch
stacktraceSize = len(packet.Stacktrace.Frames)
if stacktraceSize == 0 {
t.Error("Stacktrace should not be empty")
}
lastFrame = packet.Stacktrace.Frames[stacktraceSize-1]
expectedFilename := "github.com/Sirupsen/logrus/entry.go"
if lastFrame.Filename != expectedFilename {
t.Errorf("File name should have been %s, was %s", expectedFilename, lastFrame.Filename)
}
if !lastFrame.InApp {
t.Error("Frame should be identified as in_app")
}
})
}

0 comments on commit e42a087

Please sign in to comment.