-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathslog.go
More file actions
134 lines (119 loc) · 4.62 KB
/
Copy pathslog.go
File metadata and controls
134 lines (119 loc) · 4.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package serrors
import (
"errors"
"fmt"
"log/slog"
"strings"
)
const (
// DataKey is the slog attribute key used when [LogAttrs] falls back to wrapping
// an arbitrary Data value as a single [slog.Any] attribute. This occurs when Data is
// neither a []slog.Attr slice nor a [slog.LogValuer] that resolves to a group.
DataKey = "data"
// MessageKey is the attribute key used for the error message in the slog group value
// produced by [Error.LogValue] when the error chain contains structured [Error.Data].
// When no structured data is present, [Error.LogValue] returns a plain string value
// and this key is not emitted. This mirrors the convention of [log/slog.MessageKey].
MessageKey = "message"
// StackTraceKey is the slog attribute key used for the stack trace string in the
// group value produced by [StackTrace.LogValue]. It mirrors the [MessageKey] convention.
StackTraceKey = "stack"
)
// LogAttrs walks the error chain and returns [log/slog.Attr] values collected
// from every *[Error] node that has a non-nil Data, aggregated outermost to
// innermost. It is the slog-oriented counterpart to [AllData].
//
// Each node's Data is converted according to the same rules documented on [Error.Data]:
// - [][log/slog.Attr]: returned verbatim.
// - [log/slog.LogValuer] that resolves to a group: the group's attributes.
// - Any other type: wrapped as a single slog.Any(DataKey, ...) attribute.
//
// Example:
//
// slog.LogAttrs(ctx, slog.LevelError, "request failed",
// append(serrors.LogAttrs(err), slog.Any("error", err))...,
// )
func LogAttrs(err error) []slog.Attr {
var all []slog.Attr
var e *Error
for errors.As(err, &e) {
err = e.Err
if e.Data == nil {
continue
}
switch value := e.Data.(type) {
case []slog.Attr:
all = append(all, value...)
case slog.LogValuer:
v := slog.AnyValue(value).Resolve()
if v.Kind() == slog.KindGroup {
all = append(all, v.Group()...)
} else {
all = append(all, slog.Any(DataKey, value))
}
default:
all = append(all, slog.Any(DataKey, e.Data))
}
}
return all
}
var _ slog.LogValuer = (*Error)(nil)
// LogValue implements [log/slog.LogValuer], allowing *[Error] values to be passed
// directly to slog methods and emit structured output automatically.
//
// When no *[Error] node in the chain carries structured Data, LogValue returns a plain
// string value equivalent to e.Error(), identical to passing a plain error to slog.
// This ensures zero regression for errors without attached data.
//
// When at least one node carries Data, LogValue returns a slog group value whose first
// attribute is the full error message under the key [MessageKey], followed by all structured
// attributes collected by [LogAttrs]. Slog emits the group nested under the caller's key:
//
// // no Data anywhere in chain:
// slog.Error("dial failed", "err", wrappedErr)
// // -> err="service: dial: connection refused"
//
// // with Data (e.g. ConnCtx implements slog.LogValuer):
// slog.Error("dial failed", "err", wrappedErr)
// // -> err.message="service: dial: connection refused" err.host="db" err.attempt=2
func (e *Error) LogValue() slog.Value {
extra := LogAttrs(e)
if len(extra) == 0 {
return slog.StringValue(e.Error())
}
attrs := make([]slog.Attr, 0, 1+len(extra))
attrs = append(attrs, slog.String(MessageKey, e.Error()))
attrs = append(attrs, extra...)
return slog.GroupValue(attrs...)
}
var _ slog.LogValuer = StackTrace(nil)
// LogValue implements [log/slog.LogValuer].
// It resolves all frames and emits them as a single semicolon-separated string
// under the key [StackTraceKey] ("stack"), wrapped in a slog group so that
// [LogAttrs] and [Error.LogValue] naturally nest it under the caller's key:
//
// slog.Error("failed", "err", wrappedErr)
// // -> err.message="service: op: cause" err.stack="github.com/user/myapp/store.go:42 main.openDB; github.com/user/myapp/main.go:17 main.run"
//
// Each frame is formatted as "file:line function", where the file path has
// build-environment prefixes trimmed (see [StackTrace] for details).
// An empty [StackTrace] emits an empty group (no [StackTraceKey] attribute is produced).
//
// This is called automatically by [LogAttrs] and [Error.LogValue] when a [StackTrace]
// is stored as [Error.Data]. No manual invocation is needed for slog integration.
func (st StackTrace) LogValue() slog.Value {
frames := st.Frames()
if len(frames) == 0 {
return slog.GroupValue()
}
parts := make([]string, len(frames))
for i, frame := range frames {
parts[i] = fmt.Sprintf(
"%s:%d %s",
trimPath(frame.File),
frame.Line,
frame.Function,
)
}
return slog.GroupValue(slog.String(StackTraceKey, strings.Join(parts, "; ")))
}