Skip to content

Commit 77e53e3

Browse files
committed
refactor: integrate native log/slog support and remove custom field system
This commit introduces breaking changes by replacing the custom field system with Go's native log/slog attributes. Key updates include the removal of the FieldLogger interface and related field types, the addition of slog.Attr support, and the introduction of new logging functions. The migration guide is provided for transitioning from v0.4.1 to v0.5.0.
1 parent e81e607 commit 77e53e3

16 files changed

Lines changed: 1874 additions & 463 deletions

MIGRATION.md

Lines changed: 561 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 171 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
[![Maintainability](https://api.codeclimate.com/v1/badges/fe1720426006f3af30b0/maintainability)](https://codeclimate.com/github/muonsoft/errors/maintainability)
1010
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](CODE_OF_CONDUCT.md)
1111

12-
Errors package for structured logging. Adds stack trace without a pain
12+
Errors package for structured logging with native `log/slog` integration. Adds stack trace without a pain
1313
(no confuse with `Wrap`/`WithMessage` methods).
1414

15+
> **⚠️ Breaking Changes in v0.5.0**
16+
> Version 0.5.0 replaces the custom field system with native `log/slog` integration.
17+
> If you're migrating from v0.4.1, please read the [Migration Guide](MIGRATION.md).
18+
1519
## Key features
1620

1721
This package is based on well known [github.com/pkg/errors](https://github.com/pkg/errors).
@@ -22,13 +26,17 @@ Key differences and features:
2226
* minimalistic API: few methods to wrap an error: `errors.Errorf()`, `errors.Wrap()`;
2327
* adds stack trace idempotently (only once in a chain);
2428
* `errors.As()` method is based on typed parameters (aka generics);
25-
* options to skip caller in a stack trace and to add error fields for structured logging;
26-
* error fields are made for the statically typed logger interface;
29+
* options to skip caller in a stack trace and to add error attributes for structured logging;
30+
* **native integration with Go's `log/slog`** - error attributes use `slog.Attr`;
31+
* **supports grouped attributes** via `slog.Group`;
32+
* implements `slog.LogValuer` for seamless slog integration;
2733
* package errors can be easily marshaled into JSON with all fields in a chain.
2834

2935
## Additional features
3036

3137
* `errors.IsOfType[T any](err error)` to test for error types.
38+
* `errors.Attrs(err error) []slog.Attr` to extract all attributes from error chain.
39+
* `errors.Log(ctx, logger, level, err)` convenience function for logging with slog.
3240

3341
## Installation
3442

@@ -38,6 +46,10 @@ Run the following command to install the package
3846
go get -u github.com/muonsoft/errors
3947
```
4048

49+
Requires Go 1.21+ for `log/slog` support.
50+
51+
**Migrating from v0.4.1?** See the [Migration Guide](MIGRATION.md) for detailed instructions.
52+
4153
## How to use
4254

4355
### `errors.New()` for package-level errors
@@ -57,14 +69,14 @@ func NewNotFoundError() error {
5769
}
5870
```
5971

60-
### `errors.Errorf()` for wrapping errors with formatted message, fields and stack trace
72+
### `errors.Errorf()` for wrapping errors with formatted message, attributes and stack trace
6173

6274
`errors.Errorf()` is an equivalent to standard `fmt.Errorf()`. It formats according to a format specifier
6375
and returns the string as a value that satisfies error. You can wrap an error using `%w` modifier.
6476

6577
`errors.Errorf()` also records the stack trace at the point it was called. If the wrapped error
66-
contains a stack trace then a new one will not be added to a chain. Also, you can pass an
67-
options to set a structured fields or to skip a caller in a stack trace.
78+
contains a stack trace then a new one will not be added to a chain. Also, you can pass
79+
options to set structured attributes or to skip a caller in a stack trace.
6880
Options must be specified after formatting arguments.
6981

7082
```golang
@@ -73,7 +85,7 @@ var product Product
7385
err := row.Scan(&product.ID, &product.Name)
7486
if err != nil {
7587
// Use errors.Errorf to wrap the library error with the message context and
76-
// error fields to be used for structured logging.
88+
// error attributes to be used for structured logging.
7789
return nil, errors.Errorf(
7890
"%w: %v", errSQLError, err.Error(),
7991
errors.String("sql", findSQL),
@@ -82,18 +94,18 @@ if err != nil {
8294
}
8395
```
8496

85-
### `errors.Wrap()` for wrapping errors with fields and stack trace
97+
### `errors.Wrap()` for wrapping errors with attributes and stack trace
8698

8799
`errors.Wrap()` returns an error annotating err with a stack trace at the point `errors.Wrap()` is called.
88100
If the wrapped error contains a stack trace then a new one will not be added to a chain.
89-
If err is nil, Wrap returns nil. Also, you can pass an options to set a structured fields or to skip a caller
101+
If err is nil, Wrap returns nil. Also, you can pass options to set structured attributes or to skip a caller
90102
in a stack trace.
91103

92104
```golang
93105
data, err := service.Handle(ctx, userID, message)
94106
if err != nil {
95107
// Adds a stack trace to the line that was called (if there is no stack trace in the chain already)
96-
// and adds fields for structured logging.
108+
// and adds attributes for structured logging.
97109
return nil, errors.Wrap(
98110
err,
99111
errors.Int("userID", userID),
@@ -102,9 +114,42 @@ if err != nil {
102114
}
103115
```
104116

117+
### Working with grouped attributes
118+
119+
The package supports grouped attributes via `slog.Group`, allowing you to organize related attributes:
120+
121+
```golang
122+
err := errors.Wrap(
123+
dbErr,
124+
errors.Group("request",
125+
slog.String("method", "POST"),
126+
slog.String("path", "/api/users"),
127+
slog.Int("status", 500),
128+
),
129+
errors.Group("database",
130+
slog.String("query", "INSERT INTO users..."),
131+
slog.Duration("duration", 150*time.Millisecond),
132+
),
133+
)
134+
```
135+
136+
You can also use `errors.Attr()` to pass `slog.Attr` directly:
137+
138+
```golang
139+
err := errors.Wrap(
140+
err,
141+
errors.Attr(slog.Int64("timestamp", time.Now().Unix())),
142+
errors.Attr(slog.Group("metadata",
143+
slog.String("version", "v1.2.3"),
144+
slog.Bool("production", true),
145+
)),
146+
)
147+
```
148+
105149
### Printing error with stack trace
106150

107-
You can use formatting with `%+v` modifier to print errors with message, fields for logging and a stack trace.
151+
You can use formatting with `%+v` modifier to print errors with message, attributes and stack trace.
152+
Grouped attributes are displayed using dot notation.
108153

109154
Example
110155

@@ -117,7 +162,10 @@ func main() {
117162
)
118163
err = errors.Errorf(
119164
"find product: %w", err,
120-
errors.String("requestID", "24874020-cab7-4ef3-bac5-76858832f8b0"),
165+
errors.Group("request",
166+
slog.String("id", "24874020-cab7-4ef3-bac5-76858832f8b0"),
167+
slog.String("method", "GET"),
168+
),
121169
)
122170
fmt.Printf("%+v", err)
123171
}
@@ -127,7 +175,8 @@ Output
127175

128176
```
129177
find product: sql error: sql: no rows in result set
130-
requestID: 24874020-cab7-4ef3-bac5-76858832f8b0
178+
request.id: 24874020-cab7-4ef3-bac5-76858832f8b0
179+
request.method: GET
131180
sql: SELECT id, name FROM product WHERE id = ?
132181
productID: 123
133182
main.main
@@ -140,7 +189,7 @@ runtime.goexit
140189

141190
### Marshal error into JSON
142191

143-
Wrapped errors implements `json.Marshaler` interface. So you can easily marshal errors into JSON.
192+
Wrapped errors implement `json.Marshaler` interface. Grouped attributes are marshaled as nested objects.
144193

145194
Example
146195

@@ -153,7 +202,10 @@ func main() {
153202
)
154203
err = errors.Errorf(
155204
"find product: %w", err,
156-
errors.String("requestID", "24874020-cab7-4ef3-bac5-76858832f8b0"),
205+
errors.Group("request",
206+
slog.String("id", "24874020-cab7-4ef3-bac5-76858832f8b0"),
207+
slog.String("method", "GET"),
208+
),
157209
)
158210
errJSON, err := json.MarshalIndent(err, "", "\t")
159211
if err != nil {
@@ -169,7 +221,10 @@ Output
169221
{
170222
"error": "find product: sql error: sql: no rows in result set",
171223
"productID": 123,
172-
"requestID": "24874020-cab7-4ef3-bac5-76858832f8b0",
224+
"request": {
225+
"id": "24874020-cab7-4ef3-bac5-76858832f8b0",
226+
"method": "GET"
227+
},
173228
"sql": "SELECT id, name FROM product WHERE id = ?",
174229
"stackTrace": [
175230
{
@@ -191,31 +246,117 @@ Output
191246
}
192247
```
193248

194-
### Structured logging
249+
### Structured logging with slog
195250

196-
To use structured logging, you need to use an adapter for your logging system. It can be one of the
197-
built-in adapters from the `logging` directory, or you can implement your own adapter using `errors.Logger` interface.
251+
The package provides native integration with Go's `log/slog`. Errors implement `slog.LogValuer`,
252+
so they work seamlessly with any slog logger.
198253

199-
Example of using an adapter for [Logrus](https://github.com/sirupsen/logrus).
254+
#### Using Log convenience function
200255

201256
```golang
202257
err := errors.Errorf(
203-
"sql error: %w", sql.ErrNoRows,
204-
errors.String("sql", "SELECT id, name FROM product WHERE id = ?"),
205-
errors.Int("productID", 123),
258+
"database query failed: %w", dbErr,
259+
errors.String("query", "SELECT * FROM users WHERE id = ?"),
260+
errors.Int("userID", 123),
261+
errors.Group("performance",
262+
slog.Duration("duration", 250*time.Millisecond),
263+
slog.Int("retries", 3),
264+
),
206265
)
207-
err = errors.Errorf(
208-
"find product: %w", err,
209-
errors.String("requestID", "24874020-cab7-4ef3-bac5-76858832f8b0"),
266+
267+
// Log error with all attributes and stack trace
268+
errors.Log(ctx, slog.Default(), slog.LevelError, err)
269+
```
270+
271+
#### Extracting attributes manually
272+
273+
```golang
274+
err := errors.Errorf(
275+
"operation failed: %w", someErr,
276+
errors.String("operation", "user.create"),
277+
errors.Int("userID", 123),
210278
)
211-
logger := logrus.New()
212-
logrusadapter.Log(err, logger)
279+
280+
// Extract all attributes from error chain
281+
attrs := errors.Attrs(err)
282+
283+
// Use with slog
284+
slog.Error("request failed", append([]any{slog.Any("error", err)}, attrsToAny(attrs)...)...)
213285
```
214286

215-
Output
287+
#### Using slog.LogValuer
216288

289+
Errors automatically work as `slog.LogValuer`, so you can log them directly:
290+
291+
```golang
292+
err := errors.Wrap(
293+
dbErr,
294+
errors.String("table", "users"),
295+
errors.Int("id", 123),
296+
)
297+
298+
// The error will automatically provide its attributes to slog
299+
slog.Error("database error", "error", err)
300+
```
301+
302+
### Custom LoggableError types
303+
304+
You can implement `errors.LoggableError` interface on your custom error types to provide
305+
structured attributes:
306+
307+
```golang
308+
type ValidationError struct {
309+
Field string
310+
Value interface{}
311+
Message string
312+
}
313+
314+
func (e *ValidationError) Error() string {
315+
return fmt.Sprintf("validation failed: %s", e.Message)
316+
}
317+
318+
// Implement errors.LoggableError
319+
func (e *ValidationError) Attrs() []slog.Attr {
320+
return []slog.Attr{
321+
slog.String("field", e.Field),
322+
slog.Any("value", e.Value),
323+
slog.String("validation_message", e.Message),
324+
}
325+
}
326+
327+
// Usage
328+
err := &ValidationError{
329+
Field: "email",
330+
Value: "invalid-email",
331+
Message: "must be a valid email address",
332+
}
333+
wrapped := errors.Wrap(err, errors.String("operation", "user.create"))
334+
335+
// All attributes from ValidationError will be included
336+
attrs := errors.Attrs(wrapped)
217337
```
218-
ERRO[0000] find product: sql error: sql: no rows in result set productID=123 requestID=24874020-cab7-4ef3-bac5-76858832f8b0 sql="SELECT id, name FROM product WHERE id = ?" stackTrace="[{main.main /home/strider/projects/errors/var/scratch.go 12} {runtime.main /usr/local/go/src/runtime/proc.go 250} {runtime.goexit /usr/local/go/src/runtime/asm_amd64.s 1571}]"
338+
339+
## Available attribute options
340+
341+
The package provides convenience functions for creating attributes:
342+
343+
```golang
344+
errors.Bool(key string, value bool)
345+
errors.Int(key string, value int)
346+
errors.Uint(key string, value uint)
347+
errors.Float(key string, value float64)
348+
errors.String(key string, value string)
349+
errors.Stringer(key string, value fmt.Stringer)
350+
errors.Strings(key string, values []string)
351+
errors.Value(key string, value interface{})
352+
errors.Time(key string, value time.Time)
353+
errors.Duration(key string, value time.Duration)
354+
errors.JSON(key string, value json.RawMessage)
355+
356+
// New slog-specific options
357+
errors.Attr(attr slog.Attr) // Add any slog.Attr directly
358+
errors.WithAttrs(attrs ...slog.Attr) // Add multiple slog.Attr values
359+
errors.Group(key string, attrs ...slog.Attr) // Create a grouped attribute
219360
```
220361

221362
## Contributing

0 commit comments

Comments
 (0)