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) } }