Prometheus: add functionality to specify desired step interval in dashboards panels (#36422)

* Add select component for choosing step option

* Add onStepChange

* Add functionality for max step

* Rename minInterval to stepInterval to describe min, max and exact step interval

* Change select option from standard to exact

* Add new type StepType for better type safety

* Add tests for adjustInterval

* Add functionality and tests for exact step option

* Prometheus: Spell out min and max in select component

* Prometheus: Change width of step select component and add placeholder

* Prometheus: Adjust for the factor in exact step

* Prometheus: Update tooltip of step lable to include max and exact options and add padding to select component to give it some breathing room from other components

* Update snapshot for step tooltip

* Prometheus: make tooltip more informative

* Prometheus: add tooltip to interval input element

* Prometheus: extract default step option

* Prometheus: update snapshot for PromQueryEditor

* Prometheus: change step labels to uppercase

* Prometheus: define a default step option

* Prometheus: use default step option in both ui component and logic

* Prometheus: update snapshot for PromQueryEditor

* Prometheus: refactor datasource.ts for better readability

* Prometheus: change tool tip for step

* Prometheus: update snapshots

* Prometheus: add correct styling

* Prometheus: update snapshots

* Prometheus change variable name to something less superfluous

* Prometheus: refactor

* Prometheus: add new test for adjustInterval

* Docs: Update docummentation on the step parameter for prometheus

* Prometheus: make step input field smaller and change placeholder text to 15s

* Prometheus: update snapshots

* Prometheus: Make stepMode uniform in all places in the code

* Prometheus: update documentation and tooltip for step

* Prometheus: update snapshots
This commit is contained in:
Olof Bourghardt
2021-07-28 08:26:09 +02:00
committed by GitHub
parent 69dff96c1b
commit ddf5b65c51
6 changed files with 169 additions and 27 deletions

View File

@@ -4,7 +4,7 @@ import React, { PureComponent } from 'react';
// Types
import { InlineFormLabel, LegacyForms, Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { PromQuery } from '../types';
import { PromQuery, StepMode } from '../types';
import PromQueryField from './PromQueryField';
import PromLink from './PromLink';
@@ -24,11 +24,29 @@ const INTERVAL_FACTOR_OPTIONS: Array<SelectableValue<number>> = map([1, 2, 3, 4,
label: '1/' + value,
}));
export const DEFAULT_STEP_MODE: SelectableValue<StepMode> = {
value: 'min',
label: 'Minimum',
};
const STEP_MODES: Array<SelectableValue<StepMode>> = [
DEFAULT_STEP_MODE,
{
value: 'max',
label: 'Maximum',
},
{
value: 'exact',
label: 'Exact',
},
];
interface State {
legendFormat?: string;
formatOption: SelectableValue<string>;
interval?: string;
intervalFactorOption: SelectableValue<number>;
stepMode: SelectableValue<StepMode>;
instant: boolean;
exemplar: boolean;
}
@@ -52,6 +70,8 @@ export class PromQueryEditor extends PureComponent<PromQueryEditorProps, State>
formatOption: FORMAT_OPTIONS.find((option) => option.value === query.format) || FORMAT_OPTIONS[0],
intervalFactorOption:
INTERVAL_FACTOR_OPTIONS.find((option) => option.value === query.intervalFactor) || INTERVAL_FACTOR_OPTIONS[0],
// Step mode
stepMode: STEP_MODES.find((option) => option.value === query.stepMode) || DEFAULT_STEP_MODE,
// Switch options
instant: Boolean(query.instant),
exemplar: Boolean(query.exemplar),
@@ -84,6 +104,11 @@ export class PromQueryEditor extends PureComponent<PromQueryEditorProps, State>
this.setState({ intervalFactorOption: option }, this.onRunQuery);
};
onStepChange = (option: SelectableValue<StepMode>) => {
this.query.stepMode = option.value;
this.setState({ stepMode: option }, this.onRunQuery);
};
onLegendChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
const legendFormat = e.currentTarget.value;
this.query.legendFormat = legendFormat;
@@ -105,7 +130,7 @@ export class PromQueryEditor extends PureComponent<PromQueryEditorProps, State>
render() {
const { datasource, query, range, data } = this.props;
const { formatOption, instant, interval, intervalFactorOption, legendFormat, exemplar } = this.state;
const { formatOption, instant, interval, intervalFactorOption, stepMode, legendFormat, exemplar } = this.state;
return (
<PromQueryField
@@ -139,27 +164,36 @@ export class PromQueryEditor extends PureComponent<PromQueryEditorProps, State>
<div className="gf-form">
<InlineFormLabel
width={7}
width={5}
tooltip={
<>
An additional lower limit for the step parameter of the Prometheus query and for the{' '}
<code>$__interval</code> and <code>$__rate_interval</code> variables. The limit is absolute and not
modified by the &quot;Resolution&quot; setting.
Use &apos;Minimum&apos; or &apos;Maximum&apos; step mode to set the lower or upper bounds
respectively on the interval between data points. For example, set &quot;minimum 1h&quot; to hint
that measurements were not taken more frequently. Use the &apos;Exact&apos; step mode to set an
exact interval between data points. <code>$__interval</code> and <code>$__rate_interval</code> are
supported.
</>
}
>
Min step
Step
</InlineFormLabel>
<Select
className={'select-container'}
width={16}
isSearchable={false}
options={STEP_MODES}
onChange={this.onStepChange}
value={stepMode}
/>
<input
type="text"
className="gf-form-input width-8"
placeholder={interval}
className="gf-form-input width-4"
placeholder="15s"
onChange={this.onIntervalChange}
onBlur={this.onRunQuery}
value={interval}
/>
</div>
<div className="gf-form">
<div className="gf-form-label">Resolution</div>
<Select
@@ -169,10 +203,10 @@ export class PromQueryEditor extends PureComponent<PromQueryEditorProps, State>
value={intervalFactorOption}
/>
</div>
<div className="gf-form">
<div className="gf-form-label width-7">Format</div>
<Select
className={'select-container'}
width={16}
isSearchable={false}
options={FORMAT_OPTIONS}
@@ -189,7 +223,6 @@ export class PromQueryEditor extends PureComponent<PromQueryEditorProps, State>
/>
</InlineFormLabel>
</div>
<PromExemplarField isEnabled={exemplar} onChange={this.onExemplarChange} datasource={datasource} />
</div>
}

View File

@@ -31,8 +31,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
<FormLabel
tooltip={
<React.Fragment>
An additional lower limit for the step parameter of the Prometheus query and for the
Use 'Minimum' or 'Maximum' step mode to set the lower or upper bounds respectively on the interval between data points. For example, set "minimum 1h" to hint that measurements were not taken more frequently. Use the 'Exact' step mode to set an exact interval between data points.
<code>
$__interval
</code>
@@ -40,18 +39,46 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
<code>
$__rate_interval
</code>
variables. The limit is absolute and not modified by the "Resolution" setting.
are supported.
</React.Fragment>
}
width={7}
width={5}
>
Min step
Step
</FormLabel>
<Select
className="select-container"
isSearchable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "Minimum",
"value": "min",
},
Object {
"label": "Maximum",
"value": "max",
},
Object {
"label": "Exact",
"value": "exact",
},
]
}
value={
Object {
"label": "Minimum",
"value": "min",
}
}
width={16}
/>
<input
className="gf-form-input width-8"
className="gf-form-input width-4"
onBlur={[Function]}
onChange={[Function]}
placeholder=""
placeholder="15s"
type="text"
value=""
/>
@@ -112,6 +139,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
Format
</div>
<Select
className="select-container"
isSearchable={false}
onChange={[Function]}
options={

View File

@@ -18,7 +18,7 @@ import {
prometheusRegularEscape,
prometheusSpecialRegexEscape,
} from './datasource';
import { PromOptions, PromQuery } from './types';
import { PromOptions, PromQuery, StepMode } from './types';
import { VariableHide } from '../../../features/variables/types';
import { describe } from '../../../../test/lib/common';
import { QueryOptions } from 'app/types';
@@ -1685,6 +1685,60 @@ describe('PrometheusDatasource', () => {
templateSrvStub.replace = jest.fn((a: string) => a);
});
});
describe('adjustInterval', () => {
const dynamicInterval = 15;
const stepInterval = 35;
const range = 1642;
describe('when max step option is used', () => {
it('should return the minimum interval', () => {
let intervalFactor = 1;
let interval = ds.adjustInterval(dynamicInterval, stepInterval, range, intervalFactor, 'max');
expect(interval).toBe(dynamicInterval * intervalFactor);
intervalFactor = 3;
interval = ds.adjustInterval(dynamicInterval, stepInterval, range, intervalFactor, 'max');
expect(interval).toBe(stepInterval);
});
});
describe('when min step option is used', () => {
it('should return the maximum interval', () => {
let intervalFactor = 1;
let interval = ds.adjustInterval(dynamicInterval, stepInterval, range, intervalFactor, 'min');
expect(interval).toBe(stepInterval);
intervalFactor = 3;
interval = ds.adjustInterval(dynamicInterval, stepInterval, range, intervalFactor, 'min');
expect(interval).toBe(dynamicInterval * intervalFactor);
});
});
describe('when exact step option is used', () => {
it('should return the stepInterval * intervalFactor', () => {
let intervalFactor = 3;
let interval = ds.adjustInterval(dynamicInterval, stepInterval, range, intervalFactor, 'exact');
expect(interval).toBe(stepInterval * intervalFactor);
});
});
it('should not return a value less than the safe interval', () => {
let newStepInterval = 0.13;
let intervalFactor = 1;
let stepMode: StepMode = 'min';
let safeInterval = range / 11000;
if (safeInterval > 1) {
safeInterval = Math.ceil(safeInterval);
}
let interval = ds.adjustInterval(dynamicInterval, newStepInterval, range, intervalFactor, stepMode);
expect(interval).toBeGreaterThanOrEqual(safeInterval);
stepMode = 'max';
interval = ds.adjustInterval(dynamicInterval, newStepInterval, range, intervalFactor, stepMode);
expect(interval).toBeGreaterThanOrEqual(safeInterval);
stepMode = 'exact';
interval = ds.adjustInterval(dynamicInterval, newStepInterval, range, intervalFactor, stepMode);
expect(interval).toBeGreaterThanOrEqual(safeInterval);
});
});
});
describe('PrometheusDatasource for POST', () => {

View File

@@ -38,9 +38,11 @@ import {
PromQueryRequest,
PromScalarData,
PromVectorData,
StepMode,
} from './types';
import { PrometheusVariableSupport } from './variables';
import PrometheusMetricFindQuery from './metric_find_query';
import { DEFAULT_STEP_MODE } from './components/PromQueryEditor';
export const ANNOTATION_QUERY_STEP_DEFAULT = '60s';
const EXEMPLARS_NOT_AVAILABLE = 'Exemplars for this data source are not available.';
@@ -410,11 +412,12 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
end: 0,
};
const range = Math.ceil(end - start);
// target.stepMode specifies whether to use min, max or exact step
const stepMode = target.stepMode || (DEFAULT_STEP_MODE.value as StepMode);
// options.interval is the dynamically calculated interval
let interval: number = rangeUtil.intervalToSeconds(options.interval);
// Minimum interval ("Min step"), if specified for the query, or same as interval otherwise.
const minInterval = rangeUtil.intervalToSeconds(
const stepInterval = rangeUtil.intervalToSeconds(
this.templateSrv.replace(target.interval || options.interval, options.scopedVars)
);
// Scrape interval as specified for the query ("Min step") or otherwise taken from the datasource.
@@ -425,7 +428,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
const intervalFactor = target.intervalFactor || 1;
// Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits
const adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor);
const adjustedInterval = this.adjustInterval(interval, stepInterval, range, intervalFactor, stepMode);
let scopedVars = {
...options.scopedVars,
...this.getRangeScopedVars(options.range),
@@ -478,7 +481,13 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
return { __rate_interval: { text: rateInterval + 's', value: rateInterval + 's' } };
}
adjustInterval(interval: number, minInterval: number, range: number, intervalFactor: number) {
adjustInterval(
dynamicInterval: number,
stepInterval: number,
range: number,
intervalFactor: number,
stepMode: StepMode
) {
// Prometheus will drop queries that might return more than 11000 data points.
// Calculate a safe interval as an additional minimum to take into account.
// Fractional safeIntervals are allowed, however serve little purpose if the interval is greater than 1
@@ -487,7 +496,20 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
if (safeInterval > 1) {
safeInterval = Math.ceil(safeInterval);
}
return Math.max(interval * intervalFactor, minInterval, safeInterval);
//Calculate adjusted interval based on the current step option
let adjustedInterval = safeInterval;
if (stepMode === 'min') {
adjustedInterval = Math.max(dynamicInterval * intervalFactor, stepInterval, safeInterval);
} else if (stepMode === 'max') {
adjustedInterval = Math.min(dynamicInterval * intervalFactor, stepInterval);
if (adjustedInterval < safeInterval) {
adjustedInterval = safeInterval;
}
} else if (stepMode === 'exact') {
adjustedInterval = Math.max(stepInterval * intervalFactor, safeInterval);
}
return adjustedInterval;
}
performTimeSeriesQuery(query: PromQueryRequest, start: number, end: number) {

View File

@@ -10,6 +10,7 @@ export interface PromQuery extends DataQuery {
hinting?: boolean;
interval?: string;
intervalFactor?: number;
stepMode?: StepMode;
legendFormat?: string;
valueWithRefId?: boolean;
requestId?: string;
@@ -17,6 +18,8 @@ export interface PromQuery extends DataQuery {
showingTable?: boolean;
}
export type StepMode = 'min' | 'max' | 'exact';
export interface PromOptions extends DataSourceJsonData {
timeInterval: string;
queryTimeout: string;