buffer.go

v0.2.0
Doc Versions Source
1
package scribe
2
3
import (
4
	"context"
5
	"log/slog"
6
	"slices"
7
	"sync"
8
	"time"
9
)
10
11
// StoredRecord holds a captured log record for later retrieval.
12
type StoredRecord struct {
13
	Time    time.Time
14
	Level   slog.Level
15
	Message string
16
	Attrs   []slog.Attr
17
	PC      uintptr
18
	Groups  []string
19
}
20
21
// FromSlogRecord creates a StoredRecord from an slog.Record.
22
func FromSlogRecord(r slog.Record, groups []string) StoredRecord {
23
	attrs := make([]slog.Attr, 0, r.NumAttrs())
24
	r.Attrs(func(a slog.Attr) bool {
25
		attrs = append(attrs, a)
26
		return true
27
	})
28
29
	return StoredRecord{
30
		Time:    r.Time,
31
		Level:   r.Level,
32
		Message: r.Message,
33
		Attrs:   attrs,
34
		PC:      r.PC,
35
		Groups:  slices.Clone(groups),
36
	}
37
}
38
39
// ToSlogRecord converts back to an slog.Record (for replay).
40
func (s StoredRecord) ToSlogRecord() slog.Record {
41
	r := slog.NewRecord(s.Time, s.Level, s.Message, s.PC)
42
	r.AddAttrs(s.Attrs...)
43
	return r
44
}
45
46
// RingBuffer is a generic circular buffer.
47
type RingBuffer[T any] struct {
48
	mu       sync.RWMutex
49
	items    []T
50
	head     int // next write position
51
	size     int // current number of items
52
	capacity int
53
}
54
55
// NewRingBuffer creates a new ring buffer with the given capacity.
56
func NewRingBuffer[T any](capacity int) *RingBuffer[T] {
57
	if capacity <= 0 {
58
		capacity = 1000
59
	}
60
	return &RingBuffer[T]{
61
		items:    make([]T, capacity),
62
		capacity: capacity,
63
	}
64
}
65
66
// Push adds an item to the buffer, evicting the oldest if full.
67
func (r *RingBuffer[T]) Push(item T) {
68
	r.mu.Lock()
69
	defer r.mu.Unlock()
70
71
	r.items[r.head] = item
72
	r.head = (r.head + 1) % r.capacity
73
74
	if r.size < r.capacity {
75
		r.size++
76
	}
77
}
78
79
// Snapshot returns a copy of all items in chronological order.
80
func (r *RingBuffer[T]) Snapshot() []T {
81
	r.mu.RLock()
82
	defer r.mu.RUnlock()
83
84
	if r.size == 0 {
85
		return nil
86
	}
87
88
	result := make([]T, r.size)
89
	if r.size < r.capacity {
90
		copy(result, r.items[:r.size])
91
	} else {
92
		// Buffer is full, head points to oldest item
93
		copy(result, r.items[r.head:])
94
		copy(result[r.capacity-r.head:], r.items[:r.head])
95
	}
96
	return result
97
}
98
99
// Len returns the current number of items.
100
func (r *RingBuffer[T]) Len() int {
101
	r.mu.RLock()
102
	defer r.mu.RUnlock()
103
	return r.size
104
}
105
106
// Cap returns the buffer capacity.
107
func (r *RingBuffer[T]) Cap() int {
108
	return r.capacity
109
}
110
111
// Clear removes all items from the buffer.
112
func (r *RingBuffer[T]) Clear() {
113
	r.mu.Lock()
114
	defer r.mu.Unlock()
115
116
	// Zero out items to allow GC
117
	var zero T
118
	for i := range r.items {
119
		r.items[i] = zero
120
	}
121
	r.head = 0
122
	r.size = 0
123
}
124
125
// Filter returns items matching the predicate.
126
func (r *RingBuffer[T]) Filter(pred func(T) bool) []T {
127
	snapshot := r.Snapshot()
128
	result := make([]T, 0, len(snapshot))
129
	for _, item := range snapshot {
130
		if pred(item) {
131
			result = append(result, item)
132
		}
133
	}
134
	return result
135
}
136
137
// BufferHandler wraps another handler and stores all records in a ring buffer.
138
// Records are stored regardless of level filtering applied by the inner handler.
139
type BufferHandler struct {
140
	inner  slog.Handler
141
	buffer *RingBuffer[StoredRecord]
142
	groups []string
143
}
144
145
// NewBufferHandler creates a buffering wrapper around another handler.
146
func NewBufferHandler(inner slog.Handler, opts ...Option) *BufferHandler {
147
	o := defaultOptions()
148
	for _, opt := range opts {
149
		if opt != nil {
150
			opt(o)
151
		}
152
	}
153
154
	return &BufferHandler{
155
		inner:  inner,
156
		buffer: NewRingBuffer[StoredRecord](o.bufferSize),
157
	}
158
}
159
160
// Enabled implements slog.Handler.
161
// Always returns true to capture all records in the buffer.
162
func (h *BufferHandler) Enabled(_ context.Context, _ slog.Level) bool {
163
	return true
164
}
165
166
// Handle implements slog.Handler.
167
func (h *BufferHandler) Handle(ctx context.Context, r slog.Record) error {
168
	// Always store in buffer
169
	h.buffer.Push(FromSlogRecord(r, h.groups))
170
171
	// Delegate to inner handler (which may filter by level)
172
	if h.inner.Enabled(ctx, r.Level) {
173
		return h.inner.Handle(ctx, r)
174
	}
175
	return nil
176
}
177
178
// WithAttrs implements slog.Handler.
179
func (h *BufferHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
180
	return &BufferHandler{
181
		inner:  h.inner.WithAttrs(attrs),
182
		buffer: h.buffer, // shared buffer
183
		groups: h.groups,
184
	}
185
}
186
187
// WithGroup implements slog.Handler.
188
func (h *BufferHandler) WithGroup(name string) slog.Handler {
189
	if name == "" {
190
		return h
191
	}
192
	return &BufferHandler{
193
		inner:  h.inner.WithGroup(name),
194
		buffer: h.buffer, // shared buffer
195
		groups: append(slices.Clone(h.groups), name),
196
	}
197
}
198
199
// Buffer returns the underlying ring buffer for direct access.
200
func (h *BufferHandler) Buffer() *RingBuffer[StoredRecord] {
201
	return h.buffer
202
}
203
204
// Records returns all stored records.
205
func (h *BufferHandler) Records() []StoredRecord {
206
	return h.buffer.Snapshot()
207
}
208
209
// RecordsByLevel returns records matching the given level.
210
func (h *BufferHandler) RecordsByLevel(level slog.Level) []StoredRecord {
211
	return h.buffer.Filter(func(r StoredRecord) bool {
212
		return r.Level == level
213
	})
214
}
215
216
// RecordsSince returns records from the given time onwards.
217
func (h *BufferHandler) RecordsSince(t time.Time) []StoredRecord {
218
	return h.buffer.Filter(func(r StoredRecord) bool {
219
		return !r.Time.Before(t)
220
	})
221
}
222
223
// Replay sends all buffered records to another handler.
224
func (h *BufferHandler) Replay(ctx context.Context, target slog.Handler) error {
225
	for _, stored := range h.buffer.Snapshot() {
226
		r := stored.ToSlogRecord()
227
		if target.Enabled(ctx, r.Level) {
228
			if err := target.Handle(ctx, r); err != nil {
229
				return err
230
			}
231
		}
232
	}
233
	return nil
234
}
235

Source Files