152 lines
4.0 KiB
Go
152 lines
4.0 KiB
Go
// Package buffer provides an in-memory ring buffer of structured log entries.
|
|
// Used to capture recent log lines for inclusion in user-submitted reports
|
|
// (e.g. Improve Kandev) without unbounded memory growth.
|
|
package buffer
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap/zapcore"
|
|
)
|
|
|
|
// DefaultCapacity is the maximum number of entries kept in the default buffer.
|
|
const DefaultCapacity = 2000
|
|
|
|
// Entry is a structured log record snapshotted from a zap core.
|
|
type Entry struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Level string `json:"level"`
|
|
Logger string `json:"logger,omitempty"`
|
|
Caller string `json:"caller,omitempty"`
|
|
Message string `json:"message"`
|
|
Fields map[string]any `json:"fields,omitempty"`
|
|
Stack string `json:"stack,omitempty"`
|
|
}
|
|
|
|
// RingBuffer is a thread-safe FIFO buffer with a fixed capacity.
|
|
type RingBuffer struct {
|
|
mu sync.Mutex
|
|
entries []Entry
|
|
capacity int
|
|
}
|
|
|
|
// New returns a RingBuffer with the given capacity. A non-positive capacity
|
|
// is treated as DefaultCapacity.
|
|
func New(capacity int) *RingBuffer {
|
|
if capacity <= 0 {
|
|
capacity = DefaultCapacity
|
|
}
|
|
return &RingBuffer{capacity: capacity, entries: make([]Entry, 0, capacity)}
|
|
}
|
|
|
|
// Push appends e to the buffer, evicting the oldest entry when full.
|
|
func (b *RingBuffer) Push(e Entry) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if len(b.entries) >= b.capacity {
|
|
copy(b.entries, b.entries[1:])
|
|
b.entries = b.entries[:len(b.entries)-1]
|
|
}
|
|
b.entries = append(b.entries, e)
|
|
}
|
|
|
|
// Snapshot returns a copy of the current entries in chronological order.
|
|
// Each entry is deep-copied so callers cannot mutate buffered Fields maps.
|
|
func (b *RingBuffer) Snapshot() []Entry {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
out := make([]Entry, len(b.entries))
|
|
for i, e := range b.entries {
|
|
out[i] = e
|
|
if e.Fields != nil {
|
|
fields := make(map[string]any, len(e.Fields))
|
|
for k, v := range e.Fields {
|
|
fields[k] = v
|
|
}
|
|
out[i].Fields = fields
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Len returns the number of entries currently held.
|
|
func (b *RingBuffer) Len() int {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
return len(b.entries)
|
|
}
|
|
|
|
// Clear removes all entries.
|
|
func (b *RingBuffer) Clear() {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.entries = b.entries[:0]
|
|
}
|
|
|
|
var (
|
|
defaultOnce sync.Once
|
|
defaultBuf *RingBuffer
|
|
)
|
|
|
|
// Default returns the process-wide ring buffer used by the structured logger.
|
|
func Default() *RingBuffer {
|
|
defaultOnce.Do(func() {
|
|
defaultBuf = New(DefaultCapacity)
|
|
})
|
|
return defaultBuf
|
|
}
|
|
|
|
// Core is a zapcore.Core that pushes every emitted entry into a RingBuffer.
|
|
// It performs no encoding or output; it is intended to be teed alongside the
|
|
// real output core via zapcore.NewTee.
|
|
type Core struct {
|
|
zapcore.LevelEnabler
|
|
buffer *RingBuffer
|
|
fields []zapcore.Field
|
|
}
|
|
|
|
// NewCore returns a zapcore.Core that writes to buf at the given level.
|
|
func NewCore(buf *RingBuffer, level zapcore.LevelEnabler) zapcore.Core {
|
|
return &Core{LevelEnabler: level, buffer: buf}
|
|
}
|
|
|
|
// With returns a child Core with additional fields attached.
|
|
func (c *Core) With(fields []zapcore.Field) zapcore.Core {
|
|
clone := *c
|
|
clone.fields = append(append([]zapcore.Field{}, c.fields...), fields...)
|
|
return &clone
|
|
}
|
|
|
|
// Check adds the core to the checked entry when the level is enabled.
|
|
func (c *Core) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
|
if c.Enabled(ent.Level) {
|
|
return ce.AddCore(ent, c)
|
|
}
|
|
return ce
|
|
}
|
|
|
|
// Write captures the entry into the ring buffer.
|
|
func (c *Core) Write(ent zapcore.Entry, fields []zapcore.Field) error {
|
|
enc := zapcore.NewMapObjectEncoder()
|
|
for _, f := range c.fields {
|
|
f.AddTo(enc)
|
|
}
|
|
for _, f := range fields {
|
|
f.AddTo(enc)
|
|
}
|
|
c.buffer.Push(Entry{
|
|
Timestamp: ent.Time,
|
|
Level: ent.Level.String(),
|
|
Logger: ent.LoggerName,
|
|
Caller: ent.Caller.TrimmedPath(),
|
|
Message: ent.Message,
|
|
Fields: enc.Fields,
|
|
Stack: ent.Stack,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Sync is a no-op; the ring buffer holds entries in memory only.
|
|
func (c *Core) Sync() error { return nil }
|