Repository navigation
fix(protocol): restrict metrics endpoints to admin keys and add operational Prometheus metrics - #365
Merged
Merged
Conversation
jsam
force-pushed
the
fm/fix-metrics-leak
branch
from
October 5, 2026 17:38
34a7a15 to
93a53d9
Compare
jsam
force-pushed
the
fm/fix-metrics-leak
branch
from
October 5, 2026 18:38
305858a to
57f5983
Compare
…a Monitoring guide /metrics and /metrics/prometheus aggregate over every knowledge graph but authorized any valid API key, so a viewer key or a key scoped to one KG could read every tenant's KG, relation, tuple and session counts. Both now need an unscoped admin key and answer others with 403. The Prometheus export gains what an operator needs to run the engine: HTTP responses by status, in-flight requests and open WebSocket connections against their limits, rejections by reason (connection and rate limits, 401/403, WS preauth and message rate, login throttle), failed authentications by method, WS error replies by code, WS notices (slow consumers, timeouts, revocations) and send timeouts, subscription counts and evaluations, WAL size, dirty shards, unflushed updates and their age, flushes and flush failures, the store's read-only state, per-KG relation/tuple/view/loaded/session gauges, query memory and compute permits in use. memory_usage_bytes is now the process RSS instead of tuples x 64, with the cgroup limit beside it. The deployment guide documents the endpoint, its auth, a scrape config, every metric and recommended alerts. Closes #300
jsam
force-pushed
the
fm/fix-metrics-leak
branch
from
October 5, 2026 19:08
57f5983 to
532071a
Compare
jsam
marked this pull request as ready for review
October 5, 2026 19:17
This was referenced Oct 5, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
I want you to really hyper focus on making the symbolic engine, you know, crisp and really truly production ready. ... just ensure that both machines are propelry utilized now (Context: implement #300 exactly as written.)
Issue #300, "Observability gaps: metrics blind spots, a cross-tenant metrics leak, undocumented Prometheus endpoint", says: from the engine production-readiness audit, see the audit report section P1-4; fix the cross-tenant leak first. The substance of what it refers to:
Cross-tenant leak (fix first): /metrics and /metrics/prometheus authorize any valid API key. The auth middleware only checks that the key authenticates, with no role or scope check, and the handlers scan all knowledge graphs, so a single-KG scoped viewer key can read global KG/relation/tuple/memory/session counts for every tenant. Fix: require global admin role for the metrics handlers (or a separate metrics listener).
P1-4, observability is blind to what an operator needs most, and the Prometheus endpoint is undocumented. /metrics/prometheus exports uptime, queries_total, gauges for KG/relation/view/tuple/session counts and query-latency histograms. Missing, each operationally needed: HTTP request/error rate by status; active-connection and active-WebSocket gauges (the connection semaphores are never surfaced); rejection/throttle counters (503 "Too many connections", 429 rate-limit, 401 auth-fail and slow-consumer disconnects increment nothing); WAL size, flush lag and dirty-shard count; subscription metrics (they exist in the subscription module but prometheus_metrics never calls handler.subscription_metrics()); per-KG labels. memory_usage_bytes is a crude estimate total_tuples*64, not RSS, misleading for capacity planning. The ops guide (deployment.mdx) documents only /health, /live and /ready and never mentions /metrics/prometheus (that it exists, needs a Bearer key, or what to alert on). Fix: (a) add counters at the reject/error sites and export them; wire subscription_metrics() in; add WAL/flush gauges; label key gauges by KG; replace the fake memory gauge with RSS. (b) A "Monitoring" doc section: endpoint, auth, scrape config, metric list and recommended alert thresholds.
What Changed
/metricsand/metrics/prometheus(and their/v1versions) now sit behind a newrequire_adminroute middleware. Only an unscoped API key belonging to an admin user can read them. Viewer keys, editor keys and KG-scoped keys get a 403. The auth middleware now attaches the authenticatedPrincipalto each request so this check can be made. A non-admin.statusnow shows only how many knowledge graphs the caller can see, and leaves out server-wide totals.ServerMetrics(src/protocol/metrics.rs), exported on/metrics/prometheus:kg: relations, tuples, views, loaded, sessions.inputlayer_memory_usage_bytesnow reports process RSS instead of the oldtuples*64estimate.deployment.mdxhas a new Monitoring section covering the endpoint, admin-key auth, scrape config, the metric list and suggested alerts. The REST API, authentication, persistence, commands and OpenAPI docs (and the bundled GUI docs) are updated to match. New tests:tests/metrics_tests.rs, plus router tests that check non-admin keys are refused and the new counters are exported, and a scoped-role test for.status.Risk Assessment
✅ Low: The cross-tenant leak is closed at the route layer.
require_adminusesPrincipal::role(), which already downgrades scoped admin keys to editor and re-reads the role on each request, so demoted admins lose access. The user-decided.statusscoping goes through the singlerun_execute_programchoke point that every caller uses. The new metrics are lock-free counters or read-only snapshots, and their tests exercise real routers and servers.Testing
I drove the real server end to end over HTTP and WebSocket in disposable instances (no prior baseline commands had run). The first instance covered the access matrix for the four metrics routes, the per-caller
.statusoutput, and the contents of a scrape during live load. Two more runs triggered rate-limit (429) and connection-limit (503) rejections and a persist flush. Every observed value matched expectations: memory RSS equal to /proc VmRSS, per-graph tuple counts, counters going up on writes, subscriptions and failed auth, and 403 for every non-admin key while the same key still worked for normal queries. The new focused integration and unit tests also pass, but they are automated tests rather than live runs, so that scenario is recorded as untested under the live-validation contract. Docs are the only other part of the change; I checked that the Monitoring section's metric names match the live output, but did not render them, since there is no UI change to screenshot. All temporary servers, data and the build output were removed.cargo testruns, which are automated tests and not a live run of the product, so it did not establish a live result. The live behaviour these tests…Evidence: Metrics endpoint access matrix (401/403/200 by caller and route)
Evidence: .status output per caller (scoped and non-admin keys see 1 graph and no query total; admin sees 4 graphs and the total)
Evidence: Full Prometheus scrape under load (subscription, WebSocket limit hit, rejections)
Evidence: Memory gauge equals /proc VmRSS
Evidence: WAL, subscription and auth-failure counters before and after a write
Evidence: 429 rate limit counted, flush counter
Evidence: 503 HTTP connection limit counted
Evidence: WebSocket upgrade over the limit refused (503)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
🔧 **Rebase** - 2 issues found → auto-fixed ✅
docs/content/docs/guides/rest-api.mdx- merge conflict rebasing onto origin/maingui/lib/docs-bundle.ts- merge conflict rebasing onto origin/main🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
cargo testruns, which are automated tests and not a live run of the product, so it did not establish a live result. The live behaviour these tests…Builtcargo build --bin inputlayer-serverand ran it with an isolated config and data dir under /tmp (ports 18931 and 18932), bootstrap admin key from an env varOver WebSocket (Node built-in WebSocket driver ws.mjs): created graphs tenant_a and tenant_b with facts, viewer and editor users with per-graph access (keys issued while the users were admins, then demoted to viewer/editor), an admin key scoped to tenant_a, and an unscoped admin keycurlaccess matrix: 7 callers (no key, bad key, viewer, editor, scoped admin, unscoped admin, bootstrap admin) × /metrics, /metrics/prometheus, /v1/metrics, /v1/metrics/prometheus.statusover WebSocket as the scoped admin key, the viewer key, the editor key and the unscoped admin keyScraped /metrics/prometheus with a subscription open, 3 WebSocket connections (limit 3) and a 4th upgrade refused with 503; checked the kg-labelled gauges, subscription, WebSocket, rejection, auth-failure, WAL and persist metricsCompared inputlayer_memory_usage_bytes and the /metrics JSON memory_usage_bytes with /proc/<pid>/status VmRSSScraped before and after a subscribed write and a failed password login: the subscriber got its delta; tuples, WAL bytes, subscription evaluations and password auth failures went upSecond server (per_ip_max_rps=5, buffer_size=2): sent a burst of 20 GET /health, checked the 429 and flush countersThird run (max_connections=1, no per-IP limit): 60 concurrent scrapes, checked the 503 and http_connection_limit counterscargo test --test metrics_tests(3 passed)cargo test --lib -- status_shows_only_the_knowledge_graphs test_metrics(3 passed)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.