diff --git a/CLAUDE.md b/CLAUDE.md index 7a58b81..e6e6903 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ OtelContext is a self-hosted OTLP observability platform. Single Go binary with embedded React frontend. - **Backend:** Go 1.25, native `net/http` (no frameworks), GORM ORM, gRPC + HTTP for OTLP ingestion -- **Frontend:** React 19 + TypeScript + TanStack Query/Virtual + wouter + Radix primitives + cmdk palette + hand-rolled token CSS (`ui/src/styles/tokens.css`) with CSS Modules. The **service map renders via React Flow (`@xyflow/react`, MIT)** over a dagre layered layout (`ui/src/lib/dagreLayout.ts` — `@dagrejs/dagre`, MIT: crossing-minimised, left→right, with edge-routing waypoints) computed synchronously. React Flow owns pan/zoom/fit/minimap/grid/a11y; nodes are token-themed React components — draggable, and they collapse to status **dots** below a zoom threshold (level-of-detail) so they stop occluding edges at scale; edges route through the dagre waypoints and meet node borders. No UI framework: `@ossrandom/design-system`, cytoscape, uplot remain removed (cytoscape's physics hairball → deterministic layered layout + React Flow; the prior hand-rolled-SVG map was replaced by React Flow on 2026-06-18, then moved to the dagre layered layout) +- **Frontend:** React 19 + TypeScript + TanStack Query/Virtual + wouter + Radix primitives + cmdk palette + hand-rolled token CSS (`ui/src/styles/tokens.css`) with CSS Modules. The **service map renders via React Flow (`@xyflow/react`, MIT)** over a deterministic phyllotaxis ("sunflower") layout (`ui/src/lib/radialLayout.ts` — most-critical service near the disc centre, golden-angle spiral) computed synchronously. Unlike a layered DAG, the sunflower fills a disc evenly and never collapses sparse/disconnected graphs into a single column, so the map stays navigable from 7 to 120+ services. React Flow owns pan/zoom/fit/minimap/grid/a11y; nodes are token-themed React components — draggable, and they collapse to status **dots** below a zoom threshold (level-of-detail) so they stop occluding edges at scale. Edges are straight chords pinned to node borders (centre-to-centre at dot zoom) carrying a direction **arrowhead** (`markerEnd`) so flow is legible statically — at any zoom and under `prefers-reduced-motion`; selecting a node accents the active path (both nodes **and** edges) and dims the rest. No UI framework: `@ossrandom/design-system`, cytoscape, uplot, **and `@dagrejs/dagre`** remain removed (cytoscape's physics hairball → React Flow; the prior hand-rolled-SVG map was replaced by React Flow on 2026-06-18; the dagre layered layout it briefly used was itself replaced by the sunflower layout on 2026-06-18 because a layered DAG stacked sparse 120-service graphs into an unreadable single column) - **Ports:** gRPC `:4317` (OTLP), HTTP `:8080` (API + HTTP OTLP + WebSocket + UI) ## Strict Rules @@ -114,6 +114,8 @@ LogsServer.Export() → DB persist → logCallback → GraphRAG.OnLogIngested( MetricsServer.Export() → TSDB → metricCallback → GraphRAG.OnMetricIngested() ``` +**Pre-sample topology observer** (`internal/graphrag/topology_observer.go`): `TraceServer.Export()` also calls `GraphRAG.ObserveSpanTopology()` for **every** received span *before* the sampler's keep/drop decision, so cross-service CALLS edges are recorded independent of `SAMPLING_RATE`. Without it, an edge formed only when both the caller and callee spans survived sampling (~`rate²` joint probability), so at low sampling the service map showed nodes but almost no edges/flow. The observer is existence-only (`ServiceStore.EnsureService`/`EnsureCallEdge` create with zeroed aggregates, no-op if present) so the sampled path stays the **sole** source of CallCount/latency/error-rate; it's bounded by a per-tenant spanID→service LRU (cap 100k) + per-pair dedup (memory ≈ #service-pairs) and never touches TraceStore/eventCh, so the SQLite OOM-survival bounds are unaffected. + ## MCP Server — 7-Tool Triage Surface The MCP server (`internal/mcp/`) exposes a focused 7-tool triage surface via