package logger import ( "context" "log/slog" "sync" "time" ) // DefaultRingCapacity is how many recent log records the observability ring // buffer retains before the oldest is evicted. Compile-time constant. const DefaultRingCapacity = 2000 // Entry is one normalized log record as the admin observability log tail serves // it: {time, level, source, msg}. Time is the record's own timestamp; Source is // the provenance tag ("module/subfeature") when the record carried one. type Entry struct { Time time.Time Level string Source string Msg string } // RingBuffer is a concurrency-safe bounded buffer of recent log entries. When // full the oldest entry is overwritten, so it never grows past its capacity and // Append never blocks or fails. It is the storage behind // GET /api/v1/admin/observability/logs. type RingBuffer struct { mu sync.RWMutex buf []Entry next int full bool cap int } // NewRingBuffer builds a ring buffer holding up to capacity entries, falling // back to DefaultRingCapacity when capacity is not positive. func NewRingBuffer(capacity int) *RingBuffer { if capacity <= 0 { capacity = DefaultRingCapacity } return &RingBuffer{buf: make([]Entry, capacity), cap: capacity} } // Append stores e, overwriting the oldest entry when the buffer is full. func (r *RingBuffer) Append(e Entry) { if r == nil { return } r.mu.Lock() defer r.mu.Unlock() r.buf[r.next] = e r.next = (r.next + 1) % r.cap if r.next == 0 { r.full = true } } // Len returns how many entries are currently retained (at most the capacity). func (r *RingBuffer) Len() int { if r == nil { return 0 } r.mu.RLock() defer r.mu.RUnlock() if r.full { return r.cap } return r.next } // Snapshot returns up to limit most-recent entries, newest first. A non-positive // limit returns every retained entry. func (r *RingBuffer) Snapshot(limit int) []Entry { if r == nil { return nil } r.mu.RLock() defer r.mu.RUnlock() count := r.next if r.full { count = r.cap } if limit > 0 && limit < count { count = limit } out := make([]Entry, 0, count) for i := 0; i < count; i++ { idx := (r.next - 1 - i + r.cap*2) % r.cap out = append(out, r.buf[idx]) } return out } // ringHandler wraps another slog.Handler: every record the inner handler emits // is also normalized into buf. The inner handler's own output (console/file) is // untouched, so installing the ring changes nothing downstream. type ringHandler struct { inner slog.Handler buf *RingBuffer attrs []slog.Attr } // newRingHandler wraps inner so emitted records are captured into buf. A nil // buffer returns inner unchanged. func newRingHandler(inner slog.Handler, buf *RingBuffer) slog.Handler { if buf == nil { return inner } return &ringHandler{inner: inner, buf: buf} } // Enabled delegates to the inner handler: the ring captures exactly the records // the configured level lets through, never more. func (h *ringHandler) Enabled(ctx context.Context, level slog.Level) bool { return h.inner.Enabled(ctx, level) } // Handle captures rec into the ring buffer and forwards it to the inner handler. func (h *ringHandler) Handle(ctx context.Context, rec slog.Record) error { h.buf.Append(Entry{ Time: rec.Time, Level: levelName(rec.Level), Source: h.sourceOf(rec), Msg: rec.Message, }) return h.inner.Handle(ctx, rec) } // WithAttrs keeps the captured attributes in sync with the child logger, so a // source tagged with logger.WithSource is visible to Handle. func (h *ringHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if len(attrs) == 0 { return h } return &ringHandler{ inner: h.inner.WithAttrs(attrs), buf: h.buf, attrs: append(append([]slog.Attr{}, h.attrs...), attrs...), } } // WithGroup keeps the group nesting in sync with the child logger. func (h *ringHandler) WithGroup(name string) slog.Handler { if name == "" { return h } return &ringHandler{inner: h.inner.WithGroup(name), buf: h.buf, attrs: h.attrs} } // sourceOf resolves a record's provenance: the "source" attribute tagged by // logger.WithSource when present, else the "module" (+"/"+"sub") pair tagged by // logger.Scoped, else "". func (h *ringHandler) sourceOf(rec slog.Record) string { attrs := make([]slog.Attr, 0, len(h.attrs)+rec.NumAttrs()) attrs = append(attrs, h.attrs...) rec.Attrs(func(a slog.Attr) bool { attrs = append(attrs, a) return true }) if v, ok := attrValue(attrs, "source"); ok { return v } module, ok := attrValue(attrs, "module") if !ok { return "" } if sub, ok := attrValue(attrs, "sub"); ok && sub != "" { return module + "/" + sub } return module } // attrValue returns the value of key in attrs, recursing into groups. func attrValue(attrs []slog.Attr, key string) (string, bool) { for _, a := range attrs { if a.Value.Kind() == slog.KindGroup { if v, ok := attrValue(a.Value.Group(), key); ok { return v, true } continue } if a.Key == key { return a.Value.String(), true } } return "", false } // levelName maps a slog level to the wire name used by the log tail. func levelName(l slog.Level) string { switch { case l >= slog.LevelError: return "error" case l >= slog.LevelWarn: return "warn" case l >= slog.LevelInfo: return "info" default: return "debug" } }