Documentation
¶
Overview ¶
Package slogbox provides a slog.Handler that keeps the last N log records in a circular buffer. It is designed for exposing recent logs via health check or admin HTTP endpoints.
When FlushOn/FlushTo are configured, records are automatically forwarded on level-triggered thresholds. Handler.Flush provides explicit draining for graceful shutdown. Handler.TotalRecords and Handler.PendingFlushCount expose buffer throughput for monitoring.
Flushed records are replayed with the caller-provided context (for explicit Handler.Flush) or context.Background (for level-triggered flushes). The original request context is intentionally not stored per record to avoid GC pressure from retained context chains and stale deadlines.
Index ¶
- func HTTPHandler(h *Handler, onErr func(http.ResponseWriter, *http.Request, error)) http.Handler
- type Handler
- func (h *Handler) All() iter.Seq[slog.Record]
- func (h *Handler) Capacity() int
- func (h *Handler) Clear()
- func (h *Handler) Enabled(_ context.Context, level slog.Level) bool
- func (h *Handler) Flush(ctx context.Context) error
- func (h *Handler) Handle(_ context.Context, r slog.Record) error
- func (h *Handler) JSON() ([]byte, error)
- func (h *Handler) Len() int
- func (h *Handler) PendingFlushCount() int
- func (h *Handler) Records() []slog.Record
- func (h *Handler) RecordsAbove(minLevel slog.Level) []slog.Record
- func (h *Handler) TotalRecords() uint64
- func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler
- func (h *Handler) WithGroup(name string) slog.Handler
- func (h *Handler) WriteTo(w io.Writer) (int64, error)
- type Options
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func HTTPHandler ¶
HTTPHandler returns an http.Handler that serves the buffered records as a JSON array. It sets Content-Type to application/json and calls Handler.WriteTo to stream the response. An empty buffer produces a 200 response with "[]".
onErr is called when WriteTo returns an error. If onErr is nil and no response bytes have been written yet, the handler replies with 500 Internal Server Error. When bytes have already been sent (headers committed), the status code cannot be changed.
Example ¶
package main
import (
"fmt"
"log/slog"
"net/http"
"github.com/alexrios/slogbox"
)
func main() {
h := slogbox.New(100, nil)
logger := slog.New(h)
logger.Info("request handled", "status", 200)
http.Handle("/debug/logs", slogbox.HTTPHandler(h, nil))
fmt.Println("handler registered")
}
Output: handler registered
Types ¶
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler is a slog.Handler that stores log records in a fixed-size ring buffer. Handlers returned by Handler.WithAttrs and Handler.WithGroup share the same underlying buffer.
func New ¶
New creates a Handler with the given buffer capacity. It panics if size < 1.
Example ¶
package main
import (
"fmt"
"log/slog"
"github.com/alexrios/slogbox"
)
func main() {
h := slogbox.New(100, nil)
logger := slog.New(h)
logger.Info("server started", "port", 8080)
logger.Warn("high latency", "ms", 250)
fmt.Println(h.Len())
}
Output: 2
func (*Handler) All ¶
All returns an iterator over stored records from oldest to newest. The snapshot is taken under a read lock; the iteration itself holds no lock. If MaxAge is set, records older than MaxAge are excluded.
Example ¶
package main
import (
"fmt"
"log/slog"
"github.com/alexrios/slogbox"
)
func main() {
h := slogbox.New(10, nil)
logger := slog.New(h)
logger.Info("alpha")
logger.Info("beta")
for r := range h.All() {
fmt.Println(r.Message)
}
}
Output: alpha beta
func (*Handler) Clear ¶
func (h *Handler) Clear()
Clear removes all records from the buffer and resets flush state. It does not wait for any in-flight flush: a concurrent Handle that has already claimed its flush window will still deliver those records to FlushTo after Clear returns. New records written after Clear form a fresh window.
func (*Handler) Flush ¶
Flush explicitly drains pending records to [Options.FlushTo]. It is intended for graceful shutdown sequences where buffered records would otherwise be lost.
Flush only requires [Options.FlushTo] to be set; [Options.FlushOn] is not needed. This allows a manual-only flush pattern where records are never flushed automatically but can be drained on demand.
Flush is a no-op (returns nil) when FlushTo is not configured, when the buffer is empty, or when all records have already been flushed.
The flush window is claimed under the write lock before delivery begins (same at-most-once semantics as Handler.Handle). The provided context is passed to each FlushTo.Handle call; ctx.Err() is checked between records to support early cancellation.
On error (from FlushTo or context cancellation), Flush returns immediately. Records delivered before the error are not retried; records not yet delivered are lost because the flush window was already claimed.
Example ¶
package main
import (
"context"
"fmt"
"io"
"log/slog"
"time"
"github.com/alexrios/slogbox"
)
func main() {
target := slog.NewJSONHandler(io.Discard, nil)
h := slogbox.New(100, &slogbox.Options{
FlushTo: target,
})
logger := slog.New(h)
logger.Info("buffered event")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := h.Flush(ctx); err != nil {
fmt.Println("flush error:", err)
}
fmt.Println("pending after flush:", h.PendingFlushCount())
}
Output: pending after flush: 0
func (*Handler) Handle ¶
Handle stores a clone of r in the ring buffer. If FlushOn/FlushTo are configured and the record's level reaches the FlushOn threshold, all records since the last flush are forwarded to FlushTo. If FlushTo.Handle returns an error, Handle returns that error to the caller. The flush window is claimed before flushing begins (at-most-once semantics), so claimed records are never re-sent even when an error is returned.
func (*Handler) JSON ¶
JSON returns the buffered records as a JSON array suitable for HTTP responses. If MaxAge is set, records older than MaxAge are excluded.
Example ¶
package main
import (
"fmt"
"log/slog"
"github.com/alexrios/slogbox"
)
func main() {
h := slogbox.New(10, &slogbox.Options{Level: slog.LevelError})
logger := slog.New(h)
logger.Info("ignored") // below Error level
logger.Error("failure", "code", 500)
data, err := h.JSON()
if err != nil {
panic(err)
}
fmt.Println(h.Len())
fmt.Println(len(data) > 0)
}
Output: 1 true
func (*Handler) Len ¶
Len returns the number of records physically stored in the buffer. It does not apply MaxAge filtering.
func (*Handler) PendingFlushCount ¶
PendingFlushCount returns the number of records that would be flushed by Handler.Flush or the next level-triggered flush. Returns 0 if [Options.FlushTo] is not set.
func (*Handler) Records ¶
Records returns a snapshot of stored records from oldest to newest. If MaxAge is set, records older than MaxAge are excluded.
Example ¶
package main
import (
"fmt"
"log/slog"
"github.com/alexrios/slogbox"
)
func main() {
h := slogbox.New(10, nil)
logger := slog.New(h)
logger.Info("first")
logger.Info("second")
for _, r := range h.Records() {
fmt.Println(r.Message)
}
}
Output: first second
func (*Handler) RecordsAbove ¶
RecordsAbove returns a snapshot of stored records whose level is >= minLevel, from oldest to newest. If MaxAge is set, old records are excluded before level filtering. The semantics match Handler.Enabled: a record is included when its level reaches or exceeds minLevel.
func (*Handler) TotalRecords ¶
TotalRecords returns the total number of records ever written to the buffer. The counter is monotonically increasing and survives wrap-around; it is only reset by Handler.Clear.
func (*Handler) WithAttrs ¶
WithAttrs returns a new Handler whose records will include the given attrs. The new handler shares the same ring buffer.
func (*Handler) WithGroup ¶
WithGroup returns a new Handler that qualifies later attrs with the given group name. The new handler shares the same ring buffer.
func (*Handler) WriteTo ¶
WriteTo writes the buffered records as a JSON array to w. It implements io.WriterTo so it can be passed directly to helpers that accept that interface, and can write directly to an http.ResponseWriter. If MaxAge is set, records older than MaxAge are excluded.
Example ¶
package main
import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"github.com/alexrios/slogbox"
)
func main() {
h := slogbox.New(10, nil)
logger := slog.New(h)
logger.Info("first")
logger.Info("second")
var buf bytes.Buffer
if _, err := h.WriteTo(&buf); err != nil {
panic(err)
}
var entries []map[string]any
if err := json.Unmarshal(buf.Bytes(), &entries); err != nil {
panic(err)
}
for _, e := range entries {
fmt.Println(e["msg"])
}
}
Output: first second
type Options ¶
type Options struct {
// Level reports the minimum record level that will be stored.
// The handler discards records with lower levels.
// If Level is nil, the handler assumes LevelInfo.
Level slog.Leveler
// FlushOn sets the level threshold that triggers an automatic flush of
// buffered records to FlushTo. Both FlushOn and FlushTo must be set for
// level-triggered flush to be active.
// Flush errors are returned by Handle. Records are claimed before flushing
// begins: at-most-once delivery — a record is never re-sent even if
// FlushTo.Handle returns an error.
FlushOn slog.Leveler
// FlushTo is the destination handler for flushed records.
// Required for both level-triggered flush (with FlushOn) and explicit
// [Handler.Flush] calls. Setting FlushTo without FlushOn enables a
// manual-only pattern where records are never flushed automatically.
//
// FlushTo must not directly or indirectly log back to the same slogbox Handler;
// doing so will deadlock.
FlushTo slog.Handler
// MaxAge excludes records older than this duration from read operations
// (Records, All, JSON, WriteTo). Zero means no age filtering.
// Negative values cause New to panic.
// Len returns the physical count regardless of MaxAge.
//
// MaxAge assumes records are stored in chronological order (non-decreasing
// timestamps). If Handle is called with out-of-order timestamps, MaxAge
// filtering may return incorrect results.
MaxAge time.Duration
}
Options configure a Handler.
