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:
1 parent
0ed1cb4355
commit
f77b89a779
13 files changed
+473
-88
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user