Improve agent-facing history endpoints (Phase 1)

- Fix /api/history/{type} to search the full persistent store instead of
  only the bounded in-memory ring buffer, so filtered history isn't
  silently truncated.
- Add structured type/since/until filters to /api/history/search and
  /api/history/wait, alongside the existing free-text query.
- Add GET /api/traces/{traceId} to fetch all entries for a trace in one
  call instead of relying on substring search.
- Rate-limit /api/history/wait per client IP.
- Add docs/PLAN.md tracking the phased improvement plan; update README
  and agents_example.md for the new/changed endpoints.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
developerandClaude Sonnet 5 committed 2026-09-17 09:30:18 +00:00
1 parent 0ed1cb4355
commit f77b89a779
13 files changed
+473 -88

No files matched your search

+24 -14
View File
@@ -29,6 +29,7 @@ type Collector struct {
store storage.Store
subscribers map[chan websocket.HistoryEntry]func(websocket.HistoryEntry) bool
subMutex sync.Mutex
waitLimiter *ipRateLimiter
}
// New creates a new Collector instance.
@@ -39,6 +40,8 @@ func New(broadcaster websocket.Broadcaster, historySize int, store storage.Store
history: ring.New(historySize),
store: store,
subscribers: make(map[chan websocket.HistoryEntry]func(websocket.HistoryEntry) bool),
// Allow bursts of a few immediate waits, then steady-state ~1 req/sec per IP.
waitLimiter: newIPRateLimiter(1, 5),
}
}
@@ -129,32 +132,39 @@ func (c *Collector) GetHistory(limit int) []websocket.HistoryEntry {
return history
}
// SearchHistory searches through stored telemetry data using the provided query.
func (c *Collector) SearchHistory(ctx context.Context, query string, limit int) ([]websocket.HistoryEntry, error) {
// SearchHistory searches through stored telemetry data using the provided options.
// When a persistent store is configured, the full stored history is searched;
// otherwise it falls back to the in-memory ring buffer.
func (c *Collector) SearchHistory(ctx context.Context, opts storage.SearchOptions, limit int) ([]websocket.HistoryEntry, error) {
if limit <= 0 {
limit = 50
}
if c.store != nil {
return c.store.Search(ctx, query, limit)
return c.store.Search(ctx, opts, limit)
}
// Fallback to in-memory if no store
history := c.GetHistory(limit)
// Fallback to in-memory if no store. Use the full ring buffer, not just
// 'limit' entries, so type/time filters see everything available.
history := c.GetHistory(0)
var filtered []websocket.HistoryEntry
// Lowercase query for case-insensitive search
lowerQuery := strings.ToLower(query)
lowerQuery := strings.ToLower(opts.Query)
for _, entry := range history {
if query == "" {
filtered = append(filtered, entry)
} else {
// Basic string filter for in-memory, no need to re-marshal RawMessage
if strings.Contains(strings.ToLower(string(entry.Data)), lowerQuery) {
filtered = append(filtered, entry)
}
if opts.Query != "" && !strings.Contains(strings.ToLower(string(entry.Data)), lowerQuery) {
continue
}
// Keep it simple and just filter all then limit, since historySize is usually small
if opts.Type != "" && entry.Type != opts.Type {
continue
}
if opts.Since != 0 && entry.Timestamp < opts.Since {
continue
}
if opts.Until != 0 && entry.Timestamp > opts.Until {
continue
}
filtered = append(filtered, entry)
}
// Respect limit and return latest messages first