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) } }