Skip to content

Commit

Permalink
Release v1.9.0
Browse files Browse the repository at this point in the history
  • Loading branch information
kattrali committed Jan 5, 2021
2 parents 946ab52 + 3ee4dda commit 737ff3f
Show file tree
Hide file tree
Showing 16 changed files with 188 additions and 37 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog

## 1.9.0 (2021-01-05)

### Enhancements

* Support capturing "fatal error"-style panics from go, such as from concurrent
map read/writes, out of memory errors, and nil goroutines.

## 1.8.0 (2020-12-03)

### Enhancements
Expand Down
2 changes: 1 addition & 1 deletion bugsnag.go
Expand Up @@ -21,7 +21,7 @@ import (
)

// VERSION defines the version of this Bugsnag notifier
const VERSION = "1.8.0"
const VERSION = "1.9.0"

var panicHandlerOnce sync.Once
var sessionTrackerOnce sync.Once
Expand Down
4 changes: 2 additions & 2 deletions errors/error.go
Expand Up @@ -154,8 +154,8 @@ func (err *Error) StackFrames() []StackFrame {

// TypeName returns the type this error. e.g. *errors.stringError.
func (err *Error) TypeName() string {
if _, ok := err.Err.(uncaughtPanic); ok {
return "panic"
if p, ok := err.Err.(uncaughtPanic); ok {
return p.typeName
}
if name := reflect.TypeOf(err.Err).String(); len(name) > 0 {
return name
Expand Down
22 changes: 16 additions & 6 deletions errors/parse_panic.go
Expand Up @@ -5,7 +5,10 @@ import (
"strings"
)

type uncaughtPanic struct{ message string }
type uncaughtPanic struct {
typeName string
message string
}

func (p uncaughtPanic) Error() string {
return p.message
Expand All @@ -15,20 +18,27 @@ func (p uncaughtPanic) Error() string {
// that panicked. This is particularly useful with https://github.com/mitchellh/panicwrap.
func ParsePanic(text string) (*Error, error) {
lines := strings.Split(text, "\n")
prefixes := []string{"panic:", "fatal error:"}

state := "start"

var message string
var typeName string
var stack []StackFrame

for i := 0; i < len(lines); i++ {
line := lines[i]

if state == "start" {
if strings.HasPrefix(line, "panic: ") {
message = strings.TrimPrefix(line, "panic: ")
state = "seek"
} else {
for _, prefix := range prefixes {
if strings.HasPrefix(line, prefix) {
message = strings.TrimSpace(strings.TrimPrefix(line, prefix))
typeName = prefix[:len(prefix) - 1]
state = "seek"
break
}
}
if state == "start" {
return nil, Errorf("bugsnag.panicParser: Invalid line (no prefix): %s", line)
}

Expand Down Expand Up @@ -68,7 +78,7 @@ func ParsePanic(text string) (*Error, error) {
}

if state == "done" || state == "parsing" {
return &Error{Err: uncaughtPanic{message}, frames: stack}, nil
return &Error{Err: uncaughtPanic{typeName, message}, frames: stack}, nil
}
return nil, Errorf("could not parse panic: %v", text)
}
Expand Down
63 changes: 63 additions & 0 deletions errors/parse_panic_test.go
Expand Up @@ -140,3 +140,66 @@ func TestParsePanic(t *testing.T) {
}
}
}

var concurrentMapReadWrite = `fatal error: concurrent map read and map write
goroutine 1 [running]:
runtime.throw(0x10766f5, 0x21)
/usr/local/Cellar/go/1.15.5/libexec/src/runtime/panic.go:1116 +0x72 fp=0xc00003a6c8 sp=0xc00003a698 pc=0x102d592
runtime.mapaccess1_faststr(0x1066fc0, 0xc000060000, 0x10732e0, 0x1, 0xc000100088)
/usr/local/Cellar/go/1.15.5/libexec/src/runtime/map_faststr.go:21 +0x465 fp=0xc00003a738 sp=0xc00003a6c8 pc=0x100e9c5
main.concurrentWrite()
/myapps/go/fatalerror/main.go:14 +0x7a fp=0xc00003a778 sp=0xc00003a738 pc=0x105d83a
main.main()
/myapps/go/fatalerror/main.go:41 +0x25 fp=0xc00003a788 sp=0xc00003a778 pc=0x105d885
runtime.main()
/usr/local/Cellar/go/1.15.5/libexec/src/runtime/proc.go:204 +0x209 fp=0xc00003a7e0 sp=0xc00003a788 pc=0x102fd49
runtime.goexit()
/usr/local/Cellar/go/1.15.5/libexec/src/runtime/asm_amd64.s:1374 +0x1 fp=0xc00003a7e8 sp=0xc00003a7e0 pc=0x105a4a1
goroutine 5 [runnable]:
main.concurrentWrite.func1(0xc000060000)
/myapps/go/fatalerror/main.go:10 +0x4c
created by main.concurrentWrite
/myapps/go/fatalerror/main.go:8 +0x4b
`

func TestParseFatalError(t *testing.T) {

Err, err := ParsePanic(concurrentMapReadWrite)

if err != nil {
t.Fatal(err)
}

if Err.TypeName() != "fatal error" {
t.Errorf("Wrong type: %s", Err.TypeName())
}

if Err.Error() != "concurrent map read and map write" {
t.Errorf("Wrong message: '%s'", Err.Error())
}

if Err.StackFrames()[0].Func() != nil {
t.Errorf("Somehow managed to find a func...")
}

var result = []StackFrame{
StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/panic.go", LineNumber: 1116, Name: "throw", Package: "runtime"},
StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/map_faststr.go", LineNumber: 21, Name: "mapaccess1_faststr", Package: "runtime"},
StackFrame{File: "/myapps/go/fatalerror/main.go", LineNumber: 14, Name: "concurrentWrite", Package: "main"},
StackFrame{File: "/myapps/go/fatalerror/main.go", LineNumber: 41, Name: "main", Package: "main"},
StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/proc.go", LineNumber: 204, Name: "main", Package: "runtime"},
StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/asm_amd64.s", LineNumber: 1374, Name: "goexit", Package: "runtime"},
}

if !reflect.DeepEqual(Err.StackFrames(), result) {
t.Errorf("Wrong stack for concurrent write fatal error:")
for i, frame := range result {
t.Logf("[%d] %#v", i, frame)
if len(Err.StackFrames()) > i {
t.Logf(" %#v", Err.StackFrames()[i])
}
}
}
}
7 changes: 4 additions & 3 deletions features/fixtures/app/Dockerfile
@@ -1,9 +1,7 @@
ARG GO_VERSION
FROM golang:${GO_VERSION}-alpine

RUN apk update && \
apk upgrade && \
apk add git
RUN apk update && apk upgrade && apk add git bash

ENV GOPATH /app

Expand All @@ -15,3 +13,6 @@ RUN go get . ./sessions ./headers ./errors
# Copy test scenarios
COPY ./app /app/src/test
WORKDIR /app/src/test

RUN chmod +x run.sh
CMD ["/app/src/test/run.sh"]
22 changes: 11 additions & 11 deletions features/fixtures/app/main.go
Expand Up @@ -45,11 +45,11 @@ func configureBasicBugsnag(testcase string) {
}

switch testcase {
case "endpoint legacy":
case "endpoint-legacy":
config.Endpoint = os.Getenv("BUGSNAG_ENDPOINT")
case "endpoint notify":
case "endpoint-notify":
config.Endpoints = bugsnag.Endpoints{Notify: os.Getenv("BUGSNAG_ENDPOINT")}
case "endpoint session":
case "endpoint-session":
config.Endpoints = bugsnag.Endpoints{Sessions: os.Getenv("BUGSNAG_ENDPOINT")}
default:
config.Endpoints = bugsnag.Endpoints{
Expand All @@ -75,9 +75,9 @@ func main() {
switch *test {
case "unhandled":
unhandledCrash()
case "handled", "endpoint legacy", "endpoint notify", "endpoint session":
case "handled", "endpoint-legacy", "endpoint-notify", "endpoint-session":
handledError()
case "handled with callback":
case "handled-with-callback":
handledCallbackError()
case "session":
session()
Expand All @@ -91,19 +91,19 @@ func main() {
filtered()
case "recover":
dontDie()
case "session and error":
case "session-and-error":
sessionAndError()
case "send and exit":
case "send-and-exit":
sendAndExit()
case "user":
user()
case "multiple handled":
case "multiple-handled":
multipleHandled()
case "multiple unhandled":
case "multiple-unhandled":
multipleUnhandled()
case "make unhandled with callback":
case "make-unhandled-with-callback":
handledToUnhandled()
case "nested error":
case "nested-error":
nestedHandledError()
default:
log.Println("Not a valid test flag: " + *test)
Expand Down
25 changes: 25 additions & 0 deletions features/fixtures/app/run.sh
@@ -0,0 +1,25 @@
#!/usr/bin/env bash

# SIGTERM or SIGINT trapped (likely SIGTERM from docker), pass it onto app
# process
function _term_or_init {
kill -TERM "$APP_PID" 2>/dev/null
wait $APP_PID
}

# The bugsnag notifier monitor process needs at least 300ms, in order to ensure
# that it can send its notify
function _exit {
sleep 1
}

trap _term_or_init SIGTERM SIGINT
trap _exit EXIT

PROC="${@:1}"
$PROC &

# Wait on the app process to ensure that this script is able to trap the SIGTERM
# signal
APP_PID=$!
wait $APP_PID
6 changes: 3 additions & 3 deletions features/plain_features/endpoint.feature
Expand Up @@ -6,16 +6,16 @@ Background:
And I have built the service "app"

Scenario: An error report is sent successfully using the legacy endpoint
When I run the go service "app" with the test case "endpoint legacy"
When I run the go service "app" with the test case "endpoint-legacy"
Then I wait to receive a request
And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"

Scenario: An error report is sent successfully using the notify endpoint only
When I run the go service "app" with the test case "endpoint notify"
When I run the go service "app" with the test case "endpoint-notify"
Then I wait to receive a request
And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"

Scenario: Configuring Bugsnag will panic if the sessions endpoint is configured without the notify endpoint
When I run the go service "app" with the test case "endpoint session"
When I run the go service "app" with the test case "endpoint-session"
And I wait for 3 second
Then I should receive no requests
6 changes: 3 additions & 3 deletions features/plain_features/handled.feature
Expand Up @@ -28,7 +28,7 @@ Scenario: A handled error sends a report with a custom name
And the "file" of stack frame 0 equals "main.go"

Scenario: Sending an event using a callback to modify report contents
When I run the go service "app" with the test case "handled with callback"
When I run the go service "app" with the test case "handled-with-callback"
Then I wait to receive a request
And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
And the event "unhandled" is false
Expand All @@ -41,7 +41,7 @@ Scenario: Sending an event using a callback to modify report contents
And the "lineNumber" of stack frame 1 equals 0

Scenario: Marking an error as unhandled in a callback
When I run the go service "app" with the test case "make unhandled with callback"
When I run the go service "app" with the test case "make-unhandled-with-callback"
Then I wait to receive a request
And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
And the event "unhandled" is true
Expand All @@ -52,7 +52,7 @@ Scenario: Marking an error as unhandled in a callback
And stack frame 0 contains a local function spanning 255 to 258

Scenario: Unwrapping the causes of a handled error
When I run the go service "app" with the test case "nested error"
When I run the go service "app" with the test case "nested-error"
Then I wait to receive a request
And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
And the event "unhandled" is false
Expand Down
4 changes: 2 additions & 2 deletions features/plain_features/multieventsession.feature
Expand Up @@ -7,15 +7,15 @@ Background:
And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false"

Scenario: Handled errors know about previous reported handled errors
When I run the go service "app" with the test case "multiple handled"
When I run the go service "app" with the test case "multiple-handled"
And I wait to receive 2 requests
And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
And the event handled sessions count equals 1 for request 0
And the event handled sessions count equals 2 for request 1

Scenario: Unhandled errors know about previous reported handled errors
When I run the go service "app" with the test case "multiple unhandled"
When I run the go service "app" with the test case "multiple-unhandled"
And I wait to receive 2 requests
And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
Expand Down
23 changes: 23 additions & 0 deletions features/plain_features/panics.feature
@@ -0,0 +1,23 @@
Feature: Panic handling

Background:
Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa"
And I configure the bugsnag endpoint
And I have built the service "app"
And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false"

Scenario: Capturing a panic
When I run the go service "app" with the test case "unhandled"
Then I wait to receive a request
And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
And the event "unhandled" is true
And the event "severity" equals "error"
And the event "severityReason.type" equals "unhandledPanic"
And the exception "errorClass" equals "panic"
And the exception "message" is one of:
| interface conversion: interface is struct {}, not string |
| interface conversion: interface {} is struct {}, not string |
And the in-project frames of the stacktrace are:
| file | method |
| main.go | unhandledCrash.func1 |
| main.go | unhandledCrash |
2 changes: 1 addition & 1 deletion features/plain_features/sessioncontext.feature
Expand Up @@ -6,7 +6,7 @@ Background:
And I have built the service "app"

Scenario: An error report contains a session count when part of a session
When I run the go service "app" with the test case "session and error"
When I run the go service "app" with the test case "session-and-error"
Then I wait to receive 2 requests after the start up session
And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
And the request 1 is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
Expand Down
4 changes: 2 additions & 2 deletions features/plain_features/synchronous.feature
Expand Up @@ -8,13 +8,13 @@ Background:

Scenario: An error report is sent asynchrously but exits immediately so is not sent
Given I set environment variable "SYNCHRONOUS" to "false"
When I run the go service "app" with the test case "send and exit"
When I run the go service "app" with the test case "send-and-exit"
And I wait for 3 second
Then I should receive no requests

Scenario: An error report is report synchronously so it will send before exiting
Given I set environment variable "SYNCHRONOUS" to "true"
When I run the go service "app" with the test case "send and exit"
When I run the go service "app" with the test case "send-and-exit"
Then I wait to receive 1 requests
And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"

0 comments on commit 737ff3f

Please sign in to comment.