Skip to content

Commit c97ad88

Browse files
committed
feat: enhance error handling with direct slog.Attr support
This commit updates the error handling functions to allow passing slog.Attr directly to Errorf and Wrap, improving flexibility and usability. It also introduces new helper functions for structured attributes and updates the documentation to reflect these changes. Additionally, comprehensive tests are added to ensure the correct functionality of the new features.
1 parent 70928cb commit c97ad88

4 files changed

Lines changed: 386 additions & 48 deletions

File tree

MIGRATION.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,16 +243,33 @@ err := errors.Wrap(
243243

244244
### 3. Direct slog.Attr Usage
245245

246-
Pass `slog.Attr` directly for maximum flexibility:
246+
v0.5.0 allows passing `slog.Attr` directly without wrapping in `errors.Attr()`:
247247

248248
```go
249+
// You can pass slog.Attr directly (NEW!)
249250
err := errors.Wrap(
250251
err,
251-
errors.Attr(slog.Int64("timestamp", time.Now().Unix())),
252-
errors.Attr(slog.Group("metadata",
252+
slog.Int64("timestamp", time.Now().Unix()),
253+
slog.String("user", "john"),
254+
slog.Group("metadata",
253255
slog.String("version", "v1.2.3"),
254256
slog.Bool("production", true),
255-
)),
257+
),
258+
)
259+
260+
// Or use helper functions (also works)
261+
err := errors.Wrap(
262+
err,
263+
errors.Int64("timestamp", time.Now().Unix()),
264+
errors.String("user", "john"),
265+
)
266+
267+
// Or mix both styles
268+
err := errors.Wrap(
269+
err,
270+
errors.SkipCaller(), // errors.Option
271+
slog.String("user", "john"), // slog.Attr directly
272+
errors.Int("id", 123), // errors.Option
256273
)
257274
```
258275

README.md

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -75,88 +75,128 @@ func NewNotFoundError() error {
7575
and returns the string as a value that satisfies error. You can wrap an error using `%w` modifier.
7676

7777
`errors.Errorf()` also records the stack trace at the point it was called. If the wrapped error
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.
80-
Options must be specified after formatting arguments.
78+
contains a stack trace then a new one will not be added to a chain.
79+
80+
You can pass options to set structured attributes or to skip a caller in a stack trace.
81+
Both helper functions like `errors.String()` and `slog.Attr` directly are supported.
82+
Options/attributes must be specified after formatting arguments.
8183

8284
```golang
8385
row := repository.db.QueryRow(ctx, findSQL, id)
8486
var product Product
8587
err := row.Scan(&product.ID, &product.Name)
8688
if err != nil {
87-
// Use errors.Errorf to wrap the library error with the message context and
88-
// error attributes to be used for structured logging.
89+
// Option 1: Use helper functions
8990
return nil, errors.Errorf(
9091
"%w: %v", errSQLError, err.Error(),
9192
errors.String("sql", findSQL),
9293
errors.Int("productID", id),
9394
)
95+
96+
// Option 2: Use slog.Attr directly (more idiomatic)
97+
return nil, errors.Errorf(
98+
"%w: %v", errSQLError, err.Error(),
99+
slog.String("sql", findSQL),
100+
slog.Int("productID", id),
101+
)
94102
}
95103
```
96104

97105
### `errors.Wrap()` for wrapping errors with attributes and stack trace
98106

99107
`errors.Wrap()` returns an error annotating err with a stack trace at the point `errors.Wrap()` is called.
100108
If the wrapped error contains a stack trace then a new one will not be added to a chain.
101-
If err is nil, Wrap returns nil. Also, you can pass options to set structured attributes or to skip a caller
102-
in a stack trace.
109+
If err is nil, Wrap returns nil.
110+
111+
You can pass options to set structured attributes or to skip a caller in a stack trace.
112+
Both helper functions like `errors.String()` and `slog.Attr` directly are supported.
103113

104114
```golang
105115
data, err := service.Handle(ctx, userID, message)
106116
if err != nil {
107-
// Adds a stack trace to the line that was called (if there is no stack trace in the chain already)
108-
// and adds attributes for structured logging.
117+
// Option 1: Use helper functions
109118
return nil, errors.Wrap(
110119
err,
111120
errors.Int("userID", userID),
112121
errors.String("userMessage", message),
113122
)
123+
124+
// Option 2: Use slog.Attr directly (recommended)
125+
return nil, errors.Wrap(
126+
err,
127+
slog.Int("userID", userID),
128+
slog.String("userMessage", message),
129+
)
130+
131+
// Option 3: Mix both styles
132+
return nil, errors.Wrap(
133+
err,
134+
errors.SkipCaller(), // Option for stack trace
135+
slog.String("userMessage", message), // slog.Attr for logging
136+
)
114137
}
115138
```
116139

117-
### Working with grouped attributes
140+
### Working with slog attributes
118141

119-
The package supports grouped attributes via `slog.Group`, allowing you to organize related attributes:
142+
The package has native slog integration - you can pass `slog.Attr` directly to `Wrap()` and `Errorf()`:
120143

121144
```golang
145+
// Use slog attributes directly (recommended)
122146
err := errors.Wrap(
123147
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-
),
148+
slog.String("table", "users"),
149+
slog.Int("id", 123),
150+
slog.Duration("query_time", 50*time.Millisecond),
133151
)
134-
```
135152

136-
You can use all slog attribute types directly:
153+
// Or use helper functions (equivalent)
154+
err := errors.Wrap(
155+
dbErr,
156+
errors.String("table", "users"),
157+
errors.Int("id", 123),
158+
errors.Duration("query_time", 50*time.Millisecond),
159+
)
137160

138-
```golang
161+
// All slog types are supported
139162
err := errors.Wrap(
140163
err,
141-
errors.Int64("timestamp", time.Now().Unix()),
142-
errors.Uint64("bytes_written", uint64(1024*1024*500)),
143-
errors.Float64("cpu_usage", 0.85),
144-
errors.Any("metadata", map[string]interface{}{
164+
slog.Bool("cached", false),
165+
slog.Int64("timestamp", time.Now().Unix()),
166+
slog.Uint64("bytes_written", uint64(1024*1024*500)),
167+
slog.Float64("cpu_usage", 0.85),
168+
slog.Any("metadata", map[string]interface{}{
145169
"version": "v1.2.3",
146170
"region": "us-west-1",
147171
}),
148172
)
149173
```
150174

151-
Or pass `slog.Attr` directly for maximum flexibility:
175+
### Working with grouped attributes
176+
177+
Organize related attributes using `slog.Group` directly:
152178

153179
```golang
154180
err := errors.Wrap(
155-
err,
156-
errors.Attr(slog.Group("metadata",
157-
slog.String("version", "v1.2.3"),
158-
slog.Bool("production", true),
159-
)),
181+
dbErr,
182+
slog.Group("request",
183+
slog.String("method", "POST"),
184+
slog.String("path", "/api/users"),
185+
slog.Int("status", 500),
186+
),
187+
slog.Group("database",
188+
slog.String("query", "INSERT INTO users..."),
189+
slog.Duration("duration", 150*time.Millisecond),
190+
),
191+
)
192+
193+
// Or use the errors.Group helper
194+
err := errors.Wrap(
195+
dbErr,
196+
errors.Group("request",
197+
slog.String("method", "POST"),
198+
slog.String("path", "/api/users"),
199+
),
160200
)
161201
```
162202

errors.go

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,17 @@ func Unwrap(err error) error {
115115
// Errorf formats according to a format specifier and returns the string
116116
// as a value that satisfies error. You can wrap an error using %w modifier as it
117117
// does fmt.Errorf function.
118+
//
118119
// Errorf also records the stack trace at the point it was called. If the wrapped error
119120
// contains a stack trace then a new one will not be added to a chain.
120-
// Also, you can pass an options to set a structured fields or to skip a caller
121-
// in a stack trace. Options must be specified after formatting arguments.
121+
//
122+
// You can pass options to set structured attributes or to skip a caller in a stack trace.
123+
// Both Option functions and slog.Attr values are accepted.
124+
// Options/attributes must be specified after formatting arguments:
125+
//
126+
// errors.Errorf("failed: %w", err, errors.String("key", "value"))
127+
// errors.Errorf("failed: %w", err, slog.String("key", "value"))
128+
// errors.Errorf("failed: %w", err, errors.SkipCaller(), slog.Int("id", 123))
122129
func Errorf(message string, argsAndOptions ...interface{}) error {
123130
args, options := splitArgsAndOptions(argsAndOptions)
124131
opts := newOptions(options...)
@@ -138,12 +145,20 @@ func Errorf(message string, argsAndOptions ...interface{}) error {
138145
// Wrap returns an error annotating err with a stack trace at the point Wrap is called.
139146
// If the wrapped error contains a stack trace then a new one will not be added to a chain.
140147
// If err is nil, Wrap returns nil.
141-
// Also, you can pass an options to set a structured fields or to skip a caller
142-
// in a stack trace.
143-
func Wrap(err error, options ...Option) error {
148+
//
149+
// You can pass options to set structured attributes or to skip a caller in a stack trace.
150+
// Both Option functions and slog.Attr values are accepted:
151+
//
152+
// errors.Wrap(err, errors.String("key", "value")) // Using Option
153+
// errors.Wrap(err, slog.String("key", "value")) // Using slog.Attr directly
154+
// errors.Wrap(err, errors.SkipCaller(), slog.Int("id", 123)) // Mixed
155+
func Wrap(err error, optsOrAttrs ...interface{}) error {
144156
if err == nil {
145157
return nil
146158
}
159+
160+
options := convertToOptions(optsOrAttrs)
161+
147162
if isWrapper(err) {
148163
if len(options) == 0 {
149164
return err
@@ -265,22 +280,45 @@ func (e *stacked) MarshalJSON() ([]byte, error) {
265280
func splitArgsAndOptions(argsAndOptions []interface{}) ([]interface{}, []Option) {
266281
argsCount := len(argsAndOptions)
267282
for i := argsCount - 1; i >= 0; i-- {
268-
if _, ok := argsAndOptions[i].(Option); ok {
283+
if isOptionOrAttr(argsAndOptions[i]) {
269284
argsCount--
270285
} else {
271286
break
272287
}
273288
}
274289

275290
args := argsAndOptions[:argsCount]
276-
options := make([]Option, 0, len(argsAndOptions)-argsCount)
277-
for i := argsCount; i < len(argsAndOptions); i++ {
278-
options = append(options, argsAndOptions[i].(Option))
279-
}
291+
optsOrAttrs := argsAndOptions[argsCount:]
292+
options := convertToOptions(optsOrAttrs)
280293

281294
return args, options
282295
}
283296

297+
// isOptionOrAttr checks if a value is either an Option or slog.Attr
298+
func isOptionOrAttr(v interface{}) bool {
299+
if _, ok := v.(Option); ok {
300+
return true
301+
}
302+
if _, ok := v.(slog.Attr); ok {
303+
return true
304+
}
305+
return false
306+
}
307+
308+
// convertToOptions converts a slice of Option and/or slog.Attr to []Option
309+
func convertToOptions(items []interface{}) []Option {
310+
options := make([]Option, 0, len(items))
311+
for _, item := range items {
312+
switch v := item.(type) {
313+
case Option:
314+
options = append(options, v)
315+
case slog.Attr:
316+
options = append(options, Attr(v))
317+
}
318+
}
319+
return options
320+
}
321+
284322
func getArgErrors(message string, args []interface{}) []error {
285323
indices := getErrorIndices(message)
286324
errs := make([]error, 0, len(indices))
@@ -361,7 +399,7 @@ func writeAttrs(w io.Writer, attrs []slog.Attr, prefix string) {
361399
// writeAttr writes a single attribute value to an io.Writer
362400
func writeAttr(w io.Writer, key string, value slog.Value) {
363401
io.WriteString(w, "\n"+key+": ")
364-
402+
365403
switch value.Kind() {
366404
case slog.KindBool:
367405
if value.Bool() {

0 commit comments

Comments
 (0)