logflightrecorder

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 16, 2026 License: GPL-3.0 Imports: 9 Imported by: 0

README

logflightrecorder

CI Go Reference Go Report Card

A slog.Handler that keeps the last N log records in a fixed-size circular buffer. Zero external dependencies -- stdlib only.

Primary use case: exposing recent logs via health-check or admin HTTP endpoints. Inspired by runtime/trace.FlightRecorder, it can also act as a black box recorder that flushes context-rich logs on error.

Install

go get github.com/alexrios/logflightrecorder

Quick start

package main

import (
	"log/slog"
	"net/http"

	lfr "github.com/alexrios/logflightrecorder"
)

func main() {
	rec := lfr.New(500, nil)
	logger := slog.New(rec)
	slog.SetDefault(logger)

	http.HandleFunc("GET /debug/logs", func(w http.ResponseWriter, r *http.Request) {
		data, err := rec.JSON()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		w.Write(data)
	})

	slog.Info("server starting", "port", 8080)
	http.ListenAndServe(":8080", nil)
}

API overview

Function / Method Description
New(size, opts) Create a handler with buffer capacity size
Handle(ctx, record) Store a record (implements slog.Handler); triggers flush if FlushOn threshold is met
WithAttrs(attrs) Return a handler with additional attributes (shared buffer)
WithGroup(name) Return a handler with a group prefix (shared buffer)
Records() Snapshot of stored records, oldest to newest (respects MaxAge)
All() iter.Seq[slog.Record] iterator over stored records (respects MaxAge)
JSON() Marshal records as a JSON array (respects MaxAge)
WriteTo(w) Stream records as JSON to an io.Writer (implements io.WriterTo)
Len() Number of records physically stored (ignores MaxAge)
Capacity() Total buffer capacity
Clear() Remove all records
Options
Field Type Description
Level slog.Leveler Minimum level stored (default: INFO)
FlushOn slog.Leveler Level that triggers flush to FlushTo
FlushTo slog.Handler Destination for flushed records
MaxAge time.Duration Exclude records older than this from reads; 0 = no filter

Black box pattern

Keep a ring buffer of recent logs and flush them to stderr when an error occurs:

rec := lfr.New(500, &lfr.Options{
	FlushOn: slog.LevelError,
	FlushTo: slog.NewJSONHandler(os.Stderr, nil),
	MaxAge:  5 * time.Minute,
})
logger := slog.New(rec)

logger.Info("request started", "path", "/api/users")
logger.Info("db query", "rows", 42)
// ... when an error happens, all recent logs are flushed to stderr
logger.Error("query failed", "err", err)

Serve the ring buffer over HTTP with WriteTo:

http.HandleFunc("GET /debug/logs", func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	rec.WriteTo(w) // implements io.WriterTo
})

Benchmarks

Median values via benchstat -count=10 on an Intel Core i9-14900K (32 threads):

Benchmark ns/op B/op allocs/op
Handle 90.60 0 0
Handle_Parallel 514 0 0
Handle_WithFlush 85.55 0 0
Handle_FlushTrigger 32,230 32,768 1
Records (1000) 285,000 294,912 1
All (1000) 314,700 294,912 1
JSON (100 records, 5 attrs) 441,900 140,698 1,804
WriteTo 456,900 140,595 1,804
WithAttrs (5 attrs) 328 288 2
WithGroup 49.21 16 1
Records_WithMaxAge 237,400 294,912 1

License

GPL-3.0

Documentation

Overview

Package logflightrecorder 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.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

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

func New(size int, opts *Options) *Handler

New creates a Handler with the given buffer capacity. It panics if size < 1.

Example
package main

import (
	"fmt"
	"log/slog"

	lfr "github.com/alexrios/logflightrecorder"
)

func main() {
	h := lfr.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

func (h *Handler) All() iter.Seq[slog.Record]

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.

Example
package main

import (
	"fmt"
	"log/slog"

	lfr "github.com/alexrios/logflightrecorder"
)

func main() {
	h := lfr.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) Capacity

func (h *Handler) Capacity() int

Capacity returns the total buffer capacity (the size passed to New).

func (*Handler) Clear

func (h *Handler) Clear()

Clear removes all records from the buffer.

func (*Handler) Enabled

func (h *Handler) Enabled(_ context.Context, level slog.Level) bool

Enabled reports whether the handler stores records at the given level.

func (*Handler) Handle

func (h *Handler) Handle(_ context.Context, r slog.Record) error

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.

func (*Handler) JSON

func (h *Handler) JSON() ([]byte, error)

JSON returns the buffered records as a JSON array suitable for HTTP responses.

Example
package main

import (
	"fmt"
	"log/slog"

	lfr "github.com/alexrios/logflightrecorder"
)

func main() {
	h := lfr.New(10, &lfr.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

func (h *Handler) Len() int

Len returns the number of records physically stored in the buffer. It does not apply MaxAge filtering.

func (*Handler) Records

func (h *Handler) Records() []slog.Record

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"

	lfr "github.com/alexrios/logflightrecorder"
)

func main() {
	h := lfr.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) WithAttrs

func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs returns a new Handler whose records will include the given attrs. The new handler shares the same ring buffer.

func (*Handler) WithGroup

func (h *Handler) WithGroup(name string) slog.Handler

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

func (h *Handler) WriteTo(w io.Writer) (int64, error)

WriteTo writes the buffered records as a JSON array to w. It implements io.WriterTo, making it composable with [http.ResponseWriter].

Example
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log/slog"

	lfr "github.com/alexrios/logflightrecorder"
)

func main() {
	h := lfr.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 a flush of buffered records
	// to FlushTo. Both FlushOn and FlushTo must be set for flush to be active.
	FlushOn slog.Leveler

	// FlushTo is the destination handler for flushed records.
	// Both FlushOn and FlushTo must be set for flush to be active.
	FlushTo slog.Handler

	// MaxAge excludes records older than this duration from read operations
	// (Records, All, JSON, WriteTo). Zero means no age filtering.
	// Len returns the physical count regardless of MaxAge.
	MaxAge time.Duration
}

Options configure a Handler.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL