Files
DownLink/backend/services/logbuffer.go

146 lines
3.5 KiB
Go

package services
import (
"container/ring"
"context"
"log/slog"
"sync"
"time"
)
// LogEntry represents a structured log entry
type LogEntry struct {
Time time.Time `json:"time"`
Level string `json:"level"`
Message string `json:"msg"`
Attrs map[string]any `json:"attrs,omitempty"`
}
// LogBuffer is a service that maintains an in-memory buffer of recent logs
type LogBuffer struct {
buffer *ring.Ring
mutex sync.RWMutex
size int
}
// NewLogBuffer creates a new log buffer with the specified capacity
func NewLogBuffer(capacity int) *LogBuffer {
return &LogBuffer{
buffer: ring.New(capacity),
size: capacity,
}
}
// Add adds a log entry to the buffer
func (lb *LogBuffer) Add(entry LogEntry) {
lb.mutex.Lock()
defer lb.mutex.Unlock()
lb.buffer.Value = entry
lb.buffer = lb.buffer.Next()
}
// GetEntries returns all log entries in chronological order
func (lb *LogBuffer) GetEntries() []LogEntry {
lb.mutex.RLock()
defer lb.mutex.RUnlock()
var entries []LogEntry
lb.buffer.Do(func(val interface{}) {
if val != nil {
entries = append(entries, val.(LogEntry))
}
})
// Sort entries by time (they might be out of order due to ring buffer)
// No need for manual sort as we'll return them in the order they appear in the ring
return entries
}
// GetEntriesByLevel filters log entries by minimum log level
func (lb *LogBuffer) GetEntriesByLevel(minLevel slog.Level) []LogEntry {
allEntries := lb.GetEntries()
if minLevel == slog.LevelDebug {
return allEntries // Return all logs if debug level requested
}
var filteredEntries []LogEntry
for _, entry := range allEntries {
var entryLevel slog.Level
switch entry.Level {
case "DEBUG":
entryLevel = slog.LevelDebug
case "INFO":
entryLevel = slog.LevelInfo
case "WARN":
entryLevel = slog.LevelWarn
case "ERROR":
entryLevel = slog.LevelError
}
if entryLevel >= minLevel {
filteredEntries = append(filteredEntries, entry)
}
}
return filteredEntries
}
// Size returns the capacity of the log buffer
func (lb *LogBuffer) Size() int {
return lb.size
}
// InMemoryHandler is a slog.Handler that writes logs to the in-memory buffer
type InMemoryHandler struct {
logBuffer *LogBuffer
next slog.Handler
}
// NewInMemoryHandler creates a new slog.Handler that writes logs to both
// the in-memory buffer and the next handler
func NewInMemoryHandler(logBuffer *LogBuffer, next slog.Handler) *InMemoryHandler {
return &InMemoryHandler{
logBuffer: logBuffer,
next: next,
}
}
// Enabled implements slog.Handler.
func (h *InMemoryHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.next.Enabled(ctx, level)
}
// Handle implements slog.Handler.
func (h *InMemoryHandler) Handle(ctx context.Context, record slog.Record) error {
// Forward to next handler
if err := h.next.Handle(ctx, record); err != nil {
return err
}
// Store in buffer
attrs := make(map[string]any)
record.Attrs(func(attr slog.Attr) bool {
attrs[attr.Key] = attr.Value.Any()
return true
})
h.logBuffer.Add(LogEntry{
Time: record.Time,
Level: record.Level.String(),
Message: record.Message,
Attrs: attrs,
})
return nil
}
// WithAttrs implements slog.Handler.
func (h *InMemoryHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return NewInMemoryHandler(h.logBuffer, h.next.WithAttrs(attrs))
}
// WithGroup implements slog.Handler.
func (h *InMemoryHandler) WithGroup(name string) slog.Handler {
return NewInMemoryHandler(h.logBuffer, h.next.WithGroup(name))
}