Skip to content

Commit

Permalink
Add Sentry Report
Browse files Browse the repository at this point in the history
  • Loading branch information
Neoxelox committed Jan 8, 2024
1 parent 379c575 commit 73a1709
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 40 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@

**`Highly opinionated Go errors package.`**

![Sample output](sample.png "Sample output")
## Console Output

![Console output](console.png "Console output")

## Sentry Output

![Sentry output](sentry.png "Sentry output")

## New Relic Output

TODO

## Install

`go get github.com/neoxelox/errors`

## Usage

Check and run the example test file [errors_test](errors_test.go): `go test -v`.
Check and run the example test file [errors_test](errors_test.go): `go test -v --run TestPrint`.

See [`GoDev`](https://pkg.go.dev/github.com/neoxelox/errors) for further documentation.

Expand Down
Binary file added console.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
181 changes: 148 additions & 33 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,35 @@ package errors

import (
"fmt"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"

"github.com/getsentry/sentry-go"
)

const _MAX_FRAMES = 100

var _ANSI_COLOR_PATTERN = regexp.MustCompile(`\x1b\[[0-9;]*m`)

type frame struct {
file string
line int
function string
}

// Error represent an error with traceback.
// Error represents an error with traceback and additional info.
type Error struct {
kind string
module string
message string
cause error
extra map[string]any
stackTrace []frame
captureStackTrace bool
tags map[string]string
}

// New creates a new Error with a message (can have a format) and
Expand All @@ -33,13 +42,28 @@ func New(message string, captureStackTrace ...bool) Error {
_captureStackTrace = captureStackTrace[0]
}

module := "unknown"
stackFrames := make([]uintptr, 1)

length := runtime.Callers(2, stackFrames)
if length > 0 {
frame, _ := runtime.CallersFrames(stackFrames[:length]).Next()

separator := strings.LastIndex(frame.Function, ".")
if separator >= 0 {
module = frame.Function[:separator]
}
}

return Error{
kind: message,
module: module,
message: message,
cause: nil,
extra: nil,
stackTrace: nil,
captureStackTrace: _captureStackTrace,
tags: nil,
}
}

Expand Down Expand Up @@ -72,11 +96,14 @@ func (self Error) Raise(args ...any) *Error {
}

return &Error{
kind: self.kind,
message: fmt.Sprintf(self.message, args...),
cause: nil,
extra: make(map[string]any),
stackTrace: stackTrace,
kind: self.kind,
module: self.module,
message: fmt.Sprintf(self.message, args...),
cause: nil,
extra: make(map[string]any),
stackTrace: stackTrace,
captureStackTrace: self.captureStackTrace,
tags: make(map[string]string),
}
}

Expand Down Expand Up @@ -112,6 +139,16 @@ func (self *Error) Cause(err error) *Error {
return self
}

// Tags adds tags to the raised Error to further classify
// errors in services such as Sentry or New Relic.
func (self *Error) Tags(tags map[string]any) *Error {
for key, value := range tags {
self.tags[key] = fmt.Sprintf("%v", value)
}

return self
}

