• Stars
    star
    384
  • Rank 111,726 (Top 3 %)
  • Language
    Go
  • License
    MIT License
  • Created over 1 year ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

🔥 Error handling library with context, assertion, stack trace and source fragments

Oops - Error handling with context, assertion, stack trace and source fragments

tag Go Version GoDoc Build Status Go report Coverage Contributors License

(Yet another) error handling library: oops.OopsError is a dead-simple drop-in replacement for built-in error, adding contextual information such as stack trace, extra attributes, error code, and bug-fixing hints...

⚠️ This is NOT a logging library. oops should be used as a complement to your existing logging toolchain (zap, zerolog, logrus, slog, go-sentry...).

🥷 Start hacking oops with this playground.

logo: thanks Gimp

Jump:

🤔 Motivations

Loggers usually allow developers to build records with contextual attributes, that describe errors, such as:

  • zap.Infow("failed to fetch URL", "url", url)
  • logrus.WithFields("url", url).Error("failed to fetch URL")).

Go recommends cascading error handling, which can cause the error to be triggered far away from the call to the logger. Returning context over X callers is painful, and to be meaningful, the stack trace must be gathered by the error builder instead of the logger.

This is why we need an error wrapper!

🥵 Why develop yet another library?

  • drop-in replacement to error
  • easy to integrate without large refactoring
  • separation of concern (logger vs error)
  • extra attributes
  • developer-friendly error builder
  • no extra code for output: can be used with loggers, printf syntax...
  • out-of-the-box stack trace and source fragments
  • one-line panic handling
  • one-line assertion

Why "oops"?

Have you already heard a developer yelling at unclear error messages in Sentry, with no context, just before figuring out he wrote this piece of shit by himself?

Yes. Me too.

oops!

Example

func d() error {
    return oops.
        Code("iam_missing_permission").
        In("authz").
        Tags("authz").
        Time(time.Now()).
        With("user_id", 1234).
        With("permission", "post.create").
        Hint("Runbook: https://doc.acme.org/doc/abcd.md").
        User("user-123", "firstname", "john", "lastname", "doe").
        Errorf("permission denied")
}

func c() error {
	return d()
}

func b() error {
    // add more context
    return oops.
        In("iam").
        Tags("iam").
        Trace("e76031ee-a0c4-4a80-88cb-17086fdd19c0").
        With("hello", "world").
        Wrapf(c(), "something failed")
}

func a() error {
	return b()
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    err := a()
    if err != nil {
        logger.Error(
            err.Error(),
            slog.Any("error", err), // unwraps and flattens error context
        )
    }
}
Why 'oops'?

🚀 Install

go get github.com/samber/oops

This library is v1 and follows SemVer strictly.

No breaking changes will be made to APIs before v2.0.0.

This library has no dependencies outside the Go standard library.

💡 Quick start

This library provides a simple error builder for composing structured errors, with contextual attributes and stack trace.

Since oops.OopsError implements the error interface, you will be able to compose and wrap native errors with oops.OopsError.

🥷 Start hacking oops with this playground.

🧠 Spec

GoDoc: https://godoc.org/github.com/samber/oops

Error constructors

Constructor Description
.Errorf(format string, args ...any) error Formats an error and returns oops.OopsError object that satisfies error
.Wrap(err error) error Wraps an error into an oops.OopsError object that satisfies error
.Wrapf(err error, format string, args ...any) error Wraps an error into an oops.OopsError object that satisfies error and formats an error message
.Recover(cb func()) error Handle panic and returns oops.OopsError object that satisfies error.
.Recoverf(cb func(), format string, args ...any) error Handle panic and returns oops.OopsError object that satisfies error and formats an error message.
.Assert(condition bool) OopsErrorBuilder Panics if condition is false. Assertions can be chained.
.Assertf(condition bool, format string, args ...any) OopsErrorBuilder Panics if condition is false and formats an error message. Assertions can be chained.

Examples

// with error wrapping
err0 := oops.
    In("repository").
    Tags("database", "sql").
    Wrapf(sql.Exec(query), "could not fetch user")  // Wrapf returns nil when sql.Exec() is nil

// with panic recovery
err1 := oops.
    In("repository").
    Tags("database", "sql").
    Recover(func () {
        panic("caramba!")
    })

// with assertion
err2 := oops.
    In("repository").
    Tags("database", "sql").
    Recover(func () {
        // ...
        oops.Assertf(time.Now().Weekday() == 1, "This code should run on Monday only.")
        // ...
    })

Context

