Files
minstrel/internal/logging/recent.go
T
bvandeusenandClaude Opus 5.5 d191a28217 docs: server comments say what the code does, not how it came to be (#6277)
Comment blocks in cmd/minstrel and 30 smaller internal packages, rewritten
to rule 32's standard with each citation read in context: task, milestone
and rule ids go, and so do the stories they were attached to ("used to",
"Before #N", measurements from one install, quotes, pointers to Fabled
Librarian's records and to docs/superpowers files that are not in the
repo). Where a story wrapped a real constraint, the constraint stays as a
present-tense fact.

Comments that had gone stale are corrected:
- taste.Config: the tuned values are overlaid by internal/recsettings;
  it is no longer "a later follow-up".
- StorageConfig: the data dir holds art, caches and the stream secret,
  not only playlist covers.
- the Subsonic album list and getUser comments describe what they do
  today rather than a milestone that would change it.

Checked: the non-comment token stream of every changed file matches
HEAD, and gofmt -l is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:15:05 -04:00

123 lines
3.1 KiB
Go

package logging
import (
"context"
"fmt"
"log/slog"
"strings"
"sync"
"time"
)
// The server's recent errors, kept in memory for the admin Overview:
// what has gone wrong since the process started, without reading the
// container's logs. Only ERROR and above, and only the newest few; the full
// record is still the log stream.
// RecentErrorsKept is how many recent errors are kept.
const RecentErrorsKept = 50
// LoggedError is one error the server logged.
type LoggedError struct {
At time.Time
Message string
// Attrs is the record's attributes as key=value, the way the text log
// writes them.
Attrs string
}
// ErrorRing keeps the newest errors and counts all of them.
type ErrorRing struct {
mu sync.Mutex
buf []LoggedError
next int
total int64
}
// Errors is the process's ring, filled by every logger New returns.
var Errors = NewErrorRing(RecentErrorsKept)
// NewErrorRing returns a ring that keeps the newest n errors.
func NewErrorRing(n int) *ErrorRing {
return &ErrorRing{buf: make([]LoggedError, 0, n)}
}
func (r *ErrorRing) add(e LoggedError) {
r.mu.Lock()
defer r.mu.Unlock()
r.total++
if len(r.buf) < cap(r.buf) {
r.buf = append(r.buf, e)
return
}
r.buf[r.next] = e
r.next = (r.next + 1) % len(r.buf)
}
// Snapshot returns the kept errors, newest first, and how many have been
// logged in all since the process started.
func (r *ErrorRing) Snapshot() ([]LoggedError, int64) {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]LoggedError, 0, len(r.buf))
for i := len(r.buf) - 1; i >= 0; i-- {
out = append(out, r.buf[(r.next+i)%len(r.buf)])
}
return out, r.total
}
// ringHandler passes every record on, and copies the errors into a ring.
type ringHandler struct {
inner slog.Handler
ring *ErrorRing
// attrs are the logger's own attributes (component=… and the like),
// already rendered, group-qualified.
attrs []string
groups []string
}
func newRingHandler(inner slog.Handler, ring *ErrorRing) *ringHandler {
return &ringHandler{inner: inner, ring: ring}
}
func (h *ringHandler) Enabled(ctx context.Context, l slog.Level) bool {
return h.inner.Enabled(ctx, l)
}
func (h *ringHandler) Handle(ctx context.Context, rec slog.Record) error {
if rec.Level >= slog.LevelError {
parts := append([]string(nil), h.attrs...)
rec.Attrs(func(a slog.Attr) bool {
parts = append(parts, h.render(a))
return true
})
h.ring.add(LoggedError{At: rec.Time, Message: rec.Message, Attrs: strings.Join(parts, " ")})
}
return h.inner.Handle(ctx, rec)
}
func (h *ringHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
next := *h
next.inner = h.inner.WithAttrs(attrs)
next.attrs = append([]string(nil), h.attrs...)
for _, a := range attrs {
next.attrs = append(next.attrs, h.render(a))
}
return &next
}
func (h *ringHandler) WithGroup(name string) slog.Handler {
next := *h
next.inner = h.inner.WithGroup(name)
next.groups = append(append([]string(nil), h.groups...), name)
return &next
}
func (h *ringHandler) render(a slog.Attr) string {
key := a.Key
if len(h.groups) > 0 {
key = strings.Join(h.groups, ".") + "." + key
}
return fmt.Sprintf("%s=%v", key, a.Value.Resolve())
}