// Is compares whether an error is Error's type.
func (self Error) Is(err error) bool {
if err == nil {
Expand All @@ -120,9 +157,29 @@ func (self Error) Is(err error) bool {

switch other := err.(type) {
case Error:
return self.kind == other.kind
return self.kind == other.kind && self.module == other.module
case *Error:
return self.kind == other.kind
return self.kind == other.kind && self.module == other.module
}

return false
}

// Has checks whether an error is wrapped inside the Error itself.
func (self Error) Has(err error) bool {
if self.Is(err) {
return true
}

if self.cause != nil {
switch cause := self.cause.(type) {
case Error:
return cause.Has(err)
case *Error:
return cause.Has(err)
default:
return err == cause || err.Error() == cause.Error()
}
}

return false
Expand Down Expand Up @@ -153,11 +210,27 @@ func (self Error) MarshalJSON() ([]byte, error) {
return []byte("\"" + self.String() + "\""), nil
}

func (self Error) report(all bool, seenTraces map[string]bool) string {
if seenTraces == nil {
seenTraces = make(map[string]bool)
// Format implements the Formatter interface:
// - %s: Error message
// - %v: First error report
// - %+v: All errors reports
// - default: Error message
func (self Error) Format(format fmt.State, verb rune) {
switch verb {
case 's':
format.Write([]byte(self.String()))
case 'v':
if format.Flag('+') {
format.Write([]byte(self.StringReport()))
} else {
format.Write([]byte(self.StringReport(false)))
}
default:
format.Write([]byte(self.String()))
}
}

func (self Error) stringReport(all bool, seenTraces map[string]bool) string {
report := ""

if len(self.stackTrace) > 0 {
Expand Down Expand Up @@ -194,50 +267,92 @@ func (self Error) report(all bool, seenTraces map[string]bool) string {
report += "\nCaused by the following error:\n"
switch cause := self.cause.(type) {
case Error:
report += cause.report(all, seenTraces)
report += cause.stringReport(all, seenTraces)
case *Error:
report += cause.report(all, seenTraces)
report += cause.stringReport(all, seenTraces)
default:
report += " (Stack trace not available)\n"
report += "\x1b[0;31m" + cause.Error() + "\x1b[0m\n"
report += "\x1b[0;31m" + cause.Error() + "\x1b[0m (" +
strings.TrimPrefix(reflect.TypeOf(cause).String(), "*") + ")\n"
}
}

return report
}

// Report returns a string containing all the information about the first
// StringReport returns a string containing all the information about the first
// error (including the message, stack trace, extra...) or about all errors
// wrapped within the Error itself (default is all).
func (self Error) Report(all ...bool) string {
func (self Error) StringReport(all ...bool) string {
_all := true
if len(all) > 0 {
_all = all[0]
}

seenTraces := make(map[string]bool)

report := "\x1b[1;91m" + self.String() + "\x1b[0m\n\n"
report += "Traceback (most recent call last):\n"
report += self.report(_all, nil)
report += self.stringReport(_all, seenTraces)

return report
}

// Format implements the Formatter interface:
// - %s: Error message
// - %v: First error report
// - %+v: All errors reports
// - default: Error message
func (self Error) Format(format fmt.State, verb rune) {
switch verb {
case 's':
format.Write([]byte(self.String()))
case 'v':
if format.Flag('+') {
format.Write([]byte(self.Report()))
} else {
format.Write([]byte(self.Report(false)))
func (self Error) sentryReport(report *sentry.Event) {
if self.cause != nil {
switch cause := self.cause.(type) {
case Error:
cause.sentryReport(report)
case *Error:
cause.sentryReport(report)
default:
report.Exception = append(report.Exception, sentry.Exception{
Type: strings.TrimPrefix(reflect.TypeOf(cause).String(), "*"),
Value: cause.Error(),
})
}
default:
format.Write([]byte(self.String()))
}

for key, value := range self.extra {
report.Extra[key] = value
}

for key, value := range self.tags {
report.Tags[key] = value
}

var stackTrace *sentry.Stacktrace
if len(self.stackTrace) > 0 {
stackTrace = &sentry.Stacktrace{
Frames: make([]sentry.Frame, 0, len(self.stackTrace)),
}

for i := len(self.stackTrace) - 1; i >= 0; i-- {
stackTrace.Frames = append(stackTrace.Frames, sentry.NewFrame(runtime.Frame{
Function: self.stackTrace[i].function,
File: self.stackTrace[i].file,
Line: self.stackTrace[i].line,
}))
}
}

report.Exception = append(report.Exception, sentry.Exception{
Type: self.kind,
Value: self.String(),
Module: self.module,
Stacktrace: stackTrace,
})
}

// SentryReport returns a Sentry Event containing all the information about the
// first error and all errors wrapped within itself (including the types, packages
// messages, stack traces, extra, tags...).
func (self Error) SentryReport() *sentry.Event {
report := sentry.NewEvent()
report.Level = sentry.LevelError
report.Message = _ANSI_COLOR_PATTERN.ReplaceAllString(self.StringReport(), "")
report.Tags["package"] = self.module
self.sentryReport(report)

return report
}
Loading

0 comments on commit 73a1709

Please sign in to comment.