The library provides an error builder. Each method can be used standalone (eg: oops.With(...)) or from a previous builder instance (eg: oops.In("iam").User("user-42")).

The oops.OopsError builder must finish with either .Errorf(...), .Wrap(...) or .Wrapf(...).

Builder method Getter Description
.With(string, any) err.Context() map[string]any Supply a list of attributes key+value. Values of type func() any {} are accepted and evaluated lazily.
.WithContext(context.Context, ...any) err.Context() map[string]any Supply a list of values declared in context. Values of type func() any {} are accepted and evaluated lazily.
.Code(string) err.Code() string Set a code or slug that describes the error. Error messages are intented to be read by humans, but such code is expected to be read by machines and be transported over different services
.Time(time.Time) err.Time() time.Time Set the error time (default: time.Now())
.Since(time.Time) err.Duration() time.Duration Set the error duration
.Duration(time.Duration) err.Duration() time.Duration Set the error duration
.In(string) err.Domain() string Set the feature category or domain
.Tags(...string) err.Tags() []string Add multiple tags, describing the feature returning an error
.Trace(string) err.Trace() string Add a transaction id, trace id, correlation id... (default: ULID)
.Span(string) err.Span() string Add a span representing a unit of work or operation... (default: ULID)
.Hint(string) err.Hint() string Set a hint for faster debugging
.Owner(string) err.Owner() (string) Set the name/email of the collegue/team responsible for handling this error. Useful for alerting purpose
.User(string, any...) err.User() (string, map[string]any) Supply user id and a chain of key/value
.Tenant(string, any...) err.Tenant() (string, map[string]any) Supply tenant id and a chain of key/value
.Request(*http.Request, bool) err.Request() *http.Request Supply http request
.Response(*http.Response, bool) err.Response() *http.Response Supply http response

Examples

// simple error with stacktrace
err1 := oops.Errorf("could not fetch user")

// with optional domain
err2 := oops.
    In("repository").
    Tags("database", "sql").
    Errorf("could not fetch user")

// with custom attributes
ctx := context.WithContext(context.Background(), "a key", "value")
err3 := oops.
    With("driver", "postgresql").
    With("query", query).
    With("query.duration", queryDuration).
    With("lorem", func() string { return "ipsum" }).	// lazy evaluation
    WithContext(ctx, "a key", "another key").
    Errorf("could not fetch user")

// with trace+span
err4 := oops.
    Trace(traceID).
    Span(spanID).
    Errorf("could not fetch user")

// with hint and ownership, for helping developer to solve the issue
err5 := oops.
    Hint("The user could have been removed. Please check deleted_at column.").
    Owner("Slack: #api-gateway").
    Errorf("could not fetch user")

// with optional userID
err6 := oops.
    User(userID).
    Errorf("could not fetch user")

// with optional user data
err7 := oops.
    User(userID, "firstname", "Samuel").
    Errorf("could not fetch user")

// with optional user and tenant
err8 := oops.
    User(userID, "firstname", "Samuel").
    Tenant(workspaceID, "name", "my little project").
    Errorf("could not fetch user")

// with optional http request and response
err9 := oops.
    Request(req, false).
    Response(res, true).
    Errorf("could not fetch user")

Other helpers

  • oops.AsError[MyError](error) (MyError, bool) as an alias to errors.As(...)

Stack trace

This library provides a pretty printed stack trace for each generated error.

The stack trace max depth can be set using:

// default: 10
oops.StackTraceMaxDepth = 42

The stack trace will be printed this way:

err := oops.Errorf("permission denied")

fmt.Println(err.(oops.OopsError).Stacktrace())
Stacktrace

Wrapped errors will be reported as an annotated stack trace:

err1 := oops.Errorf("permission denied")
// ...
err2 := oops.Wrapf(err, "something failed")

fmt.Println(err2.(oops.OopsError).Stacktrace())
Stacktrace

Source fragments

The exact error location can be provided in a Go file extract.

Source fragments are hidden by default. You must run oops.SourceFragmentsHidden = false to enable this feature. Go source files being read at run time, you have to keep the source code at the same location.

In a future release, this library is expected to output a colorized extract. Please contribute!

oops.SourceFragmentsHidden = false

err1 := oops.Errorf("permission denied")
// ...
err2 := oops.Wrapf(err, "something failed")

fmt.Println(err2.(oops.OopsError).Sources())
Sources

Panic handling

oops library is delivered with a try/catch -ish error handler. 2 handlers variants are available: oops.Recover() and oops.Recoverf(). Both can be used in the oops error builder with usual methods.

🥷 Start hacking oops.Recover() with this playground.

