diff --git a/prometheus/schemas/prometheus-time-series-query/migrate/migrate.cue b/prometheus/schemas/prometheus-time-series-query/migrate/migrate.cue index 68a7e2229..81c8bfc2d 100644 --- a/prometheus/schemas/prometheus-time-series-query/migrate/migrate.cue +++ b/prometheus/schemas/prometheus-time-series-query/migrate/migrate.cue @@ -25,6 +25,7 @@ package migrate expr: string legendFormat?: string interval?: string + instant?: bool ... } @@ -44,4 +45,7 @@ spec: { if #target.interval != _|_ { minStep: #target.interval } + if #target.instant != _|_ { + instant: #target.instant + } } diff --git a/prometheus/schemas/prometheus-time-series-query/migrate/tests/instant/expected.json b/prometheus/schemas/prometheus-time-series-query/migrate/tests/instant/expected.json new file mode 100644 index 000000000..baea123b5 --- /dev/null +++ b/prometheus/schemas/prometheus-time-series-query/migrate/tests/instant/expected.json @@ -0,0 +1,7 @@ +{ + "kind": "PrometheusTimeSeriesQuery", + "spec": { + "query": "up{env=\"demo\",instance=~\"demo.do.prometheus.io.*\"}", + "instant": true + } +} diff --git a/prometheus/schemas/prometheus-time-series-query/migrate/tests/instant/input.json b/prometheus/schemas/prometheus-time-series-query/migrate/tests/instant/input.json new file mode 100644 index 000000000..ddd0c893a --- /dev/null +++ b/prometheus/schemas/prometheus-time-series-query/migrate/tests/instant/input.json @@ -0,0 +1,5 @@ +{ + "expr": "up{env=\"demo\",instance=~\"demo.do.prometheus.io.*\"}", + "instant": true, + "refID": "A" +} diff --git a/prometheus/schemas/prometheus-time-series-query/query.cue b/prometheus/schemas/prometheus-time-series-query/query.cue index 13efd1b49..ec4a62ad9 100644 --- a/prometheus/schemas/prometheus-time-series-query/query.cue +++ b/prometheus/schemas/prometheus-time-series-query/query.cue @@ -26,6 +26,7 @@ spec: close({ seriesNameFormat?: string minStep?: =~ds.#durationRegex | =~common.#variableSyntaxRegex resolution?: number + instant?: bool }) #variableSyntaxRegex: "^\\$\\w+$" diff --git a/prometheus/schemas/prometheus-time-series-query/tests/valid/prometheus-time-series-query-instant.json b/prometheus/schemas/prometheus-time-series-query/tests/valid/prometheus-time-series-query-instant.json new file mode 100644 index 000000000..3213bd145 --- /dev/null +++ b/prometheus/schemas/prometheus-time-series-query/tests/valid/prometheus-time-series-query-instant.json @@ -0,0 +1,12 @@ +{ + "kind": "PrometheusTimeSeriesQuery", + "spec": { + "datasource": { + "kind": "PrometheusDatasource", + "name": "MyDemoDatasource" + }, + "query": "max by (job, instance) (up)", + "seriesNameFormat": "{{ job }}/{{ instance }}", + "instant": true + } +} diff --git a/prometheus/sdk/go/query/options.go b/prometheus/sdk/go/query/options.go index 5ab37e8a4..fed4bc46b 100644 --- a/prometheus/sdk/go/query/options.go +++ b/prometheus/sdk/go/query/options.go @@ -54,3 +54,10 @@ func Resolution(resolution int) Option { return nil } } + +func Instant(instant bool) Option { + return func(builder *Builder) error { + builder.Instant = instant + return nil + } +} diff --git a/prometheus/sdk/go/query/query.go b/prometheus/sdk/go/query/query.go index bbe273b75..064b0d020 100644 --- a/prometheus/sdk/go/query/query.go +++ b/prometheus/sdk/go/query/query.go @@ -28,6 +28,7 @@ type PluginSpec struct { SeriesNameFormat string `json:"seriesNameFormat,omitempty" yaml:"seriesNameFormat,omitempty"` 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"` } 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 5a75faf24..fbddd8945 100644 --- a/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx +++ b/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx @@ -24,7 +24,7 @@ import { useTimeRange, } from '@perses-dev/plugin-system'; import { useId } from '@perses-dev/components'; -import { FormControl, Stack, TextField } from '@mui/material'; +import { Button, ButtonGroup, FormControl, Stack, TextField } from '@mui/material'; import { ReactElement, useContext, useMemo } from 'react'; import { PanelEditorContext } from '@perses-dev/dashboards'; import { @@ -46,6 +46,7 @@ import { useQueryState, useFormatState, useMinStepState, + useInstantState, } from './query-editor-model'; /** * The options editor component for editing a PrometheusTimeSeriesQuery's spec. @@ -74,6 +75,7 @@ export function PrometheusTimeSeriesQueryEditor(props: PrometheusTimeSeriesQuery const { handleQueryChange, handleQueryBlur } = useQueryState(props); const { format, handleFormatChange, handleFormatBlur } = useFormatState(props); const { minStep, handleMinStepChange, handleMinStepBlur } = useMinStepState(props); + const { instant, handleInstantChange } = useInstantState(props); const minStepPlaceholder = minStep ?? (datasourceResource && (datasourceResource?.plugin.spec as PrometheusDatasourceSpec).scrapeInterval) ?? @@ -151,7 +153,7 @@ export function PrometheusTimeSeriesQueryEditor(props: PrometheusTimeSeriesQuery isReadOnly={isReadonly} treeViewMetadata={treeViewMetadata} /> - + handleMinStepChange(e.target.value ? (e.target.value as DurationString) : undefined)} onBlur={handleMinStepBlur} + disabled={instant === true} sx={{ width: '250px' }} slotProps={{ inputLabel: { shrink: isReadonly ? true : undefined }, input: { readOnly: isReadonly }, }} /> + + + + + ); 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 6b480ade2..e52102945 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 @@ -119,14 +119,13 @@ export const getTimeSeriesData: TimeSeriesQueryPlugin { return stubRepsonse; }); +// Mock instant query +promStubClient.instantQuery = jest.fn(async () => { + const stubResponse: InstantQueryResponse = { + status: 'success', + data: { + resultType: 'vector', + result: [ + { + metric: { + __name__: 'up', + }, + value: [1686141338.877, '10'], + }, + ], + }, + }; + return stubResponse; +}); + const getDatasourceClient: jest.Mock = jest.fn(() => { return promStubClient; }); @@ -114,4 +133,56 @@ describe('PrometheusTimeSeriesQuery', () => { expect(results.series[0]?.formattedName).toEqual('bar - format'); }); + + it('should use instantQuery when spec.instant is true', async () => { + const ctx = createStubContext(); + (promStubClient.instantQuery as jest.Mock).mockClear(); + (promStubClient.rangeQuery as jest.Mock).mockClear(); + + await PrometheusTimeSeriesQuery.getTimeSeriesData( + { + query: 'up', + instant: true, + }, + ctx + ); + + expect(promStubClient.instantQuery).toHaveBeenCalledTimes(1); + expect(promStubClient.rangeQuery).not.toHaveBeenCalled(); + }); + + it('should use rangeQuery when spec.instant is false even if context mode is instant', async () => { + const ctx = createStubContext(); + ctx.mode = 'instant'; + (promStubClient.instantQuery as jest.Mock).mockClear(); + (promStubClient.rangeQuery as jest.Mock).mockClear(); + + await PrometheusTimeSeriesQuery.getTimeSeriesData( + { + query: 'up', + instant: false, + }, + ctx + ); + + expect(promStubClient.rangeQuery).toHaveBeenCalledTimes(1); + expect(promStubClient.instantQuery).not.toHaveBeenCalled(); + }); + + it('should use instantQuery when spec.instant is unset and context mode is instant', async () => { + const ctx = createStubContext(); + ctx.mode = 'instant'; + (promStubClient.instantQuery as jest.Mock).mockClear(); + (promStubClient.rangeQuery as jest.Mock).mockClear(); + + await PrometheusTimeSeriesQuery.getTimeSeriesData( + { + query: 'up', + }, + ctx + ); + + expect(promStubClient.instantQuery).toHaveBeenCalledTimes(1); + expect(promStubClient.rangeQuery).not.toHaveBeenCalled(); + }); }); 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 e7581ab10..e0a8d5912 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 @@ -133,3 +133,26 @@ export function useMinStepState(props: PrometheusTimeSeriesQueryEditorProps): { return { minStep, handleMinStepChange, handleMinStepBlur }; } + +/** + * Hook to manage `instant` state. Since a checkbox/switch commits immediately, changes are propagated to the spec on + * toggle rather than on blur. + */ +export function useInstantState(props: PrometheusTimeSeriesQueryEditorProps): { + instant: boolean | undefined; + handleInstantChange: (instant: boolean | undefined) => void; +} { + const { onChange, value } = props; + + const instant = value.instant; + + const handleInstantChange = (next: boolean | undefined): void => { + onChange( + produce(value, (draft) => { + draft.instant = next; + }) + ); + }; + + return { instant, handleInstantChange }; +} 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 8dc68f213..e0b75a153 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 @@ -22,5 +22,6 @@ export interface PrometheusTimeSeriesQuerySpec { seriesNameFormat?: string; minStep?: DurationString; resolution?: number; + instant?: boolean; datasource?: DatasourceSelectValue; }