From a204a7b39f00b936cd5a47caf5c5863c876bed1f Mon Sep 17 00:00:00 2001 From: Juliano Costa Date: Thu, 15 Oct 2026 14:38:51 +0200 Subject: [PATCH 1/4] [FEATURE] Prometheus: per-query exemplars toggle (perses#3445) Add an optional enableExemplars field to the PrometheusTimeSeriesQuery spec, rendered as an Auto/On/Off button group in the query editor (same pattern as the instant toggle): - Auto (unset): follow the exemplars.enable setting of the datasource - On/Off: override the datasource setting for this query - the control is disabled when exemplars are disabled on the selected datasource or when the query is instant (the exemplars API is range-based) - a per-query On cannot override a datasource-level Off Also adds the field to the CUE schema and a EnableExemplars() option to the Go SDK query builder. Signed-off-by: Juliano Costa --- .../prometheus-time-series-query/query.cue | 1 + prometheus/sdk/go/query/options.go | 10 ++++ prometheus/sdk/go/query/query.go | 1 + .../PrometheusTimeSeriesQueryEditor.tsx | 46 ++++++++++++++++++- .../get-time-series-data.ts | 6 ++- .../plugin.test.ts | 42 +++++++++++++++++ .../query-editor-model.ts | 22 +++++++++ .../time-series-query-model.ts | 6 +++ 8 files changed, 131 insertions(+), 3 deletions(-) diff --git a/prometheus/schemas/prometheus-time-series-query/query.cue b/prometheus/schemas/prometheus-time-series-query/query.cue index 60cb39ca2..dd979ce1a 100644 --- a/prometheus/schemas/prometheus-time-series-query/query.cue +++ b/prometheus/schemas/prometheus-time-series-query/query.cue @@ -27,4 +27,5 @@ spec: close({ minStep?: =~ds.#durationRegex | =~common.#variableSyntaxRegex resolution?: number instant?: bool + enableExemplars?: bool }) diff --git a/prometheus/sdk/go/query/options.go b/prometheus/sdk/go/query/options.go index 8d7354125..72b86a51d 100644 --- a/prometheus/sdk/go/query/options.go +++ b/prometheus/sdk/go/query/options.go @@ -65,3 +65,13 @@ func Instant(instant bool) Option { return nil } } + +// EnableExemplars enables or disables exemplars for this query, overriding the +// `exemplars.enable` setting of the Prometheus datasource. Exemplars are only +// fetched for range queries. +func EnableExemplars(enableExemplars bool) Option { + return func(builder *Builder) error { + builder.EnableExemplars = &enableExemplars + return nil + } +} diff --git a/prometheus/sdk/go/query/query.go b/prometheus/sdk/go/query/query.go index 3bb505f62..90cfebdb5 100644 --- a/prometheus/sdk/go/query/query.go +++ b/prometheus/sdk/go/query/query.go @@ -29,6 +29,7 @@ type PluginSpec struct { MinStep common.Duration `json:"minStep,omitempty" yaml:"minStep,omitempty"` Resolution int `json:"resolution,omitempty" yaml:"resolution,omitempty"` Instant *bool `json:"instant,omitempty" yaml:"instant,omitempty"` + EnableExemplars *bool `json:"enableExemplars,omitempty" yaml:"enableExemplars,omitempty"` } type Option func(plugin *Builder) error diff --git a/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx b/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx index 6153f0fd4..bfc36d0be 100644 --- a/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx +++ b/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Button, ButtonGroup, FormControl, Stack, TextField } from '@mui/material'; +import { Button, ButtonGroup, FormControl, Stack, TextField, Typography } from '@mui/material'; import { PanelEditorContext } from '@perses-dev/dashboards'; import type { DatasourceSelectProps } from '@perses-dev/plugin-system'; import { @@ -42,7 +42,13 @@ import { import type { PrometheusDatasourceSpec } from '../types'; import { DEFAULT_SCRAPE_INTERVAL } from '../types'; import type { PrometheusTimeSeriesQueryEditorProps } from './query-editor-model'; -import { useQueryState, useFormatState, useMinStepState, useInstantState } from './query-editor-model'; +import { + useQueryState, + useFormatState, + useMinStepState, + useInstantState, + useEnableExemplarsState, +} from './query-editor-model'; /** * The options editor component for editing a PrometheusTimeSeriesQuery's spec. */ @@ -71,6 +77,12 @@ export function PrometheusTimeSeriesQueryEditor(props: PrometheusTimeSeriesQuery const { format, handleFormatChange, handleFormatBlur } = useFormatState(props); const { minStep, handleMinStepChange, handleMinStepBlur } = useMinStepState(props); const { instant, handleInstantChange } = useInstantState(props); + const { enableExemplars, handleEnableExemplarsChange } = useEnableExemplarsState(props); + const datasourceSpec = datasourceResource?.plugin.spec as PrometheusDatasourceSpec | undefined; + // Undefined while the datasource resource is still loading: the control is disabled + // but no "disabled on the datasource" message is shown yet. + const exemplarsEnabledOnDatasource = + datasourceSpec === undefined ? undefined : datasourceSpec.exemplars?.enable === true; const minStepPlaceholder = minStep ?? (datasourceResource && (datasourceResource.plugin.spec as PrometheusDatasourceSpec).scrapeInterval) ?? @@ -191,6 +203,36 @@ export function PrometheusTimeSeriesQueryEditor(props: PrometheusTimeSeriesQuery + + + + + + + + Exemplars + {exemplarsEnabledOnDatasource === false && ' (disabled on the datasource)'} + {exemplarsEnabledOnDatasource === true && instant === true && ' (not available for instant queries)'} + + ); } diff --git a/prometheus/src/plugins/prometheus-time-series-query/get-time-series-data.ts b/prometheus/src/plugins/prometheus-time-series-query/get-time-series-data.ts index 36deff990..923c3cf34 100644 --- a/prometheus/src/plugins/prometheus-time-series-query/get-time-series-data.ts +++ b/prometheus/src/plugins/prometheus-time-series-query/get-time-series-data.ts @@ -122,7 +122,11 @@ export const getTimeSeriesData: TimeSeriesQueryPlugin> | undefined; diff --git a/prometheus/src/plugins/prometheus-time-series-query/plugin.test.ts b/prometheus/src/plugins/prometheus-time-series-query/plugin.test.ts index 462e8727d..a5d552938 100644 --- a/prometheus/src/plugins/prometheus-time-series-query/plugin.test.ts +++ b/prometheus/src/plugins/prometheus-time-series-query/plugin.test.ts @@ -261,6 +261,48 @@ describe('PrometheusTimeSeriesQuery', () => { ]); }); + it('should not query exemplars when spec.enableExemplars is false, even if the datasource enables them', async () => { + const ctx = createStubContext(); + getDatasource.mockImplementation((): DatasourceSpec => { + return { + default: false, + plugin: { + kind: 'PrometheusDatasource', + spec: { + ...datasource, + exemplars: { enable: true }, + }, + }, + }; + }); + (promStubClient.queryExemplars as Mock).mockClear(); + + const results = await PrometheusTimeSeriesQuery.getTimeSeriesData({ query: 'up', enableExemplars: false }, ctx); + + expect(promStubClient.queryExemplars).not.toHaveBeenCalled(); + expect(results.exemplars).toBeUndefined(); + }); + + it('should not query exemplars when spec.enableExemplars is true but the datasource does not enable them', async () => { + const ctx = createStubContext(); + getDatasource.mockImplementation((): DatasourceSpec => { + return { + default: false, + plugin: { + kind: 'PrometheusDatasource', + // exemplars NOT enabled on the datasource: a per-query `true` must not override it + spec: datasource, + }, + }; + }); + (promStubClient.queryExemplars as Mock).mockClear(); + + const results = await PrometheusTimeSeriesQuery.getTimeSeriesData({ query: 'up', enableExemplars: true }, ctx); + + expect(promStubClient.queryExemplars).not.toHaveBeenCalled(); + expect(results.exemplars).toBeUndefined(); + }); + it('should keep the query working when the exemplar query fails', async () => { const ctx = createStubContext(); getDatasource.mockImplementation((): DatasourceSpec => { diff --git a/prometheus/src/plugins/prometheus-time-series-query/query-editor-model.ts b/prometheus/src/plugins/prometheus-time-series-query/query-editor-model.ts index f5b94736f..6e4d9ff9f 100644 --- a/prometheus/src/plugins/prometheus-time-series-query/query-editor-model.ts +++ b/prometheus/src/plugins/prometheus-time-series-query/query-editor-model.ts @@ -157,3 +157,25 @@ export function useInstantState(props: PrometheusTimeSeriesQueryEditorProps): { return { instant, handleInstantChange }; } + +/** + * Hook to manage `enableExemplars` state. The toggle commits immediately, like `instant`. + */ +export function useEnableExemplarsState(props: PrometheusTimeSeriesQueryEditorProps): { + enableExemplars: boolean | undefined; + handleEnableExemplarsChange: (enableExemplars: boolean | undefined) => void; +} { + const { onChange, value } = props; + + const enableExemplars = value.enableExemplars; + + const handleEnableExemplarsChange = (next: boolean | undefined): void => { + onChange( + produce(value, (draft) => { + draft.enableExemplars = next; + }), + ); + }; + + return { enableExemplars, handleEnableExemplarsChange }; +} diff --git a/prometheus/src/plugins/prometheus-time-series-query/time-series-query-model.ts b/prometheus/src/plugins/prometheus-time-series-query/time-series-query-model.ts index 05f6d793f..77b57ed30 100644 --- a/prometheus/src/plugins/prometheus-time-series-query/time-series-query-model.ts +++ b/prometheus/src/plugins/prometheus-time-series-query/time-series-query-model.ts @@ -24,5 +24,11 @@ export interface PrometheusTimeSeriesQuerySpec { minStep?: DurationString; resolution?: number; instant?: boolean; + /** + * Per-query toggle for the exemplars feature. `undefined` (Auto) follows the + * `exemplars.enable` setting of the selected datasource; `true`/`false` + * override it for this query. Exemplars are only fetched for range queries. + */ + enableExemplars?: boolean; datasource?: DatasourceSelectValue; } From 01690ad260978dd362ddc637ec82e5ef702b8a5c Mon Sep 17 00:00:00 2001 From: Juliano Costa Date: Thu, 29 Oct 2026 11:23:31 +0000 Subject: [PATCH 2/4] Prometheus: label the exemplars toggle and explain Auto mode (perses#3445) - Name the On/Off/Auto group via aria-labelledby and expose each button's state with aria-pressed. - In Auto query mode, note that exemplars are skipped when the panel runs instant queries. The editor cannot see the panel's query mode, so it cannot disable the control in that case. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KvsQXh9qWa3NMgi9mUQTY3 Signed-off-by: Juliano Costa --- .../PrometheusTimeSeriesQueryEditor.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx b/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx index bfc36d0be..2818c3847 100644 --- a/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx +++ b/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx @@ -63,6 +63,7 @@ export function PrometheusTimeSeriesQueryEditor(props: PrometheusTimeSeriesQuery const datasourceSelectValue = datasource ?? DEFAULT_PROM; const datasourceSelectLabelID = useId(); // for panels with multiple queries, this component is rendered multiple times on the same page + const exemplarsLabelID = useId(); const selectedDatasource = useDatasourceSelectValueToSelector( datasourceSelectValue, @@ -205,32 +206,43 @@ export function PrometheusTimeSeriesQueryEditor(props: PrometheusTimeSeriesQuery - + Exemplars {exemplarsEnabledOnDatasource === false && ' (disabled on the datasource)'} {exemplarsEnabledOnDatasource === true && instant === true && ' (not available for instant queries)'} + {exemplarsEnabledOnDatasource === true && + instant === undefined && + ' (skipped if the panel runs instant queries)'} From 4cd1809c7927d78cf4799a1ee50bef0848fe1d6c Mon Sep 17 00:00:00 2001 From: Juliano Costa Date: Thu, 29 Oct 2026 11:39:13 +0000 Subject: [PATCH 3/4] Prometheus: keep the exemplars label grey when disabled (perses#3445) Exemplars being disabled on the datasource is a configuration state, not an error, so the label no longer switches to the error colour. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KvsQXh9qWa3NMgi9mUQTY3 Signed-off-by: Juliano Costa --- .../PrometheusTimeSeriesQueryEditor.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx b/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx index 2818c3847..6286eeb81 100644 --- a/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx +++ b/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx @@ -232,11 +232,7 @@ export function PrometheusTimeSeriesQueryEditor(props: PrometheusTimeSeriesQuery Auto - + Exemplars {exemplarsEnabledOnDatasource === false && ' (disabled on the datasource)'} {exemplarsEnabledOnDatasource === true && instant === true && ' (not available for instant queries)'} From fdbc973b70e58ec702a386ff6b86bb09cf1e4e0a Mon Sep 17 00:00:00 2001 From: Juliano Costa Date: Mon, 2 Nov 2026 23:07:26 +0100 Subject: [PATCH 4/4] docs(prometheus): clarify per-query exemplars toggle semantics enableExemplars can only narrow the datasource setting: true still requires exemplars enabled on the datasource and a range query. Document the Auto/On/Off modes in the README and fix the Go and TS comments that described it as an override. Signed-off-by: Juliano Costa --- docs/prometheus/README.md | 21 ++++++++++++++++++- prometheus/sdk/go/query/options.go | 8 ++++--- .../time-series-query-model.ts | 8 ++++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/prometheus/README.md b/docs/prometheus/README.md index 7255cb816..86afe3164 100644 --- a/docs/prometheus/README.md +++ b/docs/prometheus/README.md @@ -134,7 +134,7 @@ See also technical docs related to this plugin: The Prometheus package supports displaying exemplars on the [Time Series Chart](../timeserieschart/README.md) panel: -- When exemplars are enabled on the datasource (see below), the `PrometheusTimeSeriesQuery` plugin calls the Prometheus [`/api/v1/query_exemplars`](https://prometheus.io/docs/prometheus/latest/querying/api/#querying-exemplars) endpoint in parallel with every range query, and attaches the returned exemplars to the series matching their labels. +- When exemplars are enabled on the datasource (see below), the `PrometheusTimeSeriesQuery` plugin calls the Prometheus [`/api/v1/query_exemplars`](https://prometheus.io/docs/prometheus/latest/querying/api/#querying-exemplars) endpoint in parallel with each range query that has not turned them off (see [per-query toggle](#per-query-toggle)), and attaches the returned exemplars to the series matching their labels. - On the chart, each exemplar is rendered as a diamond marker placed at its value and timestamp, using the color of the series it belongs to. Clicking a marker opens a dialog with its metadata: the labels of the series it belongs to, the exemplar labels (like `trace_id`), its value and its timestamp (e.g. to copy a trace ID and open the corresponding trace in your tracing UI). - A failed exemplars request never impacts the panel itself: the query results are displayed without exemplars. @@ -157,6 +157,25 @@ Some requirements and caveats to be aware of: - Exemplars are only fetched for range queries: queries executed in instant mode never display exemplars. - When relying on the [proxy](https://perses.dev/perses/docs/concepts/proxy/) feature, make sure the `/api/v1/query_exemplars` endpoint is allowed so exemplars can be fetched through the Perses server. +### Per-query toggle + +Each `PrometheusTimeSeriesQuery` has an **Exemplars** option in the query editor, with three modes: + +- **Auto** (default, `enableExemplars` omitted): follows the datasource setting. +- **On** (`enableExemplars: true`): fetches exemplars for this query. +- **Off** (`enableExemplars: false`): never fetches exemplars for this query, e.g. to save the extra request on a panel that doesn't need them. + +The toggle can only narrow the datasource setting, not widen it: **On** still requires exemplars to be enabled on the datasource, and a range query. When the datasource has exemplars disabled, or the query runs in instant mode, no exemplars are fetched whatever the toggle says. + +```yaml +kind: "PrometheusTimeSeriesQuery" +spec: + query: histogram_quantile(0.99, sum by (le) (rate(http_server_duration_bucket[5m]))) + enableExemplars: false +``` + +When using the Dashboard-as-Code Go lib, use the `query.EnableExemplars(false)` option. + ## Annotation (`PrometheusPromQLAnnotation`) The annotation plugin overlays contextual events on the panels of a dashboard, like deployments, incidents or maintenance windows. The provided PromQL expression is executed as a range query over the time range of the dashboard, and each series it returns becomes an annotation spanning from the timestamp of its first sample to the timestamp of its last one. diff --git a/prometheus/sdk/go/query/options.go b/prometheus/sdk/go/query/options.go index 72b86a51d..3bf5ab8c3 100644 --- a/prometheus/sdk/go/query/options.go +++ b/prometheus/sdk/go/query/options.go @@ -66,9 +66,11 @@ func Instant(instant bool) Option { } } -// EnableExemplars enables or disables exemplars for this query, overriding the -// `exemplars.enable` setting of the Prometheus datasource. Exemplars are only -// fetched for range queries. +// EnableExemplars sets the per-query exemplars toggle. Exemplars must still be +// enabled on the Prometheus datasource (`exemplars.enable`): `true` cannot turn +// them on for a datasource that has them disabled. When this option is omitted, +// the query follows the datasource setting; `false` skips fetching exemplars for +// this query. Exemplars are only fetched for range queries. func EnableExemplars(enableExemplars bool) Option { return func(builder *Builder) error { builder.EnableExemplars = &enableExemplars diff --git a/prometheus/src/plugins/prometheus-time-series-query/time-series-query-model.ts b/prometheus/src/plugins/prometheus-time-series-query/time-series-query-model.ts index 77b57ed30..4164f590b 100644 --- a/prometheus/src/plugins/prometheus-time-series-query/time-series-query-model.ts +++ b/prometheus/src/plugins/prometheus-time-series-query/time-series-query-model.ts @@ -25,9 +25,11 @@ export interface PrometheusTimeSeriesQuerySpec { resolution?: number; instant?: boolean; /** - * Per-query toggle for the exemplars feature. `undefined` (Auto) follows the - * `exemplars.enable` setting of the selected datasource; `true`/`false` - * override it for this query. Exemplars are only fetched for range queries. + * Per-query toggle for the exemplars feature. Exemplars must be enabled on the + * selected datasource (`exemplars.enable`); this toggle cannot turn them on for + * a datasource that has them disabled. `undefined` (Auto) follows the datasource + * setting, `true` (On) fetches them when the datasource allows it, and `false` + * (Off) skips fetching for this query. Exemplars are only fetched for range queries. */ enableExemplars?: boolean; datasource?: DatasourceSelectValue;