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
@@ -3,9 +3,13 @@ package collector
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-otel/internal/storage"
|
||||
"git.tornberg.me/go-otel/internal/websocket"
|
||||
)
|
||||
|
||||
@@ -22,8 +26,18 @@ func (m *mockStore) GetHistory(ctx context.Context, limit int) ([]websocket.Hist
|
||||
return m.entries, nil
|
||||
}
|
||||
|
||||
func (m *mockStore) Search(ctx context.Context, query string, limit int) ([]websocket.HistoryEntry, error) {
|
||||
return m.entries, nil
|
||||
func (m *mockStore) Search(ctx context.Context, opts storage.SearchOptions, limit int) ([]websocket.HistoryEntry, error) {
|
||||
var filtered []websocket.HistoryEntry
|
||||
for _, e := range m.entries {
|
||||
if opts.Type != "" && e.Type != opts.Type {
|
||||
continue
|
||||
}
|
||||
if opts.Query != "" && !strings.Contains(string(e.Data), opts.Query) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, e)
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func TestCollectorWithStore(t *testing.T) {
|
||||
@@ -81,3 +95,75 @@ func TestCollectorHistoryLimit(t *testing.T) {
|
||||
t.Errorf("expected 10 entries even with limit 100, got %d", len(history))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleHistoryByTypeUsesStore(t *testing.T) {
|
||||
store := &mockStore{
|
||||
entries: []websocket.HistoryEntry{
|
||||
{Type: "traces", Timestamp: 1, Data: json.RawMessage(`{"id":1}`)},
|
||||
{Type: "traces", Timestamp: 2, Data: json.RawMessage(`{"id":2}`)},
|
||||
{Type: "logs", Timestamp: 3, Data: json.RawMessage(`{"id":3}`)},
|
||||
},
|
||||
}
|
||||
// Ring buffer is much smaller than the store's content, so a correct
|
||||
// implementation must route through the store rather than the ring buffer.
|
||||
coll := New(nil, 1, store)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/history/traces", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
coll.HandleHistoryByType("traces")(rec, req)
|
||||
|
||||
var results []websocket.HistoryEntry
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &results); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("expected 2 trace entries from the store, got %d", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleTraceByID(t *testing.T) {
|
||||
store := &mockStore{
|
||||
entries: []websocket.HistoryEntry{
|
||||
{Type: "traces", Timestamp: 1, Data: json.RawMessage(`{"traceId":"abc123"}`)},
|
||||
{Type: "logs", Timestamp: 2, Data: json.RawMessage(`{"traceId":"abc123","msg":"related"}`)},
|
||||
{Type: "traces", Timestamp: 3, Data: json.RawMessage(`{"traceId":"other"}`)},
|
||||
},
|
||||
}
|
||||
coll := New(nil, 10, store)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/traces/abc123", nil)
|
||||
req.SetPathValue("traceId", "abc123")
|
||||
rec := httptest.NewRecorder()
|
||||
coll.HandleTraceByID(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var results []websocket.HistoryEntry
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &results); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("expected 2 entries referencing trace abc123, got %d", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWaitRateLimited(t *testing.T) {
|
||||
coll := New(nil, 10, nil)
|
||||
|
||||
var lastCode int
|
||||
for i := 0; i < 10; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/history/wait?q=nope&timeout=0", nil)
|
||||
req.RemoteAddr = "203.0.113.1:12345"
|
||||
rec := httptest.NewRecorder()
|
||||
coll.HandleWait(rec, req)
|
||||
lastCode = rec.Code
|
||||
if lastCode == http.StatusTooManyRequests {
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastCode != http.StatusTooManyRequests {
|
||||
t.Fatalf("expected rate limiting to eventually return 429, last code was %d", lastCode)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user