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:
1 parent
f77b89a779
commit
0d274ff89c
10 files changed
+627
-6
No files matched your search
@@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-otel/internal/issues"
|
||||
"git.tornberg.me/go-otel/internal/storage"
|
||||
"git.tornberg.me/go-otel/internal/websocket"
|
||||
|
||||
@@ -30,6 +31,7 @@ type Collector struct {
|
||||
subscribers map[chan websocket.HistoryEntry]func(websocket.HistoryEntry) bool
|
||||
subMutex sync.Mutex
|
||||
waitLimiter *ipRateLimiter
|
||||
issues *issues.Tracker
|
||||
}
|
||||
|
||||
// New creates a new Collector instance.
|
||||
@@ -42,9 +44,20 @@ func New(broadcaster websocket.Broadcaster, historySize int, store storage.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),
|
||||
issues: issues.NewTracker(),
|
||||
}
|
||||
}
|
||||
|
||||
// ListIssues returns deduplicated error issues, most recently seen first.
|
||||
func (c *Collector) ListIssues(limit int) []issues.Issue {
|
||||
return c.issues.List(limit)
|
||||
}
|
||||
|
||||
// GetIssue returns a single issue by id.
|
||||
func (c *Collector) GetIssue(id string) (issues.Issue, bool) {
|
||||
return c.issues.Get(id)
|
||||
}
|
||||
|
||||
// SetBroadcaster updates the broadcaster used by the collector.
|
||||
func (c *Collector) SetBroadcaster(broadcaster websocket.Broadcaster) {
|
||||
c.broadcaster = broadcaster
|
||||
@@ -191,6 +204,7 @@ func (c *Collector) ConsumeTraces(ctx context.Context, td ptrace.Traces) error {
|
||||
|
||||
// Store and broadcast raw bytes as json.RawMessage to avoid unsafe interface{} usage
|
||||
c.addToHistory(websocket.DataTypeTraces, buf)
|
||||
extractSpanErrors(c.issues, td, time.Now().UnixNano())
|
||||
|
||||
entry := websocket.Entry{
|
||||
Type: string(websocket.DataTypeTraces),
|
||||
@@ -233,6 +247,7 @@ func (c *Collector) ConsumeLogs(ctx context.Context, ld plog.Logs) error {
|
||||
|
||||
// Store and broadcast raw bytes as json.RawMessage to avoid unsafe interface{} usage
|
||||
c.addToHistory(websocket.DataTypeLogs, buf)
|
||||
extractLogErrors(c.issues, ld, time.Now().UnixNano())
|
||||
|
||||
entry := websocket.Entry{
|
||||
Type: string(websocket.DataTypeLogs),
|
||||
|
||||
Reference in new issue
Block a user