-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherror.go
More file actions
395 lines (348 loc) · 9.59 KB
/
error.go
File metadata and controls
395 lines (348 loc) · 9.59 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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
// SPDX-License-Identifier: EUPL-1.2
// Structured errors, crash recovery, and reporting for the Core framework.
// Provides E() for error creation, Wrap()/WrapCode() for chaining,
// and Err for panic recovery and crash reporting.
package core
import (
"encoding/json"
"errors"
"iter"
"maps"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"sync"
"time"
)
// ErrorSink is the shared interface for error reporting.
// Implemented by ErrorLog (structured logging) and ErrorPanic (panic recovery).
type ErrorSink interface {
Error(msg string, keyvals ...any)
Warn(msg string, keyvals ...any)
}
var _ ErrorSink = (*Log)(nil)
// Err represents a structured error with operational context.
// It implements the error interface and supports unwrapping.
type Err struct {
Operation string // Operation being performed (e.g., "user.Save")
Message string // Human-readable message
Cause error // Underlying error (optional)
Code string // Error code (optional, e.g., "VALIDATION_FAILED")
}
// Error implements the error interface.
func (e *Err) Error() string {
var prefix string
if e.Operation != "" {
prefix = e.Operation + ": "
}
if e.Cause != nil {
if e.Code != "" {
return Concat(prefix, e.Message, " [", e.Code, "]: ", e.Cause.Error())
}
return Concat(prefix, e.Message, ": ", e.Cause.Error())
}
if e.Code != "" {
return Concat(prefix, e.Message, " [", e.Code, "]")
}
return Concat(prefix, e.Message)
}
// Unwrap returns the underlying error for use with errors.Is and errors.As.
func (e *Err) Unwrap() error {
return e.Cause
}
// --- Error Creation Functions ---
// E creates a new Err with operation context.
// The underlying error can be nil for creating errors without a cause.
//
// Example:
//
// return log.E("user.Save", "failed to save user", err)
// return log.E("api.Call", "rate limited", nil) // No underlying cause
func E(op, msg string, err error) error {
return &Err{Operation: op, Message: msg, Cause: err}
}
// Wrap wraps an error with operation context.
// Returns nil if err is nil, to support conditional wrapping.
// Preserves error Code if the wrapped error is an *Err.
//
// Example:
//
// return log.Wrap(err, "db.Query", "database query failed")
func Wrap(err error, op, msg string) error {
if err == nil {
return nil
}
// Preserve Code from wrapped *Err
var logErr *Err
if As(err, &logErr) && logErr.Code != "" {
return &Err{Operation: op, Message: msg, Cause: err, Code: logErr.Code}
}
return &Err{Operation: op, Message: msg, Cause: err}
}
// WrapCode wraps an error with operation context and error code.
// Returns nil only if both err is nil AND code is empty.
// Useful for API errors that need machine-readable codes.
//
// Example:
//
// return log.WrapCode(err, "VALIDATION_ERROR", "user.Validate", "invalid email")
func WrapCode(err error, code, op, msg string) error {
if err == nil && code == "" {
return nil
}
return &Err{Operation: op, Message: msg, Cause: err, Code: code}
}
// NewCode creates an error with just code and message (no underlying error).
// Useful for creating sentinel errors with codes.
//
// Example:
//
// var ErrNotFound = log.NewCode("NOT_FOUND", "resource not found")
func NewCode(code, msg string) error {
return &Err{Message: msg, Code: code}
}
// --- Standard Library Wrappers ---
// Is reports whether any error in err's tree matches target.
// Wrapper around errors.Is for convenience.
func Is(err, target error) bool {
return errors.Is(err, target)
}
// As finds the first error in err's tree that matches target.
// Wrapper around errors.As for convenience.
func As(err error, target any) bool {
return errors.As(err, target)
}
// NewError creates a simple error with the given text.
// Wrapper around errors.New for convenience.
func NewError(text string) error {
return errors.New(text)
}
// ErrorJoin combines multiple errors into one.
//
// core.ErrorJoin(err1, err2, err3)
func ErrorJoin(errs ...error) error {
return errors.Join(errs...)
}
// --- Error Introspection Helpers ---
// Operation extracts the operation name from an error.
// Returns empty string if the error is not an *Err.
func Operation(err error) string {
var e *Err
if As(err, &e) {
return e.Operation
}
return ""
}
// ErrorCode extracts the error code from an error.
// Returns empty string if the error is not an *Err or has no code.
func ErrorCode(err error) string {
var e *Err
if As(err, &e) {
return e.Code
}
return ""
}
// Message extracts the message from an error.
// Returns the error's Error() string if not an *Err.
func ErrorMessage(err error) string {
if err == nil {
return ""
}
var e *Err
if As(err, &e) {
return e.Message
}
return err.Error()
}
// Root returns the root cause of an error chain.
// Unwraps until no more wrapped errors are found.
func Root(err error) error {
if err == nil {
return nil
}
for {
unwrapped := errors.Unwrap(err)
if unwrapped == nil {
return err
}
err = unwrapped
}
}
// AllOperations returns an iterator over all operational contexts in the error chain.
// It traverses the error tree using errors.Unwrap.
func AllOperations(err error) iter.Seq[string] {
return func(yield func(string) bool) {
for err != nil {
if e, ok := err.(*Err); ok {
if e.Operation != "" {
if !yield(e.Operation) {
return
}
}
}
err = errors.Unwrap(err)
}
}
}
// StackTrace returns the logical stack trace (chain of operations) from an error.
// It returns an empty slice if no operational context is found.
func StackTrace(err error) []string {
var stack []string
for op := range AllOperations(err) {
stack = append(stack, op)
}
return stack
}
// FormatStackTrace returns a pretty-printed logical stack trace.
func FormatStackTrace(err error) string {
var ops []string
for op := range AllOperations(err) {
ops = append(ops, op)
}
if len(ops) == 0 {
return ""
}
return Join(" -> ", ops...)
}
// --- ErrorLog: Log-and-Return Error Helpers ---
// ErrorLog combines error creation with logging.
// Primary action: return an error. Secondary: log it.
type ErrorLog struct {
log *Log
}
func (el *ErrorLog) logger() *Log {
if el.log != nil {
return el.log
}
return Default()
}
// Error logs at Error level and returns a Result with the wrapped error.
func (el *ErrorLog) Error(err error, op, msg string) Result {
if err == nil {
return Result{OK: true}
}
wrapped := Wrap(err, op, msg)
el.logger().Error(msg, "op", op, "err", err)
return Result{wrapped, false}
}
// Warn logs at Warn level and returns a Result with the wrapped error.
func (el *ErrorLog) Warn(err error, op, msg string) Result {
if err == nil {
return Result{OK: true}
}
wrapped := Wrap(err, op, msg)
el.logger().Warn(msg, "op", op, "err", err)
return Result{wrapped, false}
}
// Must logs and panics if err is not nil.
func (el *ErrorLog) Must(err error, op, msg string) {
if err != nil {
el.logger().Error(msg, "op", op, "err", err)
panic(Wrap(err, op, msg))
}
}
// --- Crash Recovery & Reporting ---
// CrashReport represents a single crash event.
type CrashReport struct {
Timestamp time.Time `json:"timestamp"`
Error string `json:"error"`
Stack string `json:"stack"`
System CrashSystem `json:"system,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
// CrashSystem holds system information at crash time.
type CrashSystem struct {
OperatingSystem string `json:"operatingsystem"`
Architecture string `json:"architecture"`
Version string `json:"go_version"`
}
// ErrorPanic manages panic recovery and crash reporting.
type ErrorPanic struct {
filePath string
meta map[string]string
onCrash func(CrashReport)
}
// Recover captures a panic and creates a crash report.
// Use as: defer c.Error().Recover()
func (h *ErrorPanic) Recover() {
if h == nil {
return
}
r := recover()
if r == nil {
return
}
err, ok := r.(error)
if !ok {
err = NewError(Sprint("panic: ", r))
}
report := CrashReport{
Timestamp: time.Now(),
Error: err.Error(),
Stack: string(debug.Stack()),
System: CrashSystem{
OperatingSystem: runtime.GOOS,
Architecture: runtime.GOARCH,
Version: runtime.Version(),
},
Meta: maps.Clone(h.meta),
}
if h.onCrash != nil {
h.onCrash(report)
}
if h.filePath != "" {
h.appendReport(report)
}
}
// SafeGo runs a function in a goroutine with panic recovery.
func (h *ErrorPanic) SafeGo(fn func()) {
go func() {
defer h.Recover()
fn()
}()
}
// Reports returns the last n crash reports from the file.
func (h *ErrorPanic) Reports(n int) Result {
if h.filePath == "" {
return Result{}
}
crashMu.Lock()
defer crashMu.Unlock()
data, err := os.ReadFile(h.filePath)
if err != nil {
return Result{err, false}
}
var reports []CrashReport
if err := json.Unmarshal(data, &reports); err != nil {
return Result{err, false}
}
if n <= 0 || len(reports) <= n {
return Result{reports, true}
}
return Result{reports[len(reports)-n:], true}
}
var crashMu sync.Mutex
func (h *ErrorPanic) appendReport(report CrashReport) {
crashMu.Lock()
defer crashMu.Unlock()
var reports []CrashReport
if data, err := os.ReadFile(h.filePath); err == nil {
if err := json.Unmarshal(data, &reports); err != nil {
reports = nil
}
}
reports = append(reports, report)
data, err := json.MarshalIndent(reports, "", " ")
if err != nil {
Default().Error(Concat("crash report marshal failed: ", err.Error()))
return
}
if err := os.MkdirAll(filepath.Dir(h.filePath), 0755); err != nil {
Default().Error(Concat("crash report dir failed: ", err.Error()))
return
}
if err := os.WriteFile(h.filePath, data, 0600); err != nil {
Default().Error(Concat("crash report write failed: ", err.Error()))
}
}