func mayPanic() {
	panic("permission denied")
}

func handlePanic() error {
    return oops.
        Code("iam_authz_missing_permission").
        In("authz").
        With("permission", "post.create").
        Trace("6710668a-2b2a-4de6-b8cf-3272a476a1c9").
        Hint("Runbook: https://doc.acme.org/doc/abcd.md").
        Recoverf(func() {
            // ...
            mayPanic()
            // ...
        }, "unexpected error %d", 42)
}

Assertions

Assertions may be considered an anti-pattern for Golang since we only call panic() for unexpected and critical errors. In this situation, assertions might help developers to write safer code.

func mayPanic() {
    x := 42

    oops.
        Trace("6710668a-2b2a-4de6-b8cf-3272a476a1c9").
        Hint("Runbook: https://doc.acme.org/doc/abcd.md").
        Assertf(time.Now().Weekday() == 1, "This code should run on Monday only.").
        With("x", x).
        Assertf(x == 42, "expected x to be equal to 42, but got %d", x)

    oops.Assert(re.Match(email))

    // ...
}

func handlePanic() error {
    return oops.
        Code("iam_authz_missing_permission").
        In("authz").
        Recover(func() {
            // ...
            mayPanic()
            // ...
        })
}

Output

Errors can be printed in many ways. Logger formatters provided in this library use these methods.

Errorf %w

str := fmt.Errorf("something failed: %w", oops.Errorf("permission denied"))

fmt.Println(err.Error())
// Output:
// something failed: permission denied

printf %v

err := oops.Errorf("permission denied")

fmt.Printf("%v", err)
// Output:
// permission denied

printf %+v

err := oops.Errorf("permission denied")

fmt.Printf("%+v", err)
Output

JSON Marshal

b := json.MarshalIndent(err, "", "  ")
Output

slog.Valuer

err := oops.Errorf("permission denied")

attr := slog.Error(err.Error(),
    slog.Any("error", err))

// Output:
// slog.Group("error", ...)

📫 Loggers

Some loggers may need a custom formatter to extract attributes from oops.OopsError.

Available loggers:

We are looking for contributions and examples for:

  • zap
  • go-sentry
  • other?

Examples of formatters can be found in ToMap(), Format(), Marshal() and LogValuer methods of oops.OopsError.

🥷 Tips and best practices

Wrap/Wrapf shortcut

oops.Wrap(...) and oops.Wrapf(...) returns nil if the provided error is nil.

❌ So don't write:

err := mayFail()
if err != nil {
    return oops.Wrapf(err, ...)
}

return nil

✅ but write:

return oops.Wrapf(mayFail(), ...)

Reuse error builder

Writing a full contextualized error can be painful and very repetitive. But a single context can be used for multiple errors in a single function:

❌ So don't write:

err := mayFail1()
if err != nil {
    return oops.
        In("iam").
        Trace("77cb6664").
        With("hello", "world").
        Wrap(err)
}

err = mayFail2()
if err != nil {
    return oops.
        In("iam").
        Trace("77cb6664").
        With("hello", "world").
        Wrap(err)
}

return oops.
    In("iam").
    Trace("77cb6664").
    With("hello", "world").
    Wrap(mayFail3())

✅ but write:

errorBuilder := oops.
    In("iam").
    Trace("77cb6664").
    With("hello", "world")

err := mayFail1()
if err != nil {
    return errorBuilder.Wrap(err)
}

err = mayFail2()
if err != nil {
    return errorBuilder.Wrap(err)
}

return errorBuilder.Wrap(mayFail3())

Caller/callee attributes

Also, think about feeding error context in every caller, instead of adding extra information at the last moment.

❌ So don't write:

func a() error {
    return b()
}

func b() error {
    return c()
}

func c() error {
    return d()
}

func d() error {
    return oops.
        Code("iam_missing_permission").
        In("authz").
        Trace("4ea76885-a371-46b0-8ce0-b72b277fa9af").
        Time(time.Now()).
        With("hello", "world").
        With("permission", "post.create").
        Hint("Runbook: https://doc.acme.org/doc/abcd.md").
        User("user-123", "firstname", "john", "lastname", "doe").
        Tenant("organization-123", "name", "Microsoft").
        Errorf("permission denied")
}

✅ but write:

func a() error {
	return b()
}

func b() error {
    return oops.
        In("iam").
        Trace("4ea76885-a371-46b0-8ce0-b72b277fa9af").
        With("hello", "world").
        Wrapf(c(), "something failed")
}

func c() error {
    return d()
}

