Derive deduplicated error issues from traces and logs (Phase 2 backend)

- 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>
This commit is contained in:
developerandClaude Sonnet 5 committed 2026-09-17 09:35:39 +00:00
1 parent f77b89a779
commit 0d274ff89c
10 files changed
+627 -6

No files matched your search

+40
View File
@@ -339,6 +339,46 @@ func (c *Collector) HandleTraceByID(w http.ResponseWriter, r *http.Request) {
}
}
// HandleIssues returns deduplicated error issues derived from ingested
// traces and logs, most recently seen first.
func (c *Collector) HandleIssues(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
list := c.ListIssues(parseLimit(r, 100))
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Printf("failed to encode issues: %v", err)
http.Error(w, "failed to encode issues", http.StatusInternalServerError)
return
}
}
// HandleIssueByID returns a single issue by id, including its samples.
func (c *Collector) HandleIssueByID(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
id := r.PathValue("id")
issue, ok := c.GetIssue(id)
if !ok {
http.Error(w, "issue not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(issue); err != nil {
log.Printf("failed to encode issue: %v", err)
http.Error(w, "failed to encode issue", http.StatusInternalServerError)
return
}
}
// HandleHealth returns the health status of the collector.
func (c *Collector) HandleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")