From 8ffd2a71d3ef1acb2a9c909355a6264770f488bc Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Mon, 24 Jul 2023 11:20:55 -0400 Subject: [PATCH] Alerting: Add min interval option to alert rule query creation (#71986) This features adds a configuration option when creating an alert rule query. This option already exists as part of the alert query model but is not currently configurable through the UI. --- .../components/rule-editor/QueryOptions.tsx | 6 ++- .../components/rule-editor/QueryRows.tsx | 15 +++++- .../components/rule-editor/QueryWrapper.tsx | 53 ++++++++++++++++++- .../query-and-alert-condition/reducer.ts | 16 +++++- .../features/alerting/unified/utils/time.ts | 14 +++++ 5 files changed, 98 insertions(+), 6 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryOptions.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryOptions.tsx index 0e135599e12..655f3fc4e93 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryOptions.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryOptions.tsx @@ -6,7 +6,7 @@ import { relativeToTimeRange } from '@grafana/data/src/datetime/rangeutil'; import { clearButtonStyles, Icon, RelativeTimeRangePicker, Toggletip, useStyles2 } from '@grafana/ui'; import { AlertQuery } from 'app/types/unified-alerting-dto'; -import { AlertQueryOptions, MaxDataPointsOption } from './QueryWrapper'; +import { AlertQueryOptions, MaxDataPointsOption, MinIntervalOption } from './QueryWrapper'; export interface QueryOptionsProps { query: AlertQuery; @@ -50,6 +50,7 @@ export const QueryOptions = ({ options={queryOptions} onChange={(options) => onChangeQueryOptions(options, index)} /> + onChangeQueryOptions(options, index)} /> } @@ -67,7 +68,8 @@ export const QueryOptions = ({ .locale('en') .fromNow(true)} - {queryOptions.maxDataPoints && , MD {queryOptions.maxDataPoints}} + {queryOptions.maxDataPoints && , MD = {queryOptions.maxDataPoints}} + {queryOptions.minInterval && , Min. Interval = {queryOptions.minInterval}} ); diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx index b19fd294823..426d879ce85 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx @@ -2,7 +2,14 @@ import { omit } from 'lodash'; import React, { PureComponent, useState } from 'react'; import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; -import { DataQuery, DataSourceInstanceSettings, LoadingState, PanelData, RelativeTimeRange } from '@grafana/data'; +import { + DataQuery, + DataSourceInstanceSettings, + LoadingState, + PanelData, + rangeUtil, + RelativeTimeRange, +} from '@grafana/data'; import { Stack } from '@grafana/experimental'; import { getDataSourceSrv } from '@grafana/runtime'; import { Button, Card, Icon } from '@grafana/ui'; @@ -61,7 +68,11 @@ export class QueryRows extends PureComponent { } return { ...item, - model: { ...item.model, maxDataPoints: options.maxDataPoints }, + model: { + ...item.model, + maxDataPoints: options.maxDataPoints, + intervalMs: options.minInterval ? rangeUtil.intervalToMs(options.minInterval) : undefined, + }, }; }) ); diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx index 322a9db02b0..20d1a5e02fb 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx @@ -18,15 +18,18 @@ import { GraphTresholdsStyleMode, Icon, InlineFormLabel, Input, Tooltip, useStyl import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow'; import { AlertQuery } from 'app/types/unified-alerting-dto'; +import { msToSingleUnitDuration } from '../../utils/time'; import { AlertConditionIndicator } from '../expressions/AlertConditionIndicator'; import { QueryOptions } from './QueryOptions'; import { VizWrapper } from './VizWrapper'; export const DEFAULT_MAX_DATA_POINTS = 43200; +export const DEFAULT_MIN_INTERVAL = '1s'; export interface AlertQueryOptions { maxDataPoints?: number | undefined; + minInterval?: string | undefined; } interface Props { @@ -107,9 +110,13 @@ export const QueryWrapper = ({ // TODO add a warning label here too when the data looks like time series data and is used as an alert condition function HeaderExtras({ query, error, index }: { query: AlertQuery; error?: Error; index: number }) { - const queryOptions: AlertQueryOptions = { maxDataPoints: query.model.maxDataPoints }; + const queryOptions: AlertQueryOptions = { + maxDataPoints: query.model.maxDataPoints, + minInterval: query.model.intervalMs ? msToSingleUnitDuration(query.model.intervalMs) : undefined, + }; const alertQueryOptions: AlertQueryOptions = { maxDataPoints: queryOptions.maxDataPoints, + minInterval: queryOptions.minInterval, }; return ( @@ -222,6 +229,50 @@ export function MaxDataPointsOption({ ); } +export function MinIntervalOption({ + options, + onChange, +}: { + options: AlertQueryOptions; + onChange: (options: AlertQueryOptions) => void; +}) { + const value = options.minInterval ?? ''; + + const onMinIntervalBlur = (event: ChangeEvent) => { + const minInterval = event.target.value; + if (minInterval !== value) { + onChange({ + ...options, + minInterval, + }); + } + }; + + return ( + + + A lower limit for the interval. Recommended to be set to write frequency, for example 1m if + your data is written every minute. + + } + > + Min interval + + + + ); +} + const getStyles = (theme: GrafanaTheme2) => ({ wrapper: css` label: AlertingQueryWrapper; diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts index bead457326e..0fc29628ed3 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts @@ -1,6 +1,6 @@ import { createAction, createReducer } from '@reduxjs/toolkit'; -import { DataQuery, getDefaultRelativeTimeRange, RelativeTimeRange } from '@grafana/data'; +import { DataQuery, getDefaultRelativeTimeRange, rangeUtil, RelativeTimeRange } from '@grafana/data'; import { getNextRefIdChar } from 'app/core/utils/query'; import { findDataSourceFromExpressionRecursive } from 'app/features/alerting/utils/dataSourceFromExpression'; import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; @@ -43,6 +43,7 @@ export const rewireExpressions = createAction<{ oldRefId: string; newRefId: stri export const updateExpressionType = createAction<{ refId: string; type: ExpressionQueryType }>('updateExpressionType'); export const updateExpressionTimeRange = createAction('updateExpressionTimeRange'); export const updateMaxDataPoints = createAction<{ refId: string; maxDataPoints: number }>('updateMaxDataPoints'); +export const updateMinInterval = createAction<{ refId: string; minInterval: string }>('updateMinInterval'); export const setRecordingRulesQueries = createAction<{ recordingRuleQueries: AlertQuery[]; expression: string }>( 'setRecordingRulesQueries' @@ -96,6 +97,19 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder } : query; }); + }) + .addCase(updateMinInterval, (state, action) => { + state.queries = state.queries.map((query) => { + return query.refId === action.payload.refId + ? { + ...query, + model: { + ...query.model, + intervalMs: action.payload.minInterval ? rangeUtil.intervalToMs(action.payload.minInterval) : undefined, + }, + } + : query; + }); }); // expressions actions diff --git a/public/app/features/alerting/unified/utils/time.ts b/public/app/features/alerting/unified/utils/time.ts index 8c65c35b93f..d0bc6840377 100644 --- a/public/app/features/alerting/unified/utils/time.ts +++ b/public/app/features/alerting/unified/utils/time.ts @@ -106,3 +106,17 @@ export const safeParseDurationstr = (duration: string): number => { export const isNullDate = (date: string) => { return date.includes('0001-01-01T00'); }; + +// Format given time span in MS to the largest single unit duration string up to hours. +export function msToSingleUnitDuration(rangeMs: number): string { + if (rangeMs % (1000 * 60 * 60) === 0) { + return rangeMs / (1000 * 60 * 60) + 'h'; + } + if (rangeMs % (1000 * 60) === 0) { + return rangeMs / (1000 * 60) + 'm'; + } + if (rangeMs % 1000 === 0) { + return rangeMs / 1000 + 's'; + } + return rangeMs.toFixed() + 'ms'; +}