func d() error {
    return oops.
        Code("iam_missing_permission").
        In("authz").
        Time(time.Now()).
        With("permission", "post.create").
        Hint("Runbook: https://doc.acme.org/doc/abcd.md").
        User("user-123", "firstname", "john", "lastname", "doe").
        Tenant("organization-123", "name", "Microsoft").
        Errorf("permission denied")
}

🤝 Contributing

Don't hesitate ;)

# Install some dev dependencies
make tools

# Run tests
make test
# or
make watch-test

👤 Contributors

Contributors

💫 Show your support

Give a ⭐️ if this project helped you!

GitHub Sponsors

📝 License

Copyright © 2023 Samuel Berthe.

This project is MIT licensed.

More Repositories

1

lo

💥 A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find...)
Go
17,419
star
2

awesome-prometheus-alerts

🚨 Collection of Prometheus alerting rules
HTML
6,502
star
3

mo

🦄 Monads and popular FP abstractions, powered by Go 1.18+ Generics (Option, Result, Either...)
Go
2,587
star
4

do

⚙️ A dependency injection toolkit based on Go 1.18+ Generics.
Go
1,807
star
5

slog-multi

🚨 Design workflows of slog handlers: pipeline, middleware, fanout, routing, failover, load balancing...
Go
342
star
6

invoice-as-a-service

💰 Simple invoicing service (REST API): from JSON to PDF
PHP
181
star
7

sync-ssh-keys

🔐 Sync public ssh keys to ~/.ssh/authorized_keys, based on Github/Gitlab organization membership.
Go
138
star
8

slog-gin

🚨 Gin middleware for slog logger
Go
109
star
9

chartjs-plugin-datasource-prometheus

📊 Chart.js plugin for Prometheus
TypeScript
107
star
10

slog-formatter

🚨 slog: Attribute formatting
Go
107
star
11

slog-echo

🚨 Echo middleware for slog logger
Go
100
star
12

go-gpt-3-encoder

Go BPE tokenizer (Encoder+Decoder) for GPT2 and GPT3
Go
78
star
13

the-great-gpt-firewall

🤖 A curated list of websites that restrict access to AI Agents, AI crawlers and GPTs
Python
75
star
14

prometheus-query-js

📊 A Javascript client for Prometheus query API
TypeScript
58
star
15

hot

🌶️ In-memory caching library for Go
Go
58
star
16

github-actions-runner

✅ Docker images for starting self-hosted Github Actions runner(s).
Dockerfile
57
star
17

slog-fiber

🚨 Fiber middleware for slog logger
Go
52
star
18

slog-sampling

🚨 slog sampling: drop repetitive log records
Go
49
star
19

slog-chi

🚨 Chi middleware for slog logger
Go
43
star
20

slog-sentry

🚨 slog: Sentry handler
Go
43
star
21

awesome-olap

A curated list of awesome Online Analytical Processing databases, frameworks, ressources and other awesomeness.
37
star
22

grafana-flamegraph-panel

📊 Flame graph panels for Grafana
JavaScript
37
star
23

slog-loki

🚨 slog: Loki handler
Go
36
star
24

slog-http

🚨 net/http middleware for slog logger
Go
32
star
25

slog-zerolog

🚨 slog: Zerolog handler
Go
30
star
26

workshop-prometheus-grafana

📊 Prometheus and Grafana 101
JavaScript
30
star
27

slog-zap

🚨 slog: Zap handler
Go
23
star
28

go-metered-io

📐 A drop-in replacement to io.Reader and io.Writer with the total number of bytes transfered.
Go
23
star
29

go-amqp-pubsub

Fault tolerant Pub/Sub library for RabbitMQ
Go
21
star
30

go-singleflightx

🧬 x/sync/singleflight but with generics, batching, sharding and nullable result
Go
19
star
31

slog-logrus

🚨 slog: Logrus handler
Go
18
star
32

slog-slack

🚨 slog: Slack handler
Go
18
star
33

arp-spoofing

💥 Simple implementation of arp poisoning attack ;)
C
16
star
34

pg_cron

⏰ PostgreSQL extension for running periodic jobs
C
15
star
35

go-tcp-pool

✨ Drop-in replacement to net.Conn with pooling and auto-reconnect
Go
14
star
36

go-type-to-string

🕵️‍♂️ Extract a string representation of Go type
Go
14
star
37

slog-syslog

🚨 slog: Syslog handler
Go
13
star
38

refined-hn

JavaScript
11
star
39

git-contrib-graph

📊 Displays a github-like contribution graph, of every contributors of a repository
Go
10
star
40

slog-nats

