Skip to content

Commit bb034b3

Browse files
authored
Implement serror a library for structured errors (#441)
## Summary Implement `serror` a library for structured errors ## How was it tested? devbox run test ## Community Contribution License All community contributions in this pull request are licensed to the project maintainers under the terms of the [Apache 2 license](https://www.apache.org/licenses/LICENSE-2.0). By creating this pull request I represent that I have the right to license the contributions to the project maintainers under the Apache 2 license.
1 parent badbdad commit bb034b3

13 files changed

Lines changed: 1458 additions & 0 deletions

File tree

go.work.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
7070
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
7171
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
7272
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
73+
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
74+
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
7375
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
7476
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
7577
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE=

pkg/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
1717
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f
1818
github.com/k0kubun/pp v3.0.1+incompatible
19+
github.com/lmittmann/tint v1.0.7
1920
github.com/mattn/go-isatty v0.0.20
2021
github.com/pelletier/go-toml/v2 v2.2.3
2122
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c

pkg/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
6262
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
6363
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
6464
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
65+
github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y=
66+
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
6567
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
6668
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
6769
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=

pkg/serror/README.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# serror
2+
3+
[![Go Reference](https://pkg.go.dev/badge/github.com/jetify-com/serror.svg)](https://pkg.go.dev/github.com/jetify-com/serror)
4+
[![Go Report Card](https://goreportcard.com/badge/github.com/jetify-com/serror)](https://goreportcard.com/report/github.com/jetify-com/serror)
5+
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
6+
7+
An error handling library for Go that makes it easy to associate arbitrary structured data with errors
8+
in a similar way to what `slog` does for logging.
9+
10+
## Table of Contents
11+
12+
- [Overview](#overview)
13+
- [Features](#features)
14+
- [Installation](#installation)
15+
- [Quick Start](#quick-start)
16+
- [Key Concepts](#key-concepts)
17+
- [Design Philosophy](#design-philosophy)
18+
- [Contributing](#contributing)
19+
- [License](#license)
20+
- [Credits](#credits)
21+
22+
## Overview
23+
`serror` provides a clean API for creating, wrapping, and logging errors with associated structured data.
24+
25+
When handling errors in Go, developers often need to include contextual information to help diagnose what went wrong. The traditional approach using `fmt.Errorf` interpolates this data into a string:
26+
27+
```go
28+
return fmt.Errorf("failed to process user %d: %w", userID, err)
29+
```
30+
31+
While simple, this approach has limitations:
32+
- The contextual data (like `userID`) becomes just part of the error message
33+
- There's no way to programmatically access these values later
34+
- The data loses its type information
35+
- The format becomes rigid and harder to parse
36+
37+
`serror` solves these problems by allowing you to attach structured data to errors:
38+
39+
```go
40+
return serror.Wrap(err, "failed to process user",
41+
"user_id", userID,
42+
"attempt", attempt,
43+
"status", status,
44+
)
45+
```
46+
47+
This approach:
48+
- Preserves the original data types
49+
- Makes values accessible programmatically via `err.Get("user_id")`
50+
- Integrates seamlessly with structured logging
51+
- Maintains the flexibility to add or modify context
52+
53+
Inspired by Go's `log/slog` package, `serror` uses the same familiar key-value pair pattern for attaching attributes. If you've used `slog`, you'll feel right at home:
54+
55+
```go
56+
// slog style
57+
logger.Error("request failed",
58+
"user_id", userID,
59+
"status", status,
60+
)
61+
62+
// serror follows the same pattern
63+
return serror.New("request failed",
64+
"user_id", userID,
65+
"status", status,
66+
)
67+
```
68+
69+
## Features
70+
71+
- **Structured Attributes**: Attach key-value pairs to errors using `slog`-like syntax
72+
- **Error Wrapping**: Fully compatible with Go's error wrapping conventions
73+
- **slog Integration**: Implements `LogValuer` for automatic structured logging
74+
- **JSON Support**: Full JSON marshaling/unmarshaling capabilities
75+
- **Attribute Access**: Get attributes using dot notation (e.g., "user.id")
76+
- **Thread Safety**: Immutable design ensures safe concurrent usage
77+
- **Call Site Capture**: Automatically records where errors are created
78+
79+
## Installation
80+
81+
### Using go get
82+
```bash
83+
go get github.com/jetify-com/serror
84+
```
85+
86+
### From Source
87+
```bash
88+
git clone https://github.com/jetify-com/serror.git
89+
cd serror
90+
go install
91+
```
92+
93+
## Quick Start
94+
95+
```go
96+
// Create a new error with attributes
97+
err := serror.New("failed to process request",
98+
"userID", 123,
99+
"path", "/api/v1/users",
100+
)
101+
102+
// Wrap an existing error with additional context
103+
if err != nil {
104+
return serror.Wrap(err, "user operation failed",
105+
"operation", "create",
106+
"retry", false,
107+
)
108+
}
109+
110+
// Log the error with slog
111+
slog.Error("request failed", "err", err)
112+
```
113+
114+
## Key Concepts
115+
116+
### Creating Errors
117+
118+
```go
119+
// Basic error with attributes
120+
err := serror.New("operation failed",
121+
"code", 500,
122+
"component", "database",
123+
)
124+
125+
// Using attribute constructors
126+
err := serror.New("validation failed",
127+
serror.Int("code", 400),
128+
serror.String("field", "email"),
129+
serror.Bool("valid", false),
130+
)
131+
132+
// Group related attributes
133+
err := serror.New("validation failed",
134+
serror.Group("user",
135+
serror.Int("id", 123),
136+
serror.String("name", "alice"),
137+
),
138+
)
139+
```
140+
141+
### Wrapping Errors
142+
143+
```go
144+
baseErr := serror.New("database error",
145+
"table", "users",
146+
)
147+
148+
err := serror.Wrap(baseErr, "query failed",
149+
"query_id", "abc123",
150+
)
151+
```
152+
153+
### Adding Attributes
154+
155+
```go
156+
// Add more attributes to an existing error
157+
// Note: With() returns a new error - serror errors are immutable
158+
oldErr := serror.New("operation failed", "code", 500)
159+
newErr := oldErr.With("retry", true, "attempt", 3)
160+
161+
// oldErr is unchanged, newErr has all attributes
162+
```
163+
164+
The immutable design ensures thread safety and prevents accidental modifications. Each call to `With()` returns a new error instance with the combined attributes from the original error plus the new ones.
165+
166+
### Accessing Attributes
167+
168+
```go
169+
// Get attribute values using dot notation
170+
value := err.Get("user.id")
171+
name := err.Get("user.name")
172+
```
173+
174+
### JSON Support
175+
176+
```go
177+
// Marshal to JSON
178+
data, err := json.Marshal(serror)
179+
180+
// Unmarshal from JSON
181+
var newErr serror.Error
182+
err := json.Unmarshal(data, &newErr)
183+
```
184+
185+
### Integration with slog
186+
187+
```go
188+
err := serror.New("operation failed",
189+
"user_id", 123,
190+
"status", 500,
191+
)
192+
193+
// serror.Error implements slog.LogValuer
194+
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
195+
logger.Error("request failed", "err", err)
196+
197+
// Output:
198+
// ERROR request failed err.user_id=123 err.status=500 err.msg="operation failed"
199+
```
200+
201+
When logged, all structured attributes are automatically included in the log output, making it easy to correlate errors with their context.
202+
203+
## Design Philosophy
204+
205+
- **Standard Library Aligned**: Works with `errors.Is`, `errors.As`, `errors.Unwrap`
206+
- **slog Compatible**: Follows `slog` patterns for attribute handling
207+
- **Thread Safe**: Immutable design prevents data races
208+
- **Simple API**: Familiar interface for Go developers
209+
210+
## Documentation
211+
212+
For detailed documentation and API references, please visit our [Go Package Documentation](https://pkg.go.dev/github.com/jetify-com/serror).
213+
214+
## Contributing
215+
216+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
217+
218+
Please make sure to update tests as appropriate.
219+
220+
## Community
221+
222+
- **Issues:** [GitHub Issues](https://github.com/jetify-com/serror/issues)
223+
- **Discussions:** [GitHub Discussions](https://github.com/jetify-com/serror/discussions)
224+
225+
## License
226+
227+
This project is licensed under the Apache License, Version 2.0 - see the [LICENSE](LICENSE) file for details.
228+
229+
## Acknowledgments
230+
231+
- Thanks to the Go team for the excellent `log/slog` package design
232+
- Inspired by various error handling patterns in the Go community

pkg/serror/attr.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package serror
2+
3+
import (
4+
"log/slog"
5+
"time"
6+
)
7+
8+
const badKey = "!BADKEY"
9+
10+
// Kind is the kind of a [Value].
11+
type Kind = slog.Kind
12+
13+
const (
14+
KindAny = slog.KindAny
15+
KindBool = slog.KindBool
16+
KindDuration = slog.KindDuration
17+
KindFloat64 = slog.KindFloat64
18+
KindInt64 = slog.KindInt64
19+
KindString = slog.KindString
20+
KindTime = slog.KindTime
21+
KindUint64 = slog.KindUint64
22+
KindGroup = slog.KindGroup
23+
KindLogValuer = slog.KindLogValuer
24+
)
25+
26+
// An Attr is a key-value pair.
27+
type Attr = slog.Attr
28+
29+
// String returns an Attr for a string value.
30+
func String(key, value string) Attr {
31+
return slog.String(key, value)
32+
}
33+
34+
// Int64 returns an Attr for an int64.
35+
func Int64(key string, value int64) Attr {
36+
return slog.Int64(key, value)
37+
}
38+
39+
// Int converts an int to an int64 and returns
40+
// an Attr with that value.
41+
func Int(key string, value int) Attr {
42+
return slog.Int(key, value)
43+
}
44+
45+
// Uint64 returns an Attr for a uint64.
46+
func Uint64(key string, v uint64) Attr {
47+
return slog.Uint64(key, v)
48+
}
49+
50+
// Float64 returns an Attr for a floating-point number.
51+
func Float64(key string, v float64) Attr {
52+
return slog.Float64(key, v)
53+
}
54+
55+
// Bool returns an Attr for a bool.
56+
func Bool(key string, v bool) Attr {
57+
return slog.Bool(key, v)
58+
}
59+
60+
// Time returns an Attr for a [time.Time].
61+
// It discards the monotonic portion.
62+
func Time(key string, v time.Time) Attr {
63+
return slog.Time(key, v)
64+
}
65+
66+
// Duration returns an Attr for a [time.Duration].
67+
func Duration(key string, v time.Duration) Attr {
68+
return slog.Duration(key, v)
69+
}
70+
71+
// Group returns an Attr for a Group [Value].
72+
// The first argument is the key; the remaining arguments
73+
// are converted to Attrs as in [Logger.Log].
74+
//
75+
// Use Group to collect several key-value pairs under a single
76+
// key on a log line, or as the result of LogValue
77+
// in order to log a single value as multiple Attrs.
78+
func Group(key string, args ...any) Attr {
79+
return slog.Group(key, args...)
80+
}
81+
82+
// Any returns an Attr for the supplied value.
83+
// See [AnyValue] for how values are treated.
84+
func Any(key string, value any) Attr {
85+
return slog.Any(key, value)
86+
}

0 commit comments

Comments
 (0)