You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
LogClusters was the one unbounded structure on the in-memory hot path: an
insert-only map keyed by service×Drain-template-ID, explicitly skipped by
Prune. Template IDs shift as Drain generalizes and the key carries the
service, so the map grew with every distinct (service, template-id) pair ever
seen — unbounded by the shared 50k Drain LRU and by log volume. Under a
sustained high-shape log stream (exactly the case when INFO is kept in-memory
but not persisted) this leaks RAM.
Extend SignalStore.Prune to bound LogClusters the same way it bounds Metrics:
- TTL: evict clusters with LastSeen older than the 24h signalRetention cutoff
(ages out the template-ID accretion as orphaned clusters go stale).
- Cap: maxLogClustersPerTenant=10000 backstop, oldest-LastSeen first.
- Evicted clusters' EMITTED_BY / LOGGED_DURING edges are swept (collected by
cluster id; removed even when not yet time-stale, e.g. cap-evicted).
Prune signature gains maxLogClusters; refresh.go passes the new constant.
Tests: rewrote _SweepsStaleLogClusterEdgesOnly (which asserted the old
never-pruned behavior) into _DropsStaleLogClustersKeepsFresh; added
_LogClusterCapEvictsOldest. Verified: go build/vet, golangci-lint exit 0,
go test -race ./internal/graphrag (109), full suite 679, gofmt clean.
Closes the last unbounded structure on the in-memory path. CPU stays bounded
by the fixed 16-worker pool + drop-when-full event queue; RAM by GOMEMLIMIT
(75% budget) + per-tenant caps/TTLs on TraceStore (500k+TTL), SignalStore
metrics (2k) + now log clusters (10k), topology LRU (100k), TSDB cardinality,
and 24h idle-tenant eviction.
Claude-Session: https://claude.ai/code/session_01LDQVJrixs2nJoea67a8pEG
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@@ -219,7 +219,7 @@ Key settings in `internal/config/config.go`:
219
219
-`LOG_FTS_ENABLED` — when truthy (`true`/`yes`/`on`/`1`), provisions the SQLite FTS5 `logs_fts` virtual table + sync triggers at startup; when false, log-search uses a 24h-clamped LIKE fallback. **Defaults to `true` when `DB_DRIVER=sqlite`** (BM25 is dramatically faster than LIKE on the kept `search_logs` MCP tool) and `false` otherwise. Toggle off and reclaim the ~30% disk overhead via `POST /api/admin/drop_fts` (refused while the flag is on). The vectordb-backed semantic-search path was removed on 2026-05-24.
-`GRAPHRAG_WORKER_COUNT` (16), `GRAPHRAG_EVENT_QUEUE_SIZE` (100000; **10000 on SQLite**) — sized for 100–200 services; raise further if `otelcontext_graphrag_events_dropped_total` climbs
222
-
-`GRAPHRAG_TRACE_TTL` (`1h`; **`30m` on SQLite**), `GRAPHRAG_MAX_SPANS_PER_TENANT` (500000), `GRAPHRAG_TENANT_IDLE_TTL` (`24h`) — in-memory GraphRAG memory bounds. Spans past the per-tenant cap are skipped from the graph only (DB unaffected; metered as `otelcontext_graphrag_events_dropped_total{signal="span_capacity"}`); tenant store slices idle past the TTL are evicted (default tenant immune, self-healing via the 60s rebuild). SignalStore metrics are bounded to 2000/tenant + 24h TTL (constants).
222
+
-`GRAPHRAG_TRACE_TTL` (`1h`; **`30m` on SQLite**), `GRAPHRAG_MAX_SPANS_PER_TENANT` (500000), `GRAPHRAG_TENANT_IDLE_TTL` (`24h`) — in-memory GraphRAG memory bounds. Spans past the per-tenant cap are skipped from the graph only (DB unaffected; metered as `otelcontext_graphrag_events_dropped_total{signal="span_capacity"}`); tenant store slices idle past the TTL are evicted (default tenant immune, self-healing via the 60s rebuild). SignalStore is bounded per-tenant by a 24h TTL + a cap: metrics 2000/tenant, log clusters 10000/tenant (constants; the log-cluster cap was added because clusters key on service×Drain-template-ID and would otherwise grow unbounded as template IDs churn).
223
223
-`PPROF_ADDR` (`127.0.0.1:6060`) — `net/http/pprof` on a dedicated loopback listener (never the public mux); empty disables. Startup also sets a soft `GOMEMLIMIT` (honors the env var, else 75% of the cgroup/host budget via `internal/membudget`).
224
224
-`INGEST_MIN_SEVERITY` (`INFO`), `STORE_MIN_SEVERITY` (**defaults to `"WARN"` for all drivers**; `""` falls back to same-as-ingest) — two-tier log severity gate. The ingest gate runs at the OTLP receiver and **drops the log entirely** below the threshold (no in-memory enrichment either). The store gate runs at the persist boundary inside the async pipeline (`internal/ingest/pipeline.go:process`) and **only skips the DB row write** — the log still flows through `LogCallback` so GraphRAG Drain template mining and span/trace correlation see it. By default (`STORE_MIN_SEVERITY=WARN`, `INGEST_MIN_SEVERITY=INFO`) INFO logs reach GraphRAG/Drain in-memory but are **not** persisted — the DB only grows with WARN+. To also analyse DEBUG in-memory (not just INFO), set `INGEST_MIN_SEVERITY=DEBUG` (raises in-memory event volume). Setting `STORE_MIN_SEVERITY` ≤ `INGEST_MIN_SEVERITY` is a no-op (logged as a warning at startup). Drops surface via `Pipeline.Stats().StoreFiltered`.
225
225
-`INGEST_ASYNC_ENABLED` (true), `INGEST_PIPELINE_QUEUE_SIZE` (50000), `INGEST_PIPELINE_WORKERS` (8), `INGEST_PIPELINE_MAX_BYTES` (536870912 = 512 MB; **128 MB on SQLite**) — async ingest pipeline (`internal/ingest/pipeline.go`). Hybrid backpressure: <90% accept all, 90–100% drop healthy batches (errors/slow always pass), 100% return gRPC `RESOURCE_EXHAUSTED`. The byte cap bounds queue memory regardless of item count — at the cap even priority batches get `RESOURCE_EXHAUSTED`/429 (a 429 is recoverable, an OOM kill is not); watch `otelcontext_ingest_pipeline_queue_bytes` and reason `bytes_full`. Set `INGEST_ASYNC_ENABLED=false` to revert to synchronous DB writes inside `Export()`. Drops surface as `otelcontext_ingest_pipeline_dropped_total{signal,reason}`.
0 commit comments