package handler import ( "context" "log/slog" "sync" "time" ) // LogEntry is one captured slog record, in a shape suitable for JSON. type LogEntry struct { Time time.Time `json:"time"` Level string `json:"level"` Message string `json:"message"` Attrs map[string]any `json:"attrs,omitempty"` } // LogRing is a fixed-size circular buffer of recent slog records, populated // by RingHandler. Snapshot returns a copy in chronological order (oldest → // newest); the buffer is never blocking. type LogRing struct { mu sync.Mutex entries []LogEntry size int next int // index of the slot the next record will be written into count int // number of entries currently held (≤ size) } // NewLogRing creates a ring with capacity n. Panics on n ≤ 0 — that's a // programming error, not a runtime condition. func NewLogRing(n int) *LogRing { if n <= 0 { panic("logring: capacity must be > 0") } return &LogRing{ entries: make([]LogEntry, n), size: n, } } func (r *LogRing) push(e LogEntry) { r.mu.Lock() defer r.mu.Unlock() r.entries[r.next] = e r.next = (r.next + 1) % r.size if r.count < r.size { r.count++ } } // Snapshot returns the current contents in chronological order. func (r *LogRing) Snapshot() []LogEntry { r.mu.Lock() defer r.mu.Unlock() out := make([]LogEntry, r.count) if r.count < r.size { copy(out, r.entries[:r.count]) return out } // Buffer full and wrapping: oldest is at r.next, newest at r.next-1. copy(out, r.entries[r.next:]) copy(out[r.size-r.next:], r.entries[:r.next]) return out } // RingHandler is a slog.Handler that pushes records into a LogRing. type RingHandler struct { ring *LogRing level slog.Leveler attrs []slog.Attr group string } // NewRingHandler returns a handler that records into ring at level and above. func NewRingHandler(ring *LogRing, level slog.Leveler) *RingHandler { return &RingHandler{ring: ring, level: level} } func (h *RingHandler) Enabled(_ context.Context, l slog.Level) bool { min := slog.LevelInfo if h.level != nil { min = h.level.Level() } return l >= min } func (h *RingHandler) Handle(_ context.Context, r slog.Record) error { attrs := make(map[string]any, r.NumAttrs()+len(h.attrs)) for _, a := range h.attrs { attrs[a.Key] = a.Value.Any() } r.Attrs(func(a slog.Attr) bool { attrs[a.Key] = a.Value.Any() return true }) if len(attrs) == 0 { attrs = nil } h.ring.push(LogEntry{ Time: r.Time, Level: r.Level.String(), Message: r.Message, Attrs: attrs, }) return nil } func (h *RingHandler) WithAttrs(attrs []slog.Attr) slog.Handler { merged := make([]slog.Attr, 0, len(h.attrs)+len(attrs)) merged = append(merged, h.attrs...) merged = append(merged, attrs...) return &RingHandler{ring: h.ring, level: h.level, attrs: merged, group: h.group} } func (h *RingHandler) WithGroup(name string) slog.Handler { // We don't render groups specially — just track the deepest one for // debugging context. Most zddc-server logging is flat. return &RingHandler{ring: h.ring, level: h.level, attrs: h.attrs, group: name} } // MultiHandler fans out each Handle call to every wrapped handler. Used to // tee slog output to both stderr (the existing TextHandler) and the in-memory // ring buffer that backs the admin /.admin/logs endpoint. type MultiHandler struct { handlers []slog.Handler } // NewMultiHandler returns a handler that broadcasts to all of hs. func NewMultiHandler(hs ...slog.Handler) *MultiHandler { return &MultiHandler{handlers: hs} } func (m *MultiHandler) Enabled(ctx context.Context, l slog.Level) bool { for _, h := range m.handlers { if h.Enabled(ctx, l) { return true } } return false } func (m *MultiHandler) Handle(ctx context.Context, r slog.Record) error { var firstErr error for _, h := range m.handlers { if !h.Enabled(ctx, r.Level) { continue } if err := h.Handle(ctx, r.Clone()); err != nil && firstErr == nil { firstErr = err } } return firstErr } func (m *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler { out := make([]slog.Handler, len(m.handlers)) for i, h := range m.handlers { out[i] = h.WithAttrs(attrs) } return &MultiHandler{handlers: out} } func (m *MultiHandler) WithGroup(name string) slog.Handler { out := make([]slog.Handler, len(m.handlers)) for i, h := range m.handlers { out[i] = h.WithGroup(name) } return &MultiHandler{handlers: out} }