- Add internal/issues.Tracker: fingerprints (kind, service, title) into
deduplicated Issues with count/firstSeen/lastSeen/affected services and
bounded sample occurrences, evicting the least-recently-seen issue once
a cap is reached.
- Add internal/collector/issue_extract.go to pull issues out of ingested
OTLP data: spans with an error status (preferring exception.type/message
from an "exception" event, falling back to the span status message), and
logs at ERROR/FATAL severity (by severity number or text).
- Wire extraction into Collector.ConsumeTraces/ConsumeLogs and expose
GET /api/issues and GET /api/issues/{id}.
- Fix a pre-existing data race in the collector test's mockStore (unsynced
access to entries while addToHistory appends asynchronously), found via
`go test -race` while adding the new issue-tracking tests.
- Update docs/PLAN.md, README.md and agents_example.md for the new API.
The UI "Issues" tab itself lives in the separate frontend project that
builds this server's dist/ bundle; this commit only covers the backend.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
76 lines
2.3 KiB
Go
76 lines
2.3 KiB
Go
package issues
|
|
|
|
import "testing"
|
|
|
|
func TestTrackerDedupesByFingerprint(t *testing.T) {
|
|
tracker := NewTracker()
|
|
|
|
tracker.Record("span_error", "checkout", "charge-card: card declined", Sample{Timestamp: 100, TraceID: "t1"})
|
|
tracker.Record("span_error", "checkout", "charge-card: card declined", Sample{Timestamp: 200, TraceID: "t2"})
|
|
tracker.Record("span_error", "checkout", "different error", Sample{Timestamp: 150})
|
|
|
|
list := tracker.List(0)
|
|
if len(list) != 2 {
|
|
t.Fatalf("expected 2 distinct issues, got %d", len(list))
|
|
}
|
|
|
|
// Most recently seen first.
|
|
if list[0].Title != "charge-card: card declined" {
|
|
t.Errorf("expected most recent issue first, got %q", list[0].Title)
|
|
}
|
|
if list[0].Count != 2 {
|
|
t.Errorf("expected duplicate occurrences to merge into count 2, got %d", list[0].Count)
|
|
}
|
|
if list[0].FirstSeen != 100 || list[0].LastSeen != 200 {
|
|
t.Errorf("expected firstSeen=100 lastSeen=200, got firstSeen=%d lastSeen=%d", list[0].FirstSeen, list[0].LastSeen)
|
|
}
|
|
if len(list[0].Samples) != 2 {
|
|
t.Errorf("expected 2 samples, got %d", len(list[0].Samples))
|
|
}
|
|
}
|
|
|
|
func TestTrackerGet(t *testing.T) {
|
|
tracker := NewTracker()
|
|
tracker.Record("log_error", "api", "panic: nil pointer", Sample{Timestamp: 100})
|
|
|
|
list := tracker.List(0)
|
|
if len(list) != 1 {
|
|
t.Fatalf("expected 1 issue, got %d", len(list))
|
|
}
|
|
|
|
issue, ok := tracker.Get(list[0].ID)
|
|
if !ok {
|
|
t.Fatalf("expected to find issue %s", list[0].ID)
|
|
}
|
|
if issue.Service != "api" {
|
|
t.Errorf("expected service 'api', got %s", issue.Service)
|
|
}
|
|
|
|
if _, ok := tracker.Get("does-not-exist"); ok {
|
|
t.Errorf("expected lookup of unknown id to fail")
|
|
}
|
|
}
|
|
|
|
func TestTrackerDifferentServicesDoNotMerge(t *testing.T) {
|
|
tracker := NewTracker()
|
|
tracker.Record("span_error", "checkout", "timeout", Sample{Timestamp: 100})
|
|
tracker.Record("span_error", "billing", "timeout", Sample{Timestamp: 100})
|
|
|
|
list := tracker.List(0)
|
|
if len(list) != 2 {
|
|
t.Fatalf("expected same title on different services to stay distinct, got %d issues", len(list))
|
|
}
|
|
}
|
|
|
|
func TestTrackerListRespectsLimit(t *testing.T) {
|
|
tracker := NewTracker()
|
|
for i := 0; i < 5; i++ {
|
|
tracker.Record("log_error", "api", string(rune('a'+i)), Sample{Timestamp: int64(i)})
|
|
}
|
|
|
|
list := tracker.List(2)
|
|
if len(list) != 2 {
|
|
t.Fatalf("expected limit to cap results at 2, got %d", len(list))
|
|
}
|
|
}
|