Files
go-otel/internal/collector/issue_traces_test.go
T
mats 49f9bb3458
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 1m3s
materialized view and a bit better ui
2026-09-19 12:43:17 +02:00

134 lines
4.5 KiB
Go

package collector
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"git.tornberg.me/go-otel/internal/issues"
"git.tornberg.me/go-otel/internal/storage"
"git.tornberg.me/go-otel/internal/websocket"
)
// occStore is a mockStore that also implements OccurrenceStore and
// TraceQuerier, standing in for ClickHouse.
type occStore struct {
mockStore
omu sync.Mutex
occs []storage.Occurrence
page storage.IssueTracesPage
}
func (o *occStore) AppendOccurrence(_ context.Context, occ storage.Occurrence) error {
o.omu.Lock()
defer o.omu.Unlock()
o.occs = append(o.occs, occ)
return nil
}
func (o *occStore) IssueTraces(context.Context, string, storage.IssueTracesOptions) (storage.IssueTracesPage, error) {
return o.page, nil
}
func (o *occStore) TraceEntries(context.Context, string, int) ([]websocket.HistoryEntry, error) {
return nil, nil
}
func (o *occStore) TraceSummaries(_ context.Context, ids []string) (map[string]storage.TraceSummary, error) {
out := map[string]storage.TraceSummary{}
for _, id := range ids {
out[id] = storage.TraceSummary{ID: id, Name: "sum-" + id, Duration: 42, SpanCount: 3}
}
return out, nil
}
func getIssueTraces(t *testing.T, c *Collector, id string) (int, issueTracesResponse) {
t.Helper()
req := httptest.NewRequest(http.MethodGet, "/api/issues/"+id+"/traces", nil)
req.SetPathValue("id", id)
rec := httptest.NewRecorder()
c.HandleIssueTraces(rec, req)
var resp issueTracesResponse
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
return rec.Code, resp
}
func TestOccurrencesPersistedBeyondInMemorySamples(t *testing.T) {
store := &occStore{}
c := New(nil, 10, store)
for i := 0; i < 12; i++ {
c.issues.Record("span_error", "api", "boom", issues.Sample{Timestamp: int64(1000 + i), TraceID: "t1", Message: "boom"})
}
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
store.omu.Lock()
n := len(store.occs)
store.omu.Unlock()
if n == 12 {
break
}
time.Sleep(10 * time.Millisecond)
}
store.omu.Lock()
defer store.omu.Unlock()
if len(store.occs) != 12 {
t.Fatalf("persisted %d occurrences, want all 12 (memory keeps only 5)", len(store.occs))
}
if store.occs[0].IssueID == "" || store.occs[0].TraceID != "t1" {
t.Errorf("occurrence = %+v", store.occs[0])
}
}
func TestHandleIssueTracesFromStoreFillsSummaries(t *testing.T) {
sample, _ := json.Marshal(issues.Sample{Timestamp: 5, TraceID: "t9", Message: "x"})
store := &occStore{page: storage.IssueTracesPage{
TotalOccurrences: 7,
HasMore: true,
Traces: []storage.IssueTrace{{TraceID: "t9", Hits: 7, LastSeen: 5, Samples: []json.RawMessage{sample}}},
}}
c := New(nil, 10, store)
c.issues.Record("span_error", "api", "boom", issues.Sample{Timestamp: 1, TraceID: "t9", Message: "x"})
id := c.issues.List(10)[0].ID
code, resp := getIssueTraces(t, c, id)
if code != http.StatusOK || !resp.Complete || resp.TotalOccurrences != 7 || !resp.HasMore || resp.NextBefore != 5 {
t.Fatalf("code=%d resp=%+v", code, resp)
}
if len(resp.Traces) != 1 || resp.Traces[0].Summary == nil || resp.Traces[0].Summary.Name != "sum-t9" {
t.Errorf("summary not filled from TraceQuerier: %+v", resp.Traces)
}
if len(resp.Traces[0].Samples) != 1 || resp.Traces[0].Samples[0].Message != "x" {
t.Errorf("samples = %+v", resp.Traces[0].Samples)
}
}
func TestHandleIssueTracesInMemoryFallback(t *testing.T) {
c := New(nil, 10, nil)
c.issues.Record("span_error", "api", "boom", issues.Sample{Timestamp: 1, TraceID: "a", Message: "m"})
c.issues.Record("span_error", "api", "boom", issues.Sample{Timestamp: 3, TraceID: "b", Message: "m"})
c.issues.Record("span_error", "api", "boom", issues.Sample{Timestamp: 2, TraceID: "a", Message: "m"})
c.issues.Record("span_error", "api", "boom", issues.Sample{Timestamp: 4, Message: "no trace"})
id := c.issues.List(10)[0].ID
code, resp := getIssueTraces(t, c, id)
if code != http.StatusOK || resp.Complete {
t.Fatalf("code=%d complete=%v (fallback must say it is incomplete)", code, resp.Complete)
}
if len(resp.Traces) != 2 || resp.Traces[0].TraceID != "b" || resp.Traces[1].TraceID != "a" || resp.Traces[1].Hits != 2 {
t.Errorf("traces = %+v", resp.Traces)
}
if len(resp.Untraced) != 1 {
t.Errorf("untraced = %d, want 1", len(resp.Untraced))
}
if s := resp.Traces[1].Samples; len(s) != 2 || s[0].Timestamp != 2 || s[1].Timestamp != 1 {
t.Errorf("samples must be newest first: %+v", s)
}
if code, _ := getIssueTraces(t, c, "missing"); code != http.StatusNotFound {
t.Errorf("unknown issue: code=%d, want 404", code)
}
}