Skip to content

feat(providers): Prometheus over its HTTP API, PromQL read-only with zero runtime dependency (#424) - #1104

Open
cevheri wants to merge 40 commits into
mainfrom
feat/prometheus-provider
Open

cevheri wants to merge 40 commits into
mainfrom
feat/prometheus-provider

Conversation

@cevheri

@cevheri cevheri commented Sep 23, 2026

Copy link
Copy Markdown
Member

Implements #1085, part of #424.

Adds the prometheus provider: PromQL over the Prometheus HTTP API, no driver dependency, read-only. The explorer lists metrics, rule groups, rules, scrape pools and targets. VictoriaMetrics is measured as a relative, partial at 20 of 36 surfaces. Reference: docs/providers/prometheus.md.

Nothing under src/lib/db/providers/ outside timeseries/prometheus/ changes. The endpoint checks come from the shared src/lib/db/http/endpoint.ts of #1086.

Release notes for the published package:

  • queryLanguage, QueryTab["type"] and the language prop of QueryEditor gain "promql".
  • New optional fields: DatabaseUIConfig.fieldLabels and fieldHints, and ProviderLabels.tableStatsCaption.
  • SchemaExplorer offers Profile Table only for "sql", or "json" with no queryDialect, and Generate Code only for "sql" and "json". So Profile Table is gone on Redis, LibreDB and Prometheus, and a host that passes no capabilities sees neither.
  • SchemaExplorer offers Generate Test Data only where the kind acceptsRowWrites and the engine supportsInlineRowEdit, as the desktop tree already did. With the shipped metadata it is gone from views and from every MongoDB, Couchbase, Cassandra, ClickHouse, Druid, Trino, Elasticsearch and OpenSearch row.

Chart 0.1.68 is metadata only; the image it installs has no Prometheus provider until the next release.

Verified locally: all CLAUDE.md gates plus build:lib, attw, coverage, gitleaks and the two E2E specs; a browser pass on Prometheus 3.13.3 and VictoriaMetrics; plan mode drafting PromQL that ran unchanged; and SQLite, PostgreSQL and SQL Server regressed with least-privilege users.

For the maintainer: this PR changes the engine count in CLAUDE.md from sixteen to seventeen, as earlier provider PRs did.

…idence

The provider is written against captured answers rather than a reading of the docs (#1085, section 9, gate 4). This adds the prometheus, prometheus-auth and victoriametrics services pinned to the probed builds, a tracked configuration whose jobs and rules put every object kind, status and identity edge of section 4 in front of the server, and the verbatim answers of every endpoint the provider calls, on Prometheus 3.13.3 and on VictoriaMetrics v1.152.0.

The PromQL lexer's word table and the parser's function table are taken from the source at the v3.13.3 tag, and every lexer word is asked bare and as a __name__ selector, so the reserved-word set and the editor's word lists are pinned to the parser rather than to a reading of it.

tests/fixtures/prometheus/README.md records the provenance and the measurements that confirm or replace the provider's measured constants, each with the rule that decides it. tests/helpers/prometheus-fixtures.ts is the one reader every test loads the fixtures through.
The connection dialog picks a field's words with one boolean per engine, and #1085 section 3.3 decides the next engine declares them instead. DatabaseUIConfig gains optional fieldLabels and fieldHints keyed by connection field, read through connectionFieldLabel, which falls back to the caller's own word, and connectionFieldHint. Every connection-field label the dialog draws now reads the declared label first and keeps its own word, the per-type chains included, as the fallback; a declared hint renders under its field as a muted paragraph with an id the input names in aria-describedby. No shipped entry declares either, and a census over every shipped type pins that each draws the labels it drew before; the per-type booleans and their hints stay as they are (#1085, sections 3.3 and 6.1).
request.ts is how a Prometheus request leaves the process, and knows
nothing about Prometheus. Plaintext goes through the global fetch, read
at call time. TLS goes through node:https, because fetch cannot take a
custom CA, a client certificate or rejectUnauthorized without an undici
Agent; the TLS panel maps onto its options by the Couchbase rule,
written again because this provider touches no other provider, and
url.urlToHttpOptions lets an IPv6 literal connect.

Both paths refuse every 3xx through the shared rejectRedirect, even
when the redirect's body has already failed; count each body as it
streams and tear it down past the request's byte limit; tell a deadline
from a cancellation by the signal's reason; and keep header values,
bodies and the runtime's own messages out of every failure.

The tests run real handshakes against loopback servers. The two private
keys they load are committed as JWK, because Secret Scan flags a PEM key
and a .gitleaksignore fingerprint covers one commit only; generate.sh
remakes the whole set.
The provider's modules code against the seam types instead of the HTTP API, so the wire stays in one file and every shaping rule is testable from hand-built values. The error carries a category rather than an HTTP status, because the engine's statuses do not classify: canceled answers 499 and unavailable falls through to 500.

promql.ts writes every selector, matcher and label-values path segment built from a name. The v3.13.3 lexer reads inf and nan as numbers and its grammar refuses six keywords as metric names, atan2 and on among them, so every word of the lexer's table and every name that is not a legacy identifier becomes a __name__ matcher; the set is pinned to the table taken from the tag, and each matcher to what the live server answered. Strings are JSON.stringify's text with U+FFFD escaped, the one character the lexer refuses as a literal. A path segment uses the U__ encoding the server undoes, so no name can add a path segment or a query, and a name its decoder cannot read back is refused (#1085 sections 3.1, 3.6 and 5.3, #1085 S4).
Rule evaluation and API queries share one engine and one active-query tracker on the monitored server, whose --query.max-concurrency defaults to 20, so every query a connection runs, tree clicks included, competes with the server's own alerting rules for those slots (#1085 S6).

A connection now runs at most QUERY_CONCURRENCY_LIMIT queries at once, 4 as M11 measured, and queues the rest first in first out. A finished query hands its slot straight to the oldest waiter, so a newcomer cannot overtake the queue, and a query that fails frees its slot like any other. A query cancelled while it waits leaves the queue without running and rejects with its signal's reason, and an already-aborted signal is refused at once. A limit below one or a fraction is a RangeError, since it would deadlock or admit one query too many.

The tests read rejections with a plain await, not expect().rejects, which on bun 1.4.2 spins past the per-test timeout on a promise that never settles and gets the whole file killed unreported.
…language

ProviderCapabilities.queryLanguage gains promql and QueryTab gains the promql tab type. resolveTabType and editorLanguageForTabType answer it explicitly, because both ended in an SQL fallback a third language would fall into, and the agent answer card, which reads the same pair, tints a PromQL block indigo, a hue no verdict uses.

A Monarch tokenizer for PromQL is registered beside the LibreDB and Redis languages. The test pins its word lists to lex.go and functions.go as captured at v3.13.3, and a # comment is coloured wherever v3.13.3 reads one, a range bracket after its first duration included.

The readers already right for promql are pinned, each beside a control: the statement splitter stays off, the plan guard declines, the plan contract takes its neutral arm, the toolbar withholds its SQL-only controls, a promql tab takes the non-SQL icon, and the promql id is not one the installed Monaco bundle registers. Comments and guides that named only the old languages are corrected. Both unions are published, so the widening needs a release note (#1085, section 3.2).
…es and grid cells

A vector is one row per series with a field per label. A matrix is wide, one row per instant and one column per series named by the labels that tell the kept series apart, so the chart tab draws one line per series unchanged. NaN, +Inf and -Inf stay the engine's text, because JSON would carry NaN as null and null means no sample in the wide grid. A label that is not a legacy name, or is named timestamp or value, is JSON-quoted as its field, so no two labels share one, and vectorFieldNames is exported so the tree's metric columns and the grid's fields come from one definition (#1085, section 5.3).

Series are cut at the cap in engine order. The transport asks for one series more than the cap and flags the engine's own truncation warning, so a cut shows even when the engine drops that warning. A matrix is also held to MATRIX_SAMPLE_BUDGET grid cells, because samples undercount the grid: the captured up[5m] over four targets is 29 samples and 116 cells. Each cut is named in a warning and marks the result limited (#1085, section 5.4, M3).
… wire

The transport implements the seam over the Prometheus HTTP API and takes its request function and URL builder by injection, so tests drive it with a recording fake, and one sends through the real sender to a local server (#1085 3.1, #1085 S4).

Credentials a header cannot carry are refused before any request, naming the field and never the value (#1085 S3). Each call of #1085 4.6 and 6.2 carries exactly its parameters, a deadline and the byte cap, and the query asks for one series more than it shows, because the engine can drop its own truncation notice.

Failures are classified by errorType, never by HTTP status, and a body that is not the envelope never reaches a message (#1085 5.5); the shared redirect refusal passes through unchanged (#1085 S2). Members v3.13.3 always writes are required, so a malformed answer is a protocol failure rather than a guess.
The TSDB status lists label names by value count and cuts the list at its limit without a warning. The cut drops whole entries, so a listed metric-name entry carries the full count however long the list is. metricNameCount reads that entry. It returns 0 only for an uncut list without the entry, gives no count when a full list leaves the entry out, and never returns the list's length. When there is no count, the overview refuses rather than guessing (#1085, M2).

The overview reports the engine's own version and the published web.max-connections ceiling. Its uptime runs from the runtime start time to the server's own time, so a skewed local clock cannot move it. What the API does not publish reads N/A or is left absent, not a zero nobody measured. The Tables panel lists the top metrics by head series, and the Storage panel shows the head block's series, chunks, span and retention with no byte figure. Health names the first probe that answered neither 200 nor the /-/healthy 404 a /health fallback replaced. It finds that 404 by position, because the module names no endpoint, and then reads the build info to check that the query API answers (#1085, section 6.2).
POST /api/db/profile sent every language that is not SQL a MongoDB aggregate document, which a PromQL, Redis or LibreDB connection cannot read. The code generator would also model PromQL label names as a stored record.

offersColumnProfiling and offersCodeGeneration now sit beside maintenanceControl in src/lib/db/types.ts. Both row menus and the route ask them, and before any query the route refuses with a 400 CONFIG_ERROR that names the declared language, never the request's table or columns. No shipped engine's desktop menu changes.

The mobile row menu offered Generate Test Data on every addressable row, including views and engines without a grid row edit. It now asks the desktop tree's row-write rule (maintainer decision D-M in #1085), and unknown capabilities offer none of the three row actions.

The docs, backlog entries and docblocks that described the old gates or cited moved lines now say what is there, including that LibreDB declares no maintenance operation (#1085, section 3.2).
… record

Adding prometheus to DatabaseType makes every exhaustive record name it, so each gets its entry: the connection picker with its icon and declared password label and hint, the showcase rank, the shipped and external registries, the fence tag plus a promql alias that names no engine, the SQL export's literal and binary forms, the non-SQL grammar set, and the migration generator's refusal to write table DDL. The seed enum gains the id, and a test now holds it to SHIPPED_DATABASE_TYPES, because a zod enum missing an id compiles and is then refused at startup.

EXTERNAL_DATABASE_TYPES moves to 17, so the ten listings the catalog copy gate counts now say seventeen and name Prometheus, and the marketplace fixture pins the nine engines its sentence really named. The dialog mock mirrors the entry, and three sentences saying no shipped entry declares field copy are corrected.

The census expectations are written first, as #1085 sections 4.1 and 4.4 state them; the censuses that build every provider through the factory stay red until the provider class and its factory case land. No provider file is touched (#1085 section 3.3).
A tree click builds a statement and runs it, and on a PromQL connection both generators fell through to their SQL returns: a click on a metric sent SELECT * FROM <metric> to the engine, and Generate Query wrote a SELECT with a LIMIT.

Both now write the metric's selector through metricSelector, the one PromQL builder, so nan, Inf, sum and any name carrying a quote, a backslash or a line break become a JSON-quoted __name__ matcher. The click runs the selector alone. Generate Query keeps it as the only runnable line, with the range forms of #1085 section 5.1 as # comments above it, so no name can end a comment early and leave a second expression.

The tests pin each #1085 S4 name to one selector that decodes back to it, and classify every export for a PromQL connection: the quoting helpers stay unreachable behind the profile route's refusal, both row menus' gates and the import dialog's row-write rule, pinned here beside a writable control.
…s error classes

Section 5.5 of #1085 classifies a Prometheus failure by its category, never by its HTTP status, which does not classify: the engine answers canceled with 499, timeout with 503 and execution with 422. Each category becomes its class with the message it arrived with; an errorType this build does not know is a QueryError in the engine's words, and anything else goes through mapDatabaseError, which passes a DatabaseError through unchanged, so the shared rejectRedirect's ConnectionError reaches the caller as written (#1085 S2).

Two messages are built here. A TLS failure is named once by its Node error code, which a ConnectionError has no field for (#1085 S8). An answer past the byte cap names the cap and says to ask for a narrower range or a larger step, advice request.ts cannot give because it knows nothing about PromQL (#1085 S5).
Six kinds with no container level, in the order the agent walk reads them. Identity is the engine's own: a rule group is keyed by file and name, a rule by position and name, a target by scrape URL and a digest of its labels, because only those keys survive duplicate names (#1085, section 4.1).

One reader per kind feeds both the count and the listing, and the rules listing behind three kinds is read once per count. A capped metric listing reports a floor, and a refused listing its own sentence, whether the transport refused it or the sender refused a redirect (#1085, section 4.3).

Metric columns come from the field-naming function the grid uses, so the tree and the grid agree. One series read describes the whole folder, and the caller's bound and the series cap each mark the batch instead of presenting a cut list as complete (#1085, section 4.2).

Every source is JSON.stringify of the engine's values, so server text cannot forge structure. Existence comes from the listing, or from a count over the listing's own hour, and only an alerting rule sends a second read, for its live alerts (#1085, section 4.4, #1085 S7).
PrometheusProvider holds only lifecycle and declarations. connect() validates the host and port through the shared endpoint module of #1086. It then builds the HTTP transport, which refuses a credential that cannot go into a header before anything is sent, and proves the server with one buildinfo read.

query() sends the editor text unchanged and asks for one series more than the cap, so a cut is always seen. It refuses bound values and a text that holds only comments, takes one of the connection's query slots, and keeps an AbortController per query. That lets disconnect and cancelQuery reach a query that is running or still waiting. cancelQuery exists because the M1 measurement saw an abort end the evaluation on the server. A result cut at the series cap reports it on its own pagination (#1085 section 5.4). The object surface's existence read waits for the same slots, because it is a PromQL evaluation too (#1085 S6).

Every surface delegates to the module that owns it, and every failure arrives through the errors module. The capabilities and labels are those of #1085 section 6.3, with the row labels in the lower case every provider uses.

The factory gains a prometheus case, using the explicit /index import every multi-file provider uses. The unknown-type message names prometheus in case order, which the factory test pins. With the case in place, the censuses registered with the type-id can build the provider, and they pass: the column, source, language, pagination and edit declarations, the union parity of the agent engine list, and the UI config's write-list check.

The case moves the tunnel branch nine lines down, so the four factory.ts line citations the fingerprint guard holds move with it. It also moves two that no guard holds and that were already stale: the idle eviction cited in object-edit-plan-token.ts and the path.resolve cited in the factory test. They now name the lines they mean. The one test left red is the abstainer's doc section, which the provider doc adds.
The route rebuilt pagination from prepareQuery alone, so a provider that bounds its own result, the Prometheus series cap of #1085 section 5.4, had its wasLimited overwritten and the limited badge never showed. The response now carries the limiter's value or a provider's own true. hasMore stays on the limiter's bound, because only a bound this layer wrote can be advanced by an offset. No other provider sets pagination, so no other engine's response changes. API_DOCS.md, the query optimization guide and the export scope's comment said wasLimited was the limiter's alone and now say what else sets it; the export dialog's behaviour is unchanged.
…vider

Two seams hold the provider together (#1085 3.1, 3.5): the wire vocabulary lives only in http-transport.ts, and fetch and the HTTP, TLS and socket modules only in request.ts, which puts the refused redirects, the byte cap and the deadline on every request. The guard parses every file in the directory, proves each token is spelled where it is allowed, and flags it anywhere else; the words the seam kept as its own field names are flagged only where document parsing spells them, and a module only where a module is named, with or without the node: prefix, so a scheme string is not mistaken for one.
…name it in control 0.6

A connection's host and port go through httpOrigin and every request URL through endpointUrl, the module the other HTTP transports adopted in #1086. So a host or a port carrying a path, a query or a fragment is refused with that module's DatabaseConfigError before any request, and neither the error nor the log line repeats anything of the value. The provider keeps no validation of its own.

The test drives the provider the factory builds against a real local server. Each refused value is one a URL made by string concatenation would send to that same server, and each test shows it does, which is what makes its zero hits mean refused rather than misrouted. The refusal is compared with what the shared module itself answers, so the file fails the day the provider validates on its own. A valid origin reaches that server's /api/v1/query, an absent port is 9090, an IPv6 literal is bracketed, and the scheme follows the TLS panel.

Control 0.6 of docs/SECURITY.md says an HTTP-based connection's host and port address only that server and that no such transport follows a redirect. It now names the Prometheus files that enforce it, index.ts for the endpoint and request.ts for the redirect refusal on both request paths, and the two tests that pin them, which security:check holds to existing and running.
The integration test of the provider triad. The provider is built with no dependency injected, as the factory builds it, and globalThis.fetch is replaced per test by a fake that answers each request from the capture of that request, by path and parameters, so the endpoint binding, the request function, the wire, the limiter, the shapers, the object surface, the monitoring mappings and the error table all run together against Prometheus 3.13.3's own answers.

Three answers are selected from a wider capture with the server's own filter, and each selection is held to one real filtered capture, as the inline basic-auth 401 is held to its own, so the fake cannot drift in silence. The file also pins what no unit test can see: one series read per describeObjects call, the credential refusal of #1085 S3 and the read-only endpoint set of #1085 S9 end to end, the three identity edges of the compose rule set, and the grid's fields equal to the tree's columns for the same metric.
… range

The old query-matrix-subquery capture was taken 43 seconds after the server
started. That left rate(prometheus_http_requests_total[1m])[5m:30s] with one
evaluable step, so every one of its 63 series held a single instant, and the
matrix tests that claim to cover a stepped subquery ran over one row.

The new capture comes from the same compose server after three hours of
uptime. It holds 70 series, each sampled at the same ten instants 30 seconds
apart. The transport test and the provider test now check that shape
before they check the decoded result, so they fail if the capture ever goes
back to one instant, and the provider test checks for ten rows with a sample
in every cell.
….1.68 and a dialog E2E spec

docs/providers/prometheus.md completes the tri-sync triad for the Prometheus provider (#1085). A new test reads its measured constants, dialog strings, cancellation claim and object-surface sentences back from the code, so the doc cannot drift silently, and the object-edit census is green again.

The backlog files what this work found and did not fix, each entry reproduced or cited first: D104 to D109, U36 to U42, B84 and B85, with the B entries cited from docs/AGENT.md and D37 naming the second TLS implementation.

Every hand-kept count and engine list of the type-id half is re-derived from its denominator rather than incremented: the reference docs, the six READMEs, the Docker Hub listing within its byte budget, the brand facts, the storefront listings, and the chart and operator metadata. Chart 0.1.67 is released, so 0.1.68 is bumped by hand and the operator copy and bundle come from their generators. Comments that stated a population as a numeral now name the set they count.

The E2E spec pins the connection dialog in a browser, with PostgreSQL and MongoDB controls so neither negative is vacuous.
The gate-4 probe called every surface separately through createDatabaseProvider against victoriametrics/victoria-metrics:v1.152.0, with Prometheus 3.13.3 as the baseline in the same pass. A surface that answers empty where Prometheus answers data counts as not answering, the ScyllaDB rule, so 16 of 36 answer and the entry is partial. Each caveat is one the probe earned: three status endpoints it does not serve, three documents without a field the provider requires, empty rule folders on a single node, a string expression with no rows, and no PromQL notices. The advertised 2.24.0 stays in the version string only, because the overview that would show it fails there.

The docs table, the provider doc's per-surface section and the test carry the same tier, version and caveats. The relatives count moves to twenty-seven and the connectable total to forty-four, re-derived from compatibility.ts, and the Docker Hub listing drops its hand-kept row count to stay inside its byte budget. The unreleased chart 0.1.68 gains a victoriametrics keyword, with the operator copy from chart:bump.
…vider's bounds

The byte cap bounds the wire body, but a vector's grid is its rows times the union of their label names, so a legal answer of about 200 KB could shape into millions of cells and exhaust the heap of the one process every user shares (#1085 S5). The vector and matrix shapers now hold the rows and fields to RESULT_BYTE_BUDGET bytes of JSON, counted exactly before any row is built, keeping whole series in engine order and naming the cut in a warning. A series kept alone from an answer that held several keeps the labels that tell it apart, instead of the bare name value.

The provider tests now reach what the provider hands its transport and its limiter: a TLS connection through the default request function verifies the certificate, every request carries the byte cap and a deadline, a matrix cut by the cell budget says so on its own pagination, and time spent waiting for a query slot is not reported as the engine's time.
A promql block was a query tag that named no engine, so it could not contradict the connection. On a PostgreSQL plan run, a PromQL block written before the SQL was recorded as the run's statement, stamped postgres and judged by the SQL guard. A closing holding only PromQL was scored answered without the drive ever asking for the statement. An alias names the type-id a block's text runs on, and every PromQL server this product reaches, VictoriaMetrics included, connects through prometheus, so promql now names prometheus. A Prometheus run still takes a promql block, and the rail's editor offer is unchanged (#1085, section 3.5).
…'s cancel

The plaintext rows of "a redirect whose body never ends ... let go" and
"an answer that keeps streaming ... torn down" waited, with no bound, for
the server to see the connection close. Bun closes the connection of a
Response it collects, half a second to a second later, so both rows
passed with the redirect body cancel in sendPlain or the reader cancel
in readCapped deleted. Production runs on Node, whose fetch never closes
a body read through a reader on its own, so that regression would hold
a socket open on every over-cap answer, and on every refused redirect
until a collection runs.

Both tests now keep every fetch Response reachable until afterEach
restores fetch, check that the hold took the plaintext answer, and fail
with a message naming the open connection when the close has not come
two seconds after the send settled. Deleting either cancel now fails its
row; the real code closes the connection within milliseconds.
A metric may legally be named update, delete or drop, and a tree click or Generate Query runs its bare selector. The confirmation gate read that text under the default SQL grammar, so a read-only PromQL selector opened the Query Safety Check dialog and posted the text for AI analysis, on both execution paths. PromQL has no write path (#1085, section 2): editor text only ever reaches POST /api/v1/query.

NON_SQL_DESTRUCTIVE_VOCABULARY gains a prometheus row that names no operation and declares itself the gate's whole answer, and isDangerousQuery asks that row before its SQL keyword test. MongoDB and Redis keep the keyword test in front of their rows, so their reading is unchanged. A new test holds the vocabulary table to the set readsSqlText reports as not SQL, so adding a type to one table forces a decision about the other.
…d shell's tabs

@monaco-editor/react calls the first render's onMount once, so the run and format shortcuts, the Run Query and Format SQL actions and the blur sync kept the mounting render's language, grammar, Monaco namespace and onChange. Both shells mount the editor on an SQL tab and retype it later, so on a PromQL tab Shift+Alt+F ran the SQL formatter (up == 0 became up = = 0, with the undo history cleared) and Cmd+Enter could cut an expression at a semicolon inside a # comment. They now read a ref refreshed after every commit, as Explain does (#200), and Format SQL is offered on SQL tabs only. This also fixes older SQL cases: Cmd+Enter sent the whole buffer after a fresh load, and split under the previous engine's grammar after a connection switch.

The embedded StudioWorkspace never retyped its tabs, so every non-SQL host connection opened Query 1 as SQL with an SQL Format button. It now types open tabs from the declared capabilities like the standalone shell, keyed on the connection id and the resolved type so a host passing fresh objects causes no write (#1085).
…ope errors

VictoriaMetrics v1.152.0 sends metadata entries without a unit, targets without a scrape interval or timeout, and a TSDB status without head statistics. The strict decoders refused each answer whole, so a metric's Source tab, the Targets folder and the Tables tab failed over a member that only describes the object. Members that identify or classify an object stay required. A descriptive member an engine was measured to omit now decodes as absent and stays out of the source JSON. When no head statistics came, the storage row is refused as unmeasured rather than filled in.

A body that is not the API envelope was reported as a proxy or a login page answering. But Prometheus's own ready gate answers 503 without an envelope while it starts up or shuts down, so a restarting server sent users looking for a proxy. The message now names the path and the status, and offers the possible sources without choosing one.

Two tests could not fail. None proved that the query deadline still fires beside the caller's signal, which every production query carries, and none held the listing boundary at exactly the cap. Each now fails on the one-line mutation it missed.
…badge

The Tables tab counted the 50 metrics with the most head series as the database's tables ("Tables 50, 858 rows" beside an Overview of 344 metrics), and the agent's table-stats reading gave a model the same 50 rows as a count. ProviderLabels gains an optional tableStatsCaption, which the dashboard now passes to the Tables tab. The tab shows the caption above its cards and counts the rows as listed. A search that finds no listed row no longer claims the table does not exist. The agent reading puts the caption in the header the model reads. Prometheus builds its caption from TSDB_TOP_METRICS and says "at most", because VictoriaMetrics ignores the limit and lists ten. An engine that declares no caption renders as before (#1085 6.2).

The limited badge said "Rows beyond the bound were not fetched.", which is true of the SQL limiter only. The Prometheus cell budget and series cap leave out series the provider already fetched, and in a matrix those series are columns. The new sentence claims neither rows nor fetching, and wasLimited is still set on every truncated result (#1085 5.4).

Also adds a test through the real composition proving the byte budget reaches the result shaper, and renames the stale TSDB head field names in the seam guard.
… decode as absent

The VictoriaMetrics entry, its test and both doc rows still described the pass in which the decoders refused its metadata, targets and TSDB status whole. A new probe, surface by surface through createDatabaseProvider against v1.152.0 with Prometheus 3.13.3 as the baseline, finds the Tables tab, a metric's Source tab and the Targets folder answering, so the caveats now say what a user sees there: ten top metrics, no unit, no scrape interval or timeout. The caveat on the three paths VictoriaMetrics does not serve quoted the old non-envelope message.

Two differences are recorded for the first time: a target not yet scraped reads as down, and a subquery's points are counted back from its evaluation time, which is also why MetricsQL is no longer called a superset of PromQL. The tier stays partial. 20 of the 36 surfaces outside the editor answer, counting an object count with an unavailable count the way a monitoring read with a failed panel was already counted.
…r reports

On a VictoriaMetrics connection the transport, the monitoring readers, the object surface and the error mapping all said that Prometheus answered, refused, reported or held something, which is false there: this provider serves wire-compatible relatives unchanged. Those messages now name the server, as the result shaper already did, and the product's name stays only where it names this provider, in its own declarations and in refusals of a caller's arguments.

The VictoriaMetrics table in the provider doc quotes three of these messages, so it is regenerated from a fresh probe of both servers. That pass named /api/v1/status/flags in the overview's message where the previous one named /api/v1/status/runtimeinfo, so the sentence under the table now says the path can differ from one call to the next.
…e fixed code

The review fixes changed what these files describe: the result byte budget, the promql fence alias, the confirmation gate's own Prometheus row, the Tables caption, the reworded limited badge, the editor's run and format behaviour on a PromQL tab, and descriptive members read as absent. The provider doc now states each, and states the chart as it behaves: a missing sample, and a NaN or Inf among numbers, is drawn at 0, so a raw range charts false zeros while a stepped subquery charts correctly.

It also corrects claims that were false: Prometheus as the only engine that is not SQL, a raw U+FFFD shown where the escape was meant, the reason given for the metric cap in the doc and in its docblock, a chart changelog promising a provider its default image lacks, stale driver-free and population counts, the engine count in CLAUDE.md, a conformance test title, and the Rancher corrections table. The doc test now pins the byte budget, the escape, the Tables caption and the storage refusal.

The backlog records what the review measured and this change leaves: the chart's zeros, the Redis keyword prompt, the byte cap against the image heap, the cql fence alias, provider comments and docs that count the old fleet, and the lagging translated TLS paragraphs; D105 now covers the table size too.
The Prometheus dialog told the reader to "Leave User empty to send this as a bearer token", and a refused credential says "Check User and Password or token", but the field itself was labelled "Username", the dialog's word for every engine. Found in the browser pass. The engine now declares the label beside its password label, so the three agree, and every other engine keeps "Username".
A series kept alone is named by the labels that tell it from the whole
answer, while two or more are named among themselves, so one series
alone can cost more than two. The byte budget stopped at the first
prefix that did not fit, so a wide label union left an empty grid
(kube_pod_labels[1d:1m]: 0 of 173 series, where 10 fit), and a raw
range the cell budget cut to one series was dropped whole because its
name repeated in every row (node_load1[30d]: 0 rows, where 172,800 fit
under value).

The walk now keeps the longest run of two or more series that fits and
prices one series only when no two fit: under its name when that fits,
else under value with one notice naming the series once. The byte loop
returns the names it priced and the grid writes exactly those, so the
count stays exact. New tests pin the held argument through the cell
budget and the series cap, and the shared-label terms of both
estimates, which one-line mutants used to pass.
The admin Operations tab reads the same getTableStats rows as the monitoring Tables tab but ignored ProviderLabels.tableStatsCaption, so on Prometheus it titled the 50 metrics with the most head series "Tables (50)" and answered a filter for a metric outside them with "No tables found." (#1085 6.2). It now reads the caption as the Tables tab does: over a list that is there, the caption heads the rows, the panel reads "Listed (N)", and a filter that misses every row of a non-empty list says "No listed table matches the filter."; a refused or empty list keeps its text. The caption's docblock names the new reader.

The transport test's table of non-envelope bodies checked no source sentence, so changing notAnApiAnswer's 503 check to status >= 500 passed every test while telling a proxy's 502 that Prometheus was starting up. Each row now names its sentence and pins the whole message, and a new 504 row covers the other side of the equality.
…second review found

The chart sentences in the READMEs and FEATURES.md said a stepped subquery draws one line per series. The tab opens on the first series only, more are added from the Y-Axis menu, and it draws at most eight at once. U44 now gives the real condition under which a NaN-heavy column becomes categorical and the default x axis: at most 50 distinct values. Past that it is typed unknown.

The backlog records what the review measured and this change leaves as it is:
- U45, U46 and B87 from the browser pass and the local gates.
- U47: the embedded StudioWorkspace has no listener for the editor's run event.
- D111: MySQL's uncaptioned top-100 table list.

DOC6 now names each of the eight stale fleet-count sentences and gives the real reason they are left. The QueryEditor comment no longer claims the Cmd+Enter symptoms in the embedded shell, where the shortcut never ran anything.
…ions list

The Operations list printed every row as schema, a dot, then the name, so a row whose schema is empty read with a leading dot: a Prometheus metric as ".go_gc_duration_seconds", and an Elasticsearch or OpenSearch index the same way. A row with a schema still prints it before the name.
…doc to the code

RESULT_BYTE_BUDGET rested on figures only its docblock held. The local harness now bundles the provider and runs it in the image's runtime under the chart's 512Mi limit, and every answer shaped against the budget completed while three quarters of the heap was held live, so 16 MiB stays. The fixture README records the rule, the answers and the decision as M14, the provider doc gains its row, and provider-doc.test.ts checks each measured constant against its recorded decision, so a constant moved without a new measurement fails.

The same measurement shows that nothing bounds the responses waiting to be sent (D112), and the notice naming a series kept under `value` sits outside the budget (D113).

The provider doc and the monitoring docblocks now state the lone-column rule, the admin Operations list as a caption reader, the embedded shell's run controls, and what the chart tab draws and how it types a NaN-heavy column, with those chart sentences pinned against the chart's own analysis.
@gitguardian

gitguardian Bot commented Sep 23, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 13 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
36754982 Triggered Generic Password 4129081 tests/integration/db/prometheus-provider.test.ts View secret
37481047 Triggered Generic Password 7e54e9b tests/components/ConnectionModal.test.tsx View secret
37558113 Triggered Base64 Basic Authentication 268893f tests/unit/db/prometheus/http-transport.test.ts View secret
36754981 Triggered Generic Password 268893f tests/unit/db/prometheus/http-transport.test.ts View secret
37481047 Triggered Generic Password 7e54e9b tests/components/ConnectionModal.test.tsx View secret
37558112 Triggered Generic Password 268893f tests/unit/db/prometheus/http-transport.test.ts View secret
37558114 Triggered Base64 Basic Authentication 268893f tests/unit/db/prometheus/http-transport.test.ts View secret
37558111 Triggered Generic Password 268893f tests/unit/db/prometheus/http-transport.test.ts View secret
36754981 Triggered Generic Password 268893f tests/unit/db/prometheus/http-transport.test.ts View secret
36754982 Triggered Generic Password 268893f tests/unit/db/prometheus/http-transport.test.ts View secret
36754982 Triggered Generic Password 4129081 tests/integration/db/prometheus-provider.test.ts View secret
36754981 Triggered Generic Password 268893f tests/unit/db/prometheus/http-transport.test.ts View secret
37558110 Triggered Generic Password 4129081 tests/integration/db/prometheus-provider.test.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Comment thread src/lib/db/providers/timeseries/prometheus/index.ts Fixed
@codecov

codecov Bot commented Sep 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

… time

The comment-only check cut each line with /#.*$/. Before a line terminator the split leaves in place, such as U+2028, the pattern could not reach the end of the input, so it retried at every later #, which is quadratic in the length of the user's text (CodeQL js/polynomial-redos), and it left a comment the lexer ends only at \r or \n standing as an expression. Each line is now cut at its first # by index. The editor's tokenizer takes the same rule, [^\r\n]* in place of .*$, and the provider doc states it.
The vector's label names were sorted by the default comparison, which SonarCloud reads as an unintended alphabetical sort (typescript:S2871). The order is deliberate, UTF-16 code units, the same on every runtime and in every locale, so it is now stated as a comparator and pinned by a test that localeCompare fails. The Operations list title's nested ternary is split into a named noun.
@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants