-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherror.go
More file actions
479 lines (432 loc) · 12.4 KB
/
error.go
File metadata and controls
479 lines (432 loc) · 12.4 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
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
// 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 (
"errors"
)
// ErrorSink is the shared interface for error reporting.
// Implemented by ErrorLog (structured logging) and ErrorPanic (panic recovery).
//
// var sink core.ErrorSink = core.Default()
// sink.Warn("homelab degraded", "service", "agent")
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.
//
// err := &core.Err{Operation: "config.Load", Message: "missing config.host", Code: "CONFIG_MISSING"}
// core.Println(err.Error())
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.
//
// err := &core.Err{Operation: "agent.Run", Message: "failed"}
// msg := err.Error()
// core.Println(msg)
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.
//
// cause := core.NewError("connection refused")
// err := &core.Err{Operation: "agent.Ping", Message: "homelab unreachable", Cause: cause}
// root := err.Unwrap()
// core.Println(root)
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.
//
// err := core.Wrap(core.EOF, "stream.Read", "agent stream closed")
// if core.Is(err, core.EOF) { core.Println("done") }
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.
//
// err := core.E("agent.Run", "failed", nil)
// var coreErr *core.Err
// if core.As(err, &coreErr) { core.Println(coreErr.Operation) }
func As(err error, target any) bool {
return errors.As(err, target)
}
// NewError creates a simple core.*Err carrying the given text.
// Returns *Err so introspection helpers (Operation/ErrorCode/
// ErrorMessage) work uniformly across NewError, NewCode, and E.
//
// err := core.NewError("config.host missing")
// core.Println(err.Error())
func NewError(text string) error {
return &Err{Message: 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.
//
// err := core.E("agent.Run", "failed", nil)
// op := core.Operation(err)
// core.Println(op)
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.
//
// err := core.WrapCode(nil, "RATE_LIMITED", "agent.Call", "too many requests")
// code := core.ErrorCode(err)
// core.Println(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.
//
// err := core.E("config.Load", "missing config.host", nil)
// msg := core.ErrorMessage(err)
// core.Println(msg)
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.
//
// cause := core.NewError("connection refused")
// err := core.Wrap(cause, "agent.Ping", "homelab unreachable")
// root := core.Root(err)
// core.Println(root)
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.
//
// err := core.Wrap(core.E("net.Dial", "refused", nil), "agent.Ping", "homelab unreachable")
// for op := range core.AllOperations(err) {
// core.Println(op)
// }
func AllOperations(err error) 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.
//
// err := core.Wrap(core.E("net.Dial", "refused", nil), "agent.Ping", "homelab unreachable")
// stack := core.StackTrace(err)
// core.Println(core.Join(" -> ", stack...))
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.
//
// err := core.Wrap(core.E("net.Dial", "refused", nil), "agent.Ping", "homelab unreachable")
// trace := core.FormatStackTrace(err)
// core.Println(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.
//
// c := core.New()
// r := c.LogError(core.NewError("offline"), "agent.Ping", "homelab unreachable")
// if !r.OK { return r }
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.
//
// err := &core.Err{Operation: "agent.Run", Message: "failed"}
// msg := err.Error()
// core.Println(msg)
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.
//
// c := core.New()
// r := c.Log().Warn(core.NewError("missing config.host"), "config.Load", "using default host")
// if !r.OK { return r }
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.
//
// c := core.New()
// c.Log().Must(nil, "agent.Start", "startup failed")
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.
//
// report := core.CrashReport{Error: "panic: agent failed", Meta: map[string]string{"service": "agent"}}
// core.Println(report.Error)
type CrashReport struct {
Timestamp 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.
//
// system := core.CrashSystem{OperatingSystem: "linux", Architecture: "amd64", Version: "go1.26"}
// core.Println(system.OperatingSystem)
type CrashSystem struct {
OperatingSystem string `json:"operatingsystem"`
Architecture string `json:"architecture"`
Version string `json:"go_version"`
}
// ErrorPanic manages panic recovery and crash reporting.
//
// c := core.New()
// defer c.Error().Recover()
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()
//
// c := core.New()
// 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: Now(),
Error: err.Error(),
Stack: string(StackBuf()),
System: CrashSystem{
OperatingSystem: OS(),
Architecture: Arch(),
Version: GoVersion(),
},
Meta: MapClone(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.
//
// c := core.New()
// c.Error().SafeGo(func() {
// core.Info("agent worker started")
// })
func (h *ErrorPanic) SafeGo(fn func()) {
go func() {
defer h.Recover()
fn()
}()
}
// Reports returns the last n crash reports from the file.
//
// c := core.New()
// r := c.Error().Reports(5)
// if r.OK { reports := r.Value.([]core.CrashReport); _ = reports }
func (h *ErrorPanic) Reports(n int) Result {
if h.filePath == "" {
return Result{}
}
crashMu.Lock()
defer crashMu.Unlock()
r := ReadFile(h.filePath)
if !r.OK {
return r
}
data := r.Value.([]byte)
var reports []CrashReport
if r := JSONUnmarshal(data, &reports); !r.OK {
return r
}
if n <= 0 || len(reports) <= n {
return Result{reports, true}
}
return Result{reports[len(reports)-n:], true}
}
var crashMu Mutex
func (h *ErrorPanic) appendReport(report CrashReport) {
crashMu.Lock()
defer crashMu.Unlock()
var reports []CrashReport
if read := ReadFile(h.filePath); read.OK {
if r := JSONUnmarshal(read.Value.([]byte), &reports); !r.OK {
reports = nil
}
}
reports = append(reports, report)
data := JSONMarshalIndent(reports, "", " ")
if !data.OK {
Default().Error(Concat("crash report marshal failed: ", data.Error()))
return
}
if r := MkdirAll(PathDir(h.filePath), 0755); !r.OK {
Default().Error(Concat("crash report dir failed: ", r.Error()))
return
}
if r := WriteFile(h.filePath, data.Value.([]byte), 0600); !r.OK {
Default().Error(Concat("crash report write failed: ", r.Error()))
}
}