package logging import ( "context" "fmt" "log/slog" "strings" "sync" "time" ) // The server's recent errors, kept in memory for the admin Overview (M504): // 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()) }