feat(api): add /v1/prometheus/query_exemplars and harden the proxy - #2806
Conversation
Proxies to Prometheus's native /api/v1/query_exemplars for Prometheus-backed connections. ClickHouse-backed connections get an empty success: there is no table function for this, and exemplars for those charts are read from the metric table by the client instead. The window is bounded by narrowing, not rejecting. A 30-day dashboard range is an ordinary request, and Prometheus keeps exemplars in a small recent buffer, so the older part has nothing to return anyway — rejecting would surface as a chart-level error on a healthy chart. Genuinely invalid or inverted bounds still 400. The bounding logic is an exported pure function so it can be tested without a route. Three fixes to proxyToPrometheus, which this route is a new entry point into: - Send X-Content-Type-Options: nosniff, and forward the upstream content-type only when it is a JSON media type. The connection host is member-configured, so its body is untrusted output on our own origin — /api/* is same-origin-proxied and the session cookie is sameSite lax, so a text/html body would otherwise render as script. Pre-existing on main; fixed here because this adds a route to it. - Count proxy failures. The helper writes 400/502/504 and returns normally, so the callers' catch never fired and all four proxied endpoints reported zero errors while still recording duration. 5xx only: an upstream 4xx is usually a user's malformed PromQL, and counting those makes the metric track typos rather than backend health. - Type the integration-test upstream fixture as Response, which drops the `as any` at its call sites and brings the package back under its eslint warning ceiling. Split out of #2536. The route is self-contained — it has no dependency on the shared exemplar code, only a comment mentioning it.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 533c4c5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🟡 Tier 3 — StandardIntroduces new logic, modifies core functionality, or touches areas with non-trivial risk. Why this tier:
Review process: Full human review — logic, architecture, edge cases. Stats
|
Greptile SummaryThis PR adds a Prometheus exemplar-query endpoint and strengthens behavior shared by the Prometheus proxy.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/api/src/routers/api/prometheus.ts | Adds the exemplar route, bounded-window resolution, proxy response hardening, credential-safe target rendering, and status-based error accounting. |
| packages/api/src/routers/api/tests/prometheus.int.test.ts | Adds integration coverage for exemplar routing, team scoping, backend branching, outgoing window bounds, hardened headers, and credential redaction. |
| packages/api/src/routers/api/tests/prometheus.test.ts | Adds focused unit coverage for exemplar-window resolution, proxy outcome accounting, and disconnect classification. |
| .changeset/query-exemplars-route.md | Documents the new endpoint and shared proxy behavior changes for the API package release. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Authenticated query_exemplars request] --> B{Connection exists for team?}
B -- No --> C[404 bad_data]
B -- Yes --> D{Prometheus-backed?}
D -- No --> E[Return success with empty data]
D -- Yes --> F[Parse and bound start/end]
F --> G{Valid window?}
G -- No --> H[400 bad_data]
G -- Yes --> I[Proxy to Prometheus query_exemplars]
I --> J[Stream response as application/json with nosniff]
I --> K[Record server-side proxy failures]
Reviews (4): Last reviewed commit: "Merge branch 'main' into jordansimonovsk..." | Re-trigger Greptile
E2E Test Results✅ All tests passed • 271 passed • 1 skipped • 1041s
Tests ran across 4 shards in parallel. |
Deep Review
✅ No critical issues found. Nothing in this diff introduces data loss, an auth bypass, an injection, a leaked secret, or a happy-path crash. The nosniff + relabel hardening holds up under scrutiny: CORS is pinned to 🟡 P2 -- recommended
🔵 P3 nitpicks (13)
Reviewers (12): correctness, security, adversarial, reliability, testing, maintainability, api-contract, performance, kieran-typescript, project-standards, agent-native, learnings-researcher. Testing gaps:
|
…aults correctly Addresses review findings on the query_exemplars route. Content-type. The JSON allowlist is gone; every proxied response is now relabelled `application/json; charset=utf-8`. The allowlist was prefix-anchored, so `application/json, text/html` cleared it — which is also what Headers.get() returns for two separate Content-Type headers — while the browser's MIME extraction keeps the last essence and renders the body as HTML on our origin. Prometheus only ever answers application/json, so passing anything through bought nothing and was the whole bypass surface. Client disconnects no longer count against backend health, but the first attempt at that was wrong: it tested `res.destroyed`, and `pipeline` destroys the destination before rejecting whichever end failed, so every upstream fault was being reclassified as a user cancellation and the error counter stayed at zero for truncated bodies. The error code is the only usable signal; pulled out as `isClientDisconnect` with the reasoning attached. Also: nosniff moved to router middleware so the handlers' own catch blocks get it, not just the proxy helper; both window bounds forwarded rather than only `start`; basic-auth credentials stripped from the URL echoed in 502/504 bodies; dropped `selectPassword` where no ClickHouse client is built; and removed a comment naming a function that does not exist. Tests cover the comma-list bypass, relabelling of a valid non-standard JSON type, the narrowed window reaching the outgoing URL, the 5xx-only counting rule, credential redaction, and the disconnect discriminator.
…etheus-query-exemplars
Second of five PRs replacing #2536. Independent of #2805 — either can merge first.
What it does
Adds
/v1/prometheus/query_exemplars, proxying to Prometheus's native endpoint for Prometheus-backed connections. ClickHouse-backed connections get an empty success: there is no table function for exemplars, and those charts read them from the metric table client-side instead.The window is narrowed rather than rejected when it exceeds the cap. A 30-day dashboard range is an ordinary request, and Prometheus keeps exemplars in a small recent circular buffer, so the older part of a wide window has nothing to return — rejecting it would surface as a chart-level error on a perfectly healthy chart. Genuinely invalid or inverted bounds still 400.
Three fixes to the shared proxy
This adds a fourth entry point into
proxyToPrometheus, so it fixes what was already there:Security (pre-existing on
main, from 973d120). The proxy forwarded the upstreamcontent-typeverbatim with nonosniff. The connection host is member-configured,/api/*is same-origin-proxied by the app, and the session cookie issameSite: lax— so atext/htmlbody from a hostile host would render as script on our own origin. Now sendsnosniffunconditionally and passes the content-type through only for JSON media types. Not introduced by this work, but this widens the surface, so it is fixed here.Error accounting.
proxyToPrometheushandles its own failures by writing 400/502/504 and returning normally, so the callers'catchnever executed. All four proxied endpoints reported zero errors while still recording duration. Now counted — on 5xx only, since an upstream 4xx is usually a user's malformed PromQL and counting those makes the metric track typos rather than backend health.Test typing. The integration-test upstream fixture is typed as
Response, dropping theas anyat its call sites. Incidentally this brings@hyperdx/apiback under its eslint warning ceiling, which the original PR had exceeded.Verification
make ci-lintandmake ci-unitpass.resolveExemplarWindowwas extracted as a pure function so the bounding logic has unit tests that do not need Docker — 7 cases covering narrowing, the inverted range, missing and unparseable bounds, and ISO timestamps. The header hardening has integration tests (Docker-gated, typechecked but not executed here).