details.go

v0.2.0
Doc Versions Source
1
package culpa
2
3
import (
4
	"context"
5
	"io"
6
	"log/slog"
7
	"net/http"
8
	"time"
9
)
10
11
// HintDetail carries a debugging hint.
12
type HintDetail struct{ Hint string }
13
14
// LogValue implements [slog.LogValuer].
15
func (d HintDetail) LogValue() slog.Value { return slog.StringValue(d.Hint) }
16
17
// InDetail carries a feature category or domain.
18
type InDetail struct{ Category string }
19
20
// LogValue implements [slog.LogValuer].
21
func (d InDetail) LogValue() slog.Value { return slog.StringValue(d.Category) }
22
23
// OwnerDetail carries the name or email of the responsible team.
24
type OwnerDetail struct{ Owner string }
25
26
// LogValue implements [slog.LogValuer].
27
func (d OwnerDetail) LogValue() slog.Value { return slog.StringValue(d.Owner) }
28
29
// PublicDetail carries a message safe to show to end users.
30
type PublicDetail struct{ Message string }
31
32
// LogValue implements [slog.LogValuer].
33
func (d PublicDetail) LogValue() slog.Value { return slog.StringValue(d.Message) }
34
35
// RequestDetail carries an HTTP request snapshot.
36
type RequestDetail struct {
37
	Method string
38
	URL    string
39
	Header http.Header
40
	Body   []byte
41
}
42
43
// LogValue implements [slog.LogValuer].
44
func (d RequestDetail) LogValue() slog.Value {
45
	return slog.GroupValue(
46
		slog.String("method", d.Method),
47
		slog.String("url", d.URL),
48
	)
49
}
50
51
// ResponseDetail carries an HTTP response snapshot.
52
type ResponseDetail struct {
53
	StatusCode int
54
	Header     http.Header
55
	Body       []byte
56
}
57
58
// LogValue implements [slog.LogValuer].
59
func (d ResponseDetail) LogValue() slog.Value {
60
	return slog.GroupValue(
61
		slog.Int("statusCode", d.StatusCode),
62
	)
63
}
64
65
// TagsDetail carries freeform string tags.
66
// Uses MergeCombine so tags accumulate across layers.
67
type TagsDetail struct{ Tags []string }
68
69
// MergeStrategy implements MergeStrategyProvider.
70
func (TagsDetail) MergeStrategy() MergeStrategy { return MergeCombine }
71
72
// LogValue implements [slog.LogValuer].
73
func (d TagsDetail) LogValue() slog.Value { return slog.AnyValue(d.Tags) }
74
75
// TimeDetail carries a timestamp associated with the error.
76
type TimeDetail struct{ Time time.Time }
77
78
// LogValue implements [slog.LogValuer].
79
func (d TimeDetail) LogValue() slog.Value { return slog.TimeValue(d.Time) }
80
81
// TraceDetail carries a distributed trace ID for correlation.
82
type TraceDetail struct{ TraceID string }
83
84
// LogValue implements [slog.LogValuer].
85
func (d TraceDetail) LogValue() slog.Value { return slog.StringValue(d.TraceID) }
86
87
// ContextDetail carries key-value pairs extracted from context.
88
type ContextDetail struct{ Values map[any]any }
89
90
// LogValue implements [slog.LogValuer].
91
func (d ContextDetail) LogValue() slog.Value { return slog.AnyValue(d.Values) }
92
93
// CodeDetail carries a machine-readable error code or slug.
94
type CodeDetail struct{ Code any }
95
96
// LogValue implements [slog.LogValuer].
97
func (d CodeDetail) LogValue() slog.Value { return slog.AnyValue(d.Code) }
98
99
// TemporaryDetail marks an error as temporary (retryable).
100
type TemporaryDetail struct{}
101
102
// LogValue implements [slog.LogValuer].
103
func (TemporaryDetail) LogValue() slog.Value { return slog.BoolValue(true) }
104
105
// TimeoutDetail marks an error as a timeout.
106
type TimeoutDetail struct{}
107
108
// LogValue implements [slog.LogValuer].
109
func (TimeoutDetail) LogValue() slog.Value { return slog.BoolValue(true) }
110
111
// DeadlineDetail marks an error as a deadline exceeded.
112
type DeadlineDetail struct{}
113
114
// LogValue implements [slog.LogValuer].
115
func (DeadlineDetail) LogValue() slog.Value { return slog.BoolValue(true) }
116
117
// CancelDetail marks an error as a cancellation.
118
type CancelDetail struct{}
119
120
// LogValue implements [slog.LogValuer].
121
func (CancelDetail) LogValue() slog.Value { return slog.BoolValue(true) }
122
123
// WithHint attaches a HintDetail.
124
func WithHint(err error, hint string) error {
125
	return WithDetail(err, HintDetail{Hint: hint})
126
}
127
128
// WithIn attaches an InDetail.
129
func WithIn(err error, category string) error {
130
	return WithDetail(err, InDetail{Category: category})
131
}
132
133
// WithOwner attaches an OwnerDetail.
134
func WithOwner(err error, owner string) error {
135
	return WithDetail(err, OwnerDetail{Owner: owner})
136
}
137
138
// WithPublic attaches a PublicDetail.
139
func WithPublic(err error, msg string) error {
140
	return WithDetail(err, PublicDetail{Message: msg})
141
}
142
143
// WithRequest attaches a RequestDetail from an HTTP request.
144
func WithRequest(err error, req *http.Request, withBody bool) error {
145
	d := RequestDetail{
146
		Method: req.Method,
147
		URL:    req.URL.String(),
148
		Header: req.Header.Clone(),
149
	}
150
	if withBody && req.Body != nil {
151
		d.Body, _ = io.ReadAll(req.Body)
152
	}
153
	return WithDetail(err, d)
154
}
155
156
// WithResponse attaches a ResponseDetail from an HTTP response.
157
func WithResponse(err error, res *http.Response, withBody bool) error {
158
	d := ResponseDetail{
159
		StatusCode: res.StatusCode,
160
		Header:     res.Header.Clone(),
161
	}
162
	if withBody && res.Body != nil {
163
		d.Body, _ = io.ReadAll(res.Body)
164
	}
165
	return WithDetail(err, d)
166
}
167
168
// WithTags attaches a TagsDetail.
169
func WithTags(err error, tags ...string) error {
170
	return WithDetail(err, TagsDetail{Tags: tags})
171
}
172
173
// WithTime attaches a TimeDetail.
174
func WithTime(err error, t time.Time) error {
175
	return WithDetail(err, TimeDetail{Time: t})
176
}
177
178
// WithTrace attaches a TraceDetail (distributed trace ID).
179
func WithTrace(err error, traceID string) error {
180
	return WithDetail(err, TraceDetail{TraceID: traceID})
181
}
182
183
// WithContext attaches a ContextDetail with values extracted from ctx
184
// for the given keys.
185
func WithContext(err error, ctx context.Context, keys ...any) error {
186
	vals := make(map[any]any, len(keys))
187
	for _, k := range keys {
188
		vals[k] = ctx.Value(k)
189
	}
190
	return WithDetail(err, ContextDetail{Values: vals})
191
}
192
193
// WithCode attaches a CodeDetail.
194
func WithCode(err error, code any) error {
195
	return WithDetail(err, CodeDetail{Code: code})
196
}
197
198
// WithTemporary marks the error as temporary.
199
func WithTemporary(err error) error {
200
	return WithDetail(err, TemporaryDetail{})
201
}
202
203
// WithTimeout marks the error as a timeout.
204
func WithTimeout(err error) error {
205
	return WithDetail(err, TimeoutDetail{})
206
}
207
208
// IsTemporary reports whether the error is marked as temporary.
209
func IsTemporary(err error) bool {
210
	var d TemporaryDetail
211
	return FindDetail(err, &d)
212
}
213
214
// IsTimeout reports whether the error is marked as a timeout.
215
func IsTimeout(err error) bool {
216
	var d TimeoutDetail
217
	return FindDetail(err, &d)
218
}
219
220
// WithDeadline marks the error as a deadline exceeded.
221
func WithDeadline(err error) error {
222
	return WithDetail(err, DeadlineDetail{})
223
}
224
225
// WithCancel marks the error as a cancellation.
226
func WithCancel(err error) error {
227
	return WithDetail(err, CancelDetail{})
228
}
229
230
// IsDeadline reports whether the error is marked as deadline exceeded.
231
func IsDeadline(err error) bool {
232
	var d DeadlineDetail
233
	return FindDetail(err, &d)
234
}
235
236
// IsCanceled reports whether the error is marked as canceled.
237
func IsCanceled(err error) bool {
238
	var d CancelDetail
239
	return FindDetail(err, &d)
240
}
241
242
// WrapContext wraps ctx.Err() with a message, auto-applying the appropriate
243
// marker (CancelDetail or DeadlineDetail). If context.Cause returns a
244
// different error than ctx.Err(), it is included in the chain.
245
// Returns nil if ctx.Err() is nil.
246
func WrapContext(ctx context.Context, msg string) error {
247
	ctxErr := ctx.Err()
248
	if ctxErr == nil {
249
		return nil
250
	}
251
252
	cause := context.Cause(ctx)
253
254
	var inner error = ctxErr
255
	if cause != nil && cause != ctxErr {
256
		inner = &joinedError{msg: ctxErr.Error(), errs: []error{ctxErr, cause}}
257
	}
258
259
	err := withStacktrace(&culpaError{msg: msg, cause: inner})
260
261
	switch ctxErr {
262
	case context.Canceled:
263
		err = WithCancel(err)
264
	case context.DeadlineExceeded:
265
		err = WithDeadline(err)
266
	}
267
268
	return err
269
}
270

Source Files