Files
synapbus/internal/trace/retention.go
T
Algis DumbrisandClaude Opus 4.6 be098081b4 feat: implement trace logging, REST API, and observability
Add comprehensive trace logging and observability features:

- Enhanced trace store with owner-scoped queries, filtering (agent, action,
  time range), pagination, streaming export, and retention cleanup
- REST API endpoints: GET /api/traces (list with filters), GET /api/traces/export
  (streaming JSON/CSV), GET /api/traces/stats (action counts)
- Owner isolation enforced at every layer (store, API, tests)
- Hand-rolled Prometheus metrics (pure Go, zero CGO): traces_total,
  traces_by_action, errors_total, active_agents at GET /metrics
- Configurable slog JSON handler with --log-level flag
- Request ID middleware for cross-referencing logs and traces
- Batch trace writing (64 entries or 100ms flush interval)
- Background retention cleanup via --trace-retention flag
- SQL migration 002 adds owner_id column and composite indexes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:57:05 +02:00

86 lines
1.9 KiB
Go

package trace
import (
"context"
"log/slog"
"time"
)
// RetentionCleaner periodically deletes traces older than the retention period.
type RetentionCleaner struct {
store TraceStore
retention time.Duration
interval time.Duration
logger *slog.Logger
cancel context.CancelFunc
done chan struct{}
}
// NewRetentionCleaner creates a new retention cleaner.
// retention is the maximum age of traces to keep.
// interval is how often to run the cleanup (defaults to 1 hour).
func NewRetentionCleaner(store TraceStore, retention time.Duration, interval time.Duration) *RetentionCleaner {
if interval == 0 {
interval = 1 * time.Hour
}
return &RetentionCleaner{
store: store,
retention: retention,
interval: interval,
logger: slog.Default().With("component", "trace-retention"),
done: make(chan struct{}),
}
}
// Start begins the background retention cleanup goroutine.
func (rc *RetentionCleaner) Start() {
ctx, cancel := context.WithCancel(context.Background())
rc.cancel = cancel
go rc.loop(ctx)
}
// Stop stops the retention cleaner and waits for the goroutine to finish.
func (rc *RetentionCleaner) Stop() {
if rc.cancel != nil {
rc.cancel()
<-rc.done
}
}
func (rc *RetentionCleaner) loop(ctx context.Context) {
defer close(rc.done)
// Run once immediately
rc.cleanup(ctx)
ticker := time.NewTicker(rc.interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
rc.cleanup(ctx)
}
}
}
func (rc *RetentionCleaner) cleanup(ctx context.Context) {
cutoff := time.Now().Add(-rc.retention)
deleted, err := rc.store.DeleteOlderThan(ctx, cutoff)
if err != nil {
rc.logger.Error("trace retention cleanup failed",
"error", err,
"cutoff", cutoff,
)
return
}
if deleted > 0 {
rc.logger.Info("trace retention cleanup completed",
"deleted", deleted,
"cutoff", cutoff,
)
}
}