Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ package migrate
expr: string
legendFormat?: string
interval?: string
instant?: bool
...
}

Expand All @@ -44,4 +45,7 @@ spec: {
if #target.interval != _|_ {
minStep: #target.interval
}
if #target.instant != _|_ {
instant: #target.instant
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"query": "up{env=\"demo\",instance=~\"demo.do.prometheus.io.*\"}",
"instant": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"expr": "up{env=\"demo\",instance=~\"demo.do.prometheus.io.*\"}",
"instant": true,
"refID": "A"
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ spec: close({
seriesNameFormat?: string
minStep?: =~ds.#durationRegex | =~common.#variableSyntaxRegex
resolution?: number
instant?: bool
})

#variableSyntaxRegex: "^\\$\\w+$"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"kind": "PrometheusTimeSeriesQuery",
"spec": {
"datasource": {
"kind": "PrometheusDatasource",
"name": "MyDemoDatasource"
},
"query": "max by (job, instance) (up)",
"seriesNameFormat": "{{ job }}/{{ instance }}",
"instant": true
}
}
7 changes: 7 additions & 0 deletions prometheus/sdk/go/query/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
1 change: 1 addition & 0 deletions prometheus/sdk/go/query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -46,6 +46,7 @@ import {
useQueryState,
useFormatState,
useMinStepState,
useInstantState,
} from './query-editor-model';
/**
* The options editor component for editing a PrometheusTimeSeriesQuery's spec.
Expand Down Expand Up @@ -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) ??
Expand Down Expand Up @@ -151,7 +153,7 @@ export function PrometheusTimeSeriesQueryEditor(props: PrometheusTimeSeriesQuery
isReadOnly={isReadonly}
treeViewMetadata={treeViewMetadata}
/>
<Stack direction="row" spacing={2}>
<Stack direction="row" spacing={2} alignItems="flex-start">
<TextField
fullWidth
label="Legend"
Expand All @@ -172,12 +174,27 @@ export function PrometheusTimeSeriesQueryEditor(props: PrometheusTimeSeriesQuery
value={minStep ?? ''}
onChange={(e) => 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 },
}}
/>
<ButtonGroup disabled={isReadonly} sx={{ '& .MuiButton-root': { px: 2, py: 2 } }}>
<Button variant={instant === false ? 'contained' : 'outlined'} onClick={() => handleInstantChange(false)}>
Range
</Button>
<Button variant={instant === true ? 'contained' : 'outlined'} onClick={() => handleInstantChange(true)}>
Instant
</Button>
<Button
variant={instant === undefined ? 'contained' : 'outlined'}
onClick={() => handleInstantChange(undefined)}
>
Auto
</Button>
</ButtonGroup>
</Stack>
</Stack>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,13 @@ export const getTimeSeriesData: TimeSeriesQueryPlugin<PrometheusTimeSeriesQueryS
// Make the request to Prom

let response;
switch (context.mode) {
case 'instant':
response = await client.instantQuery({ query, time: end }, { ...interpolatedOptions, signal: abortSignal });
break;
case 'range':
default:
response = await client.rangeQuery({ query, start, end, step }, { ...interpolatedOptions, signal: abortSignal });
break;
// `spec.instant` is a per-query override: `true` forces instant, `false` forces range.
// When left unset (Auto), defer to the panel-provided `context.mode`.
const isInstant = spec.instant ?? context.mode === 'instant';
if (isInstant) {
response = await client.instantQuery({ query, time: end }, { ...interpolatedOptions, signal: abortSignal });
} else {
response = await client.rangeQuery({ query, start, end, step }, { ...interpolatedOptions, signal: abortSignal });
}

// TODO: What about error responses from Prom that have a response body?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jest.mock('echarts/core');

import { TimeSeriesQueryContext } from '@perses-dev/plugin-system';
import { DatasourceSpec } from '@perses-dev/spec';
import { RangeQueryResponse } from '../../model';
import { RangeQueryResponse, InstantQueryResponse } from '../../model';
import { PrometheusDatasource } from '../prometheus-datasource';
import { PrometheusDatasourceSpec } from '../types';
import { PrometheusTimeSeriesQuery } from './';
Expand Down Expand Up @@ -48,6 +48,25 @@ promStubClient.rangeQuery = jest.fn(async () => {
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;
});
Expand Down Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export interface PrometheusTimeSeriesQuerySpec {
seriesNameFormat?: string;
minStep?: DurationString;
resolution?: number;
instant?: boolean;
datasource?: DatasourceSelectValue<PrometheusDatasourceSelector>;
}
Loading