🚨 slog: NATS handler
Go
10
star
41

slog-parquet

🚨 slog: Parquet handler + Object Storage
Go
10
star
42

slog-datadog

🚨 slog: Datadog handler
Go
10
star
43

free_proxy_list

Free proxy list [NOT MAINTAINED ANYMORE - please fork]
Shell
9
star
44

slog-otel

OTEL toolchain for slog
Makefile
8
star
45

slog-graylog

🚨 slog: Graylog handler
Go
7
star
46

powEUr

Python
7
star
47

slog-telegram

🚨 slog: Telegram handler
Go
7
star
48

node-promfiler

Expose a http endpoint for exporting node.js v8 profiling
JavaScript
7
star
49

slog-webhook

🚨 slog: Webhook handler
Go
7
star
50

slog-betterstack

🚨 slog: Betterstack handler
Go
7
star
51

slog-common

Common toolchain for slog
Go
6
star
52

slog-kafka

🚨 slog: Kafka handler
Go
6
star
53

slog-channel

🚨 slog: Go channel handler
Go
6
star
54

slog-logstash

🚨 slog: Logstash handler
Go
5
star
55

ansible-role-airbyte

Ansible role for Airbyte
5
star
56

slog-fluentd

🚨 slog: Fluentd handler
Go
4
star
57

remote-dev-environment

👨‍💻 My development environment is too slow, let's fix that !
4
star
58

GoogleCalendarNotifier-FitbitTracker

Google Calendar notifier for Fitbit Tracker
Gosu
4
star
59

slog-mock

🚨 slog: mock handler
Go
4
star
60

slog-quickwit

🚨 slog: Quickwit handler
Go
4
star
61

criterion-rpm-package

RPM package for Criterion (C unit testing)
Shell
3
star
62

dagobert

A simple Go client for the clip-as-service server
Go
3
star
63

rabbitmq-flooding

Cluster recovery testing. Floods RabbitMQ with random data.
Python
3
star
64

go-psi

🥵 Pressure Stall Informations (PSI) and starvation notifier
Go
3
star
65

slog-microsoft-teams

🚨 slog: Microsoft Teams handler
Go
3
star
66

llvm_dart_binding

Binding Dart/LLVM (using LLVM bytecode from Dart)
Dart
3
star
67

lab-langchain-getting-started

Python
2
star
68

BTCC_api

A basic API wrapper for the BTCC Trading and Market FIX API.
JavaScript
2
star
69

ngx-domarrow

Declarative and template-driven DOMArrow integration for Angular2+
TypeScript
2
star
70

github-stackoverflow-email-scrapping

Scrape top Github and Stack-Overflow users to find email address
Go
2
star
71

nft-http-api

🚦 NFT over HTTP API
Go
2
star
72

go-quickwit

🍱 A Go ingestion client for Quickwit
Go
2
star
73

celery_demonstration

Async worker + scheduling
Python
2
star
74

dockerfiles

Dockerfile
1
star
75

SaaS-Cookbook-List

List of Cookbook about SaaS development (ENG/FR)
1
star
76

dotfiles

@samber's dotfiles
JavaScript
1
star
77

grafana-dashboard-nomad

Grafana dashboards for Nomad (Docker orchestrator from Hashicorp)
1
star
78

go-clevercloud-api

Go library for Clever-Cloud api
Go
1
star
79

lab-langchain

Python
1
star
80

slog-mattermost

🚨 slog: Mattermost handler
Go
1
star
81

jitsi-virtual-background

JavaScript
1
star
82

raw-ip-udp-sockets-chap

Simple implementation of CHAP protocol, with raw socket layers (3+4)
C
1
star
83

lab-parquet

Go
1
star
84

canvas-to-bmp

TypeScript
1
star
85

refined-cycle-app

JavaScript
1
star
86

azure-ad-oauth2-proxy

Dockerfile
1
star
87

packer-qemu-debian

Builds Debian 8 image for Qemu
Shell
1
star
88

poc-selenium-unit-test-css

Python
1
star
89

maxscale-experiments

Demonstration step-by-step of MaxScale for master/slave query spliting/routing #mysql #docker
Shell
1
star
90

google-takeout-to-s3

🚨 Simple script to upload encrypted Google Takeout archives to S3.
1
star
91

messenger-bot-clock

Messenger bot replying with current time
JavaScript
1
star
92

fb-messenger-bot-psychologist

🤖 A Messenger bot talking like a psychologist
Emacs Lisp
1
star
93

promql-exporter

Prometheus exporter for PromQL endpoints (replacing federation and remote-write)
Go
1
star