• Stars
    star
    164
  • Rank 221,735 (Top 5 %)
  • Language
    Go
  • License
    MIT License
  • Created 12 months ago
  • Updated about 2 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
15,102
star
2

awesome-prometheus-alerts

🚨 Collection of Prometheus alerting rules
HTML
5,944
star
3

mo

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

do

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

slog-multi

🚨 Design workflows of slog handlers: pipeline, middleware, fanout, routing, failover, load balancing...
Go
234
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
134
star
8

chartjs-plugin-datasource-prometheus

📊 Chart.js plugin for Prometheus
TypeScript
95
star
9

slog-formatter

🚨 slog: Attribute formatting
Go
79
star
10

go-gpt-3-encoder

Go BPE tokenizer (Encoder+Decoder) for GPT2 and GPT3
Go
77
star
11

slog-echo

🚨 Echo middleware for slog logger
Go
72
star
12

slog-gin

🚨 Gin middleware for slog logger
Go
65
star
13

the-great-gpt-firewall

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

prometheus-query-js

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

github-actions-runner

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

grafana-flamegraph-panel

📊 Flame graph panels for Grafana
JavaScript
37
star
17

slog-fiber

🚨 Fiber middleware for slog logger
Go
35
star
18

slog-sampling

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

workshop-prometheus-grafana

📊 Prometheus and Grafana 101
JavaScript
30
star
20

slog-sentry

🚨 slog: Sentry handler
Go
30
star
21

slog-chi

🚨 Chi middleware for slog logger
Go
22
star
22

awesome-olap

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

go-amqp-pubsub

Fault tolerant Pub/Sub library for RabbitMQ
Go
16
star
24

pg_cron

⏰ PostgreSQL extension for running periodic jobs
C
15
star
25

slog-loki

🚨 slog: Loki handler
Go
14
star
26

arp-spoofing

💥 Simple implementation of arp poisoning attack ;)
C
14
star
27

slog-slack

🚨 slog: Slack handler
Go
14
star
28

slog-zap

🚨 slog: Zap handler
Go
12
star
29

slog-zerolog

🚨 slog: Zerolog handler
Go
12
star
30

go-tcp-pool

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

refined-hn

JavaScript
11
star
32

slog-logrus

🚨 slog: Logrus handler
Go
11
star
33

slog-http

🚨 net/http middleware for slog logger
Go
10
star
34

free_proxy_list

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

slog-syslog

🚨 slog: Syslog handler
Go
9
star
36

slog-parquet

🚨 slog: Parquet handler + Object Storage
Go
9
star
37

go-type-to-string

🕵️‍♂️ Extract a string representation of Go type
Go
8
star
38

git-contrib-graph

📊 Displays a github-like contribution graph, of every contributors of a repository
Go
8
star
39

powEUr

Python
7
star
40

node-promfiler

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

slog-datadog

🚨 slog: Datadog handler
Go
7
star
42

slog-channel

🚨 slog: Go channel handler
Go
5
star
43

go-singleflightx

🧬 x/sync/singleflight but with generics, batching and nullable result
Go
5
star
44

slog-nats

🚨 slog: NATS handler
Go
5
star
45

slog-kafka

🚨 slog: Kafka handler
Go
4
star
46

remote-dev-environment

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

GoogleCalendarNotifier-FitbitTracker

Google Calendar notifier for Fitbit Tracker
Gosu
4
star
48

ansible-role-airbyte

Ansible role for Airbyte
4
star
49

criterion-rpm-package

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

dagobert

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

rabbitmq-flooding

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

slog-graylog

🚨 slog: Graylog handler
Go
3
star
53

go-psi

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

slog-telegram

🚨 slog: Telegram handler
Go
3
star
55

hot

🌶️ In-memory caching library for Go
Go
3
star
56

llvm_dart_binding

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

slog-webhook

🚨 slog: Webhook handler
Go
3
star
58

slog-common

Common toolchain for slog
Go
2
star
59

slog-logstash

🚨 slog: Logstash handler
Go
2
star
60

lab-langchain-getting-started

Python
2
star
61

BTCC_api

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

github-stackoverflow-email-scrapping

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

ngx-domarrow

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

celery_demonstration

Async worker + scheduling
Python
2
star
65

go-metered-io

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

dotfiles

@samber's dotfiles
JavaScript
1
star
67

grafana-dashboard-nomad

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

slog-fluentd

🚨 slog: Fluentd handler
Go
1
star
69

go-clevercloud-api

Go library for Clever-Cloud api
Go
1
star
70

lab-langchain

Python
1
star
71

slog-mattermost

🚨 slog: Mattermost handler
Go
1
star
72

dockerfiles

Dockerfile
1
star
73

jitsi-virtual-background

JavaScript
1
star
74

SaaS-Cookbook-List

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

raw-ip-udp-sockets-chap

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

lab-parquet

Go
1
star
77

nft-http-api

🚦 NFT over HTTP API
Go
1
star
78

canvas-to-bmp

TypeScript
1
star
79

refined-cycle-app

JavaScript
1
star
80

azure-ad-oauth2-proxy

Dockerfile
1
star
81

packer-qemu-debian

Builds Debian 8 image for Qemu
Shell
1
star
82

poc-selenium-unit-test-css

Python
1
star
83

maxscale-experiments

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

messenger-bot-clock

Messenger bot replying with current time
JavaScript
1
star
85

hello-world-node-pg-redis

Simple health check with NodeJS + Redis + PostgreSQL
JavaScript
1
star
86

slog-microsoft-teams

🚨 slog: Microsoft Teams handler
Go
1
star
87

fb-messenger-bot-psychologist

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