internal/report: report errors with persistent backing
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m48s
Test / ShareFS (push) Successful in 3m47s
Test / Hakurei (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 5m30s
Test / Hakurei (race detector) (push) Successful in 6m37s
Test / Flake checks (push) Successful in 1m20s
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m48s
Test / ShareFS (push) Successful in 3m47s
Test / Hakurei (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 5m30s
Test / Hakurei (race detector) (push) Successful in 6m37s
Test / Flake checks (push) Successful in 1m20s
This is useful for reporting errors from processes that must never terminate. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
186
internal/report/report.go
Normal file
186
internal/report/report.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
)
|
||||
|
||||
const (
|
||||
// Inconsistent denotes an error diagnosed due to inconsistent state.
|
||||
Inconsistent = iota
|
||||
// Degraded denotes an error condition causing a degraded state.
|
||||
Degraded
|
||||
// Fatal denotes an unrecoverable error. This is generally followed by
|
||||
// notifying the user and suggesting immediate restart, or an automatic
|
||||
// restart of a nonessential daemon process.
|
||||
Fatal
|
||||
)
|
||||
|
||||
// A Reporter represents an error reporting object backed by user-facing display
|
||||
// and optional persistent storage. A Reporter can be used simultaneously from
|
||||
// multiple goroutines.
|
||||
type Reporter struct {
|
||||
// Backing logger, the zero value implies the return value of [log.Default].
|
||||
log atomic.Pointer[log.Logger]
|
||||
// Backing persistent storage.
|
||||
pathname atomic.Pointer[check.Absolute]
|
||||
// Reporting behaviour.
|
||||
flag atomic.Uint64
|
||||
|
||||
// Caller-supplied reporting functions.
|
||||
notify []func(e Error)
|
||||
// Synchronises access to notify.
|
||||
notifyMu sync.RWMutex
|
||||
|
||||
// Synchronises access to persistent storage.
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// SetOutput sets the backing [log.Logger].
|
||||
func (r *Reporter) SetOutput(l *log.Logger) { r.log.Store(l) }
|
||||
|
||||
// SetPathname sets the pathname to persistent storage.
|
||||
func (r *Reporter) SetPathname(a *check.Absolute) { r.pathname.Store(a) }
|
||||
|
||||
const (
|
||||
// DStrict causes all dispatches to end in a call to panic.
|
||||
DStrict = 1 << iota
|
||||
// DNoRecover disallows recovering from error write to persistent storage.
|
||||
DNoRecover
|
||||
// DBypassEarly allows persistent storage to be skipped before it is ready.
|
||||
DBypassEarly
|
||||
)
|
||||
|
||||
// SetFlags sets flags for r.
|
||||
func (r *Reporter) SetFlags(flag uint64) { r.flag.Store(flag) }
|
||||
|
||||
// An Error represents an error reported via [Reporter.Dispatch].
|
||||
type Error struct {
|
||||
// Nature of error.
|
||||
Severity int
|
||||
// User-facing reporting message.
|
||||
Message string
|
||||
// Underlying error.
|
||||
Err error
|
||||
}
|
||||
|
||||
// RepresentableError is implemented by errors friendly to JSON serialisation.
|
||||
type RepresentableError interface {
|
||||
error
|
||||
Representable()
|
||||
}
|
||||
|
||||
// MarshalJSON returns an incomplete JSON representation of e.
|
||||
func (e Error) MarshalJSON() (data []byte, err error) {
|
||||
v := struct {
|
||||
Severity any `json:"kind"`
|
||||
Message string `json:"message"`
|
||||
Err json.RawMessage `json:"error"`
|
||||
}{Message: e.Message}
|
||||
|
||||
switch e.Severity {
|
||||
case Inconsistent:
|
||||
v.Severity = "inconsistent"
|
||||
case Degraded:
|
||||
v.Severity = "degradation"
|
||||
case Fatal:
|
||||
v.Severity = "fatal"
|
||||
default:
|
||||
v.Severity = e.Severity
|
||||
}
|
||||
|
||||
var re RepresentableError
|
||||
if errors.As(e.Err, &re) {
|
||||
v.Err, err = json.Marshal(re)
|
||||
} else {
|
||||
v.Err, err = json.Marshal(e.Err.Error())
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return json.Marshal(&v)
|
||||
}
|
||||
|
||||
// Error is generally only called during a [Reporter.Dispatch] call ending in
|
||||
// a panic or an otherwise unrecoverable condition.
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("%s: %v", e.Message, e.Err)
|
||||
}
|
||||
|
||||
// Notify arranges for f to be called for all future dispatched errors.
|
||||
func (r *Reporter) Notify(f func(Error)) {
|
||||
r.notifyMu.Lock()
|
||||
r.notify = append(r.notify, f)
|
||||
r.notifyMu.Unlock()
|
||||
}
|
||||
|
||||
// getLog returns the address of the underlying [log.Logger], or the return
|
||||
// value of [log.Default].
|
||||
func (r *Reporter) getLog() *log.Logger {
|
||||
l := r.log.Load()
|
||||
if l == nil {
|
||||
l = log.Default()
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// dispatch implements Dispatch but returns the reporting error.
|
||||
func (r *Reporter) dispatch(severity int, message string, e error) (err error) {
|
||||
p := Error{severity, message, e}
|
||||
r.getLog().Println(message+":", e)
|
||||
|
||||
flag := r.flag.Load()
|
||||
defer func() {
|
||||
if flag&DNoRecover != 0 && err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if a := r.pathname.Load(); a != nil {
|
||||
var w *os.File
|
||||
r.mu.Lock()
|
||||
w, err = os.OpenFile(
|
||||
a.Append(strconv.FormatUint(uint64(time.Now().UnixNano()), 10)).String(),
|
||||
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
|
||||
0400,
|
||||
)
|
||||
if err != nil {
|
||||
r.mu.Unlock()
|
||||
return
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(p)
|
||||
if _err := w.Close(); err == nil {
|
||||
err = _err
|
||||
}
|
||||
r.mu.Unlock()
|
||||
} else if flag&DBypassEarly == 0 {
|
||||
panic(p)
|
||||
}
|
||||
|
||||
if flag&DStrict != 0 {
|
||||
panic(p)
|
||||
}
|
||||
|
||||
r.notifyMu.RLock()
|
||||
defer r.notifyMu.RUnlock()
|
||||
for _, f := range r.notify {
|
||||
f(p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Dispatch reports an error and saves it to backing storage if available.
|
||||
func (r *Reporter) Dispatch(severity int, message string, e error) {
|
||||
if err := r.dispatch(severity, message, e); err != nil {
|
||||
r.getLog().Println(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user