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>
187 lines
4.4 KiB
Go
187 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|