mirror of https://github.com/grafana/grafana
Alerting: Add limits and move state and label matching filters to the BE (#66267)
* WIP * Add instance totals to combined rule. Use totals to display instances stats in the UI * WIP * add global summaries, fix TS errors * fix useCombined test * fix test * use activeAt from rule when available * Fix NaN in global stats * Add no data total to global summary * Add totals recalculation for filtered rules * Fix instances totals, remove instances filtering from alert list view * Update tests * Fetch alerts considering filtering label matchers * WIP - Fetch alerts appending state filter to endpoint * Fix multiple values for state in request being applyied * fix test * Calculate hidden by for grafana managed alerts * Use INSTANCES_DISPLAY_LIMIT constant for limiting alert instances instead of 1 * Rename matchers parameter according to API changes * Fix calculating total number of grafana instances * Rename matcher prop after previous change * Display button to remove max instances limit * Change matcher query param to be an array of strings * Add test for paramsWithMatcherAndState method * Refactor matcher to be an string array to be consistent with state * Use matcher query string as matcher object type (encoded JSON) * Avoind encoding matcher parameters twice * fix tests * Enable toggle for the limit/show all button and restore limit and filters when we come back from custom view * Move getMatcherListFromString method to utils/alertmanager.ts * Fix limit toggle button being shown when it's not necessary * Use filteredTotals from be response to calculate hidden by count * Fix variables not being replaced correctly * Fix total shown to be all the instances filtered without limits * Adress some PR review comments * Move paramsWithMatcherAndState inside prometheusUrlBuilder method --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com> Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com> Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com>pull/66737/head
parent
44035ecbb2
commit
64ee42d01e
@ -0,0 +1,38 @@ |
|||||||
|
import { paramsWithMatcherAndState } from './prometheus'; |
||||||
|
|
||||||
|
const matcher = [{ name: 'severity', isRegex: false, isEqual: true, value: 'critical' }]; |
||||||
|
const matcherToJson = matcher.map((m) => JSON.stringify(m)); |
||||||
|
const matchers = [...matcher, { name: 'label1', isRegex: false, isEqual: true, value: 'hello there' }]; |
||||||
|
const matchersToJson = matchers.map((m) => JSON.stringify(m)); |
||||||
|
|
||||||
|
describe('paramsWithMatcherAndState method', () => { |
||||||
|
it('Should return same params object with no changes if there are no states nor matchers', () => { |
||||||
|
const params: Record<string, string | string[]> = { hello: 'there', bye: 'bye' }; |
||||||
|
expect(paramsWithMatcherAndState(params)).toStrictEqual(params); |
||||||
|
}); |
||||||
|
it('Should return params object with state if there are states and no matchers', () => { |
||||||
|
const params: Record<string, string | string[]> = { hello: 'there', bye: 'bye' }; |
||||||
|
const state: string[] = ['firing', 'pending']; |
||||||
|
expect(paramsWithMatcherAndState(params, state)).toStrictEqual({ ...params, state: state }); |
||||||
|
}); |
||||||
|
it('Should return params object with state if there are matchers and no states', () => { |
||||||
|
const params: Record<string, string | string[]> = { hello: 'there', bye: 'bye' }; |
||||||
|
expect(paramsWithMatcherAndState(params, undefined, matcher)).toStrictEqual({ |
||||||
|
...params, |
||||||
|
matcher: matcherToJson, |
||||||
|
}); |
||||||
|
expect(paramsWithMatcherAndState(params, undefined, matchers)).toStrictEqual({ |
||||||
|
...params, |
||||||
|
matcher: matchersToJson, |
||||||
|
}); |
||||||
|
}); |
||||||
|
it('Should return params object with stateand matchers if there are states and matchers', () => { |
||||||
|
const params: Record<string, string | string[]> = { hello: 'there', bye: 'bye' }; |
||||||
|
const state: string[] = ['firing', 'pending']; |
||||||
|
expect(paramsWithMatcherAndState(params, state, matchers)).toStrictEqual({ |
||||||
|
...params, |
||||||
|
state: state, |
||||||
|
matcher: matchersToJson, |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -1,140 +1,138 @@ |
|||||||
|
import { isUndefined, omitBy, sum } from 'lodash'; |
||||||
import pluralize from 'pluralize'; |
import pluralize from 'pluralize'; |
||||||
import React, { Fragment, useState } from 'react'; |
import React, { Fragment } from 'react'; |
||||||
import { useDebounce } from 'react-use'; |
|
||||||
|
|
||||||
import { Stack } from '@grafana/experimental'; |
import { Stack } from '@grafana/experimental'; |
||||||
import { Badge } from '@grafana/ui'; |
import { Badge } from '@grafana/ui'; |
||||||
import { CombinedRule, CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; |
import { |
||||||
|
AlertGroupTotals, |
||||||
|
AlertInstanceTotalState, |
||||||
|
CombinedRuleGroup, |
||||||
|
CombinedRuleNamespace, |
||||||
|
} from 'app/types/unified-alerting'; |
||||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; |
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; |
||||||
|
|
||||||
import { isAlertingRule, isRecordingRule, isRecordingRulerRule, isGrafanaRulerRulePaused } from '../../utils/rules'; |
|
||||||
|
|
||||||
interface Props { |
interface Props { |
||||||
includeTotal?: boolean; |
namespaces: CombinedRuleNamespace[]; |
||||||
group?: CombinedRuleGroup; |
|
||||||
namespaces?: CombinedRuleNamespace[]; |
|
||||||
} |
} |
||||||
|
|
||||||
const emptyStats = { |
// All available states for a rule need to be initialized to prevent NaN values when adding a number and undefined
|
||||||
total: 0, |
const emptyStats: Required<AlertGroupTotals> = { |
||||||
recording: 0, |
recording: 0, |
||||||
[PromAlertingRuleState.Firing]: 0, |
alerting: 0, |
||||||
[PromAlertingRuleState.Pending]: 0, |
[PromAlertingRuleState.Pending]: 0, |
||||||
[PromAlertingRuleState.Inactive]: 0, |
[PromAlertingRuleState.Inactive]: 0, |
||||||
paused: 0, |
paused: 0, |
||||||
error: 0, |
error: 0, |
||||||
} as const; |
nodata: 0, |
||||||
|
}; |
||||||
|
|
||||||
export const RuleStats = ({ group, namespaces, includeTotal }: Props) => { |
export const RuleStats = ({ namespaces }: Props) => { |
||||||
const evaluationInterval = group?.interval; |
|
||||||
const [calculated, setCalculated] = useState(emptyStats); |
|
||||||
|
|
||||||
// Performance optimization allowing reducing number of stats calculation
|
|
||||||
// The problem occurs when we load many data sources.
|
|
||||||
// Then redux store gets updated multiple times in a pretty short period, triggering calculating stats many times.
|
|
||||||
// debounce allows to skip calculations which results would be abandoned in milliseconds
|
|
||||||
useDebounce( |
|
||||||
() => { |
|
||||||
const stats = { ...emptyStats }; |
const stats = { ...emptyStats }; |
||||||
|
|
||||||
const calcRule = (rule: CombinedRule) => { |
// sum all totals for all namespaces
|
||||||
if (rule.promRule && isAlertingRule(rule.promRule)) { |
namespaces.forEach(({ groups }) => { |
||||||
if (isGrafanaRulerRulePaused(rule)) { |
groups.forEach((group) => { |
||||||
stats.paused += 1; |
const groupTotals = omitBy(group.totals, isUndefined); |
||||||
} |
for (let key in groupTotals) { |
||||||
stats[rule.promRule.state] += 1; |
// @ts-ignore
|
||||||
} |
stats[key] += groupTotals[key]; |
||||||
if (ruleHasError(rule)) { |
|
||||||
stats.error += 1; |
|
||||||
} |
} |
||||||
if ( |
}); |
||||||
(rule.promRule && isRecordingRule(rule.promRule)) || |
}); |
||||||
(rule.rulerRule && isRecordingRulerRule(rule.rulerRule)) |
|
||||||
) { |
|
||||||
stats.recording += 1; |
|
||||||
} |
|
||||||
stats.total += 1; |
|
||||||
}; |
|
||||||
|
|
||||||
if (group) { |
const statsComponents = getComponentsFromStats(stats); |
||||||
group.rules.forEach(calcRule); |
const hasStats = Boolean(statsComponents.length); |
||||||
} |
|
||||||
|
|
||||||
if (namespaces) { |
const total = sum(Object.values(stats)); |
||||||
namespaces.forEach((namespace) => namespace.groups.forEach((group) => group.rules.forEach(calcRule))); |
|
||||||
} |
|
||||||
|
|
||||||
setCalculated(stats); |
statsComponents.unshift( |
||||||
}, |
<Fragment key="total"> |
||||||
400, |
{total} {pluralize('rule', total)} |
||||||
[group, namespaces] |
</Fragment> |
||||||
); |
); |
||||||
|
|
||||||
const statsComponents: React.ReactNode[] = []; |
return ( |
||||||
|
<Stack direction="row"> |
||||||
|
{hasStats && ( |
||||||
|
<div> |
||||||
|
<Stack gap={0.5}>{statsComponents}</Stack> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</Stack> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
if (includeTotal) { |
interface RuleGroupStatsProps { |
||||||
statsComponents.push( |
group: CombinedRuleGroup; |
||||||
<Fragment key="total"> |
} |
||||||
{calculated.total} {pluralize('rule', calculated.total)} |
|
||||||
</Fragment> |
export const RuleGroupStats = ({ group }: RuleGroupStatsProps) => { |
||||||
|
const stats = group.totals; |
||||||
|
const evaluationInterval = group?.interval; |
||||||
|
|
||||||
|
const statsComponents = getComponentsFromStats(stats); |
||||||
|
const hasStats = Boolean(statsComponents.length); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Stack direction="row"> |
||||||
|
{hasStats && ( |
||||||
|
<div> |
||||||
|
<Stack gap={0.5}>{statsComponents}</Stack> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
{evaluationInterval && ( |
||||||
|
<> |
||||||
|
<div>|</div> |
||||||
|
<Badge text={evaluationInterval} icon="clock-nine" color={'blue'} /> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</Stack> |
||||||
); |
); |
||||||
|
}; |
||||||
|
|
||||||
|
export function getComponentsFromStats( |
||||||
|
stats: Partial<Record<AlertInstanceTotalState | 'paused' | 'recording', number>> |
||||||
|
) { |
||||||
|
const statsComponents: React.ReactNode[] = []; |
||||||
|
|
||||||
|
if (stats[AlertInstanceTotalState.Alerting]) { |
||||||
|
statsComponents.push(<Badge color="red" key="firing" text={`${stats[AlertInstanceTotalState.Alerting]} firing`} />); |
||||||
} |
} |
||||||
|
|
||||||
if (calculated[PromAlertingRuleState.Firing]) { |
if (stats.error) { |
||||||
statsComponents.push( |
statsComponents.push(<Badge color="red" key="errors" text={`${stats.error} errors`} />); |
||||||
<Badge color="red" key="firing" text={`${calculated[PromAlertingRuleState.Firing]} firing`} /> |
|
||||||
); |
|
||||||
} |
} |
||||||
|
|
||||||
if (calculated.error) { |
if (stats.nodata) { |
||||||
statsComponents.push(<Badge color="red" key="errors" text={`${calculated.error} errors`} />); |
statsComponents.push(<Badge color="blue" key="nodata" text={`${stats.nodata} no data`} />); |
||||||
} |
} |
||||||
|
|
||||||
if (calculated[PromAlertingRuleState.Pending]) { |
if (stats[AlertInstanceTotalState.Pending]) { |
||||||
statsComponents.push( |
statsComponents.push( |
||||||
<Badge color={'orange'} key="pending" text={`${calculated[PromAlertingRuleState.Pending]} pending`} /> |
<Badge color={'orange'} key="pending" text={`${stats[AlertInstanceTotalState.Pending]} pending`} /> |
||||||
); |
); |
||||||
} |
} |
||||||
|
|
||||||
if (calculated[PromAlertingRuleState.Inactive] && calculated.paused) { |
if (stats[AlertInstanceTotalState.Normal] && stats.paused) { |
||||||
statsComponents.push( |
statsComponents.push( |
||||||
<Badge |
<Badge |
||||||
color="green" |
color="green" |
||||||
key="paused" |
key="paused" |
||||||
text={`${calculated[PromAlertingRuleState.Inactive]} normal (${calculated.paused} paused)`} |
text={`${stats[AlertInstanceTotalState.Normal]} normal (${stats.paused} paused)`} |
||||||
/> |
/> |
||||||
); |
); |
||||||
} |
} |
||||||
|
|
||||||
if (calculated[PromAlertingRuleState.Inactive] && !calculated.paused) { |
if (stats[AlertInstanceTotalState.Normal] && !stats.paused) { |
||||||
statsComponents.push( |
statsComponents.push( |
||||||
<Badge color="green" key="inactive" text={`${calculated[PromAlertingRuleState.Inactive]} normal`} /> |
<Badge color="green" key="inactive" text={`${stats[AlertInstanceTotalState.Normal]} normal`} /> |
||||||
); |
); |
||||||
} |
} |
||||||
|
|
||||||
if (calculated.recording) { |
if (stats.recording) { |
||||||
statsComponents.push(<Badge color="purple" key="recording" text={`${calculated.recording} recording`} />); |
statsComponents.push(<Badge color="purple" key="recording" text={`${stats.recording} recording`} />); |
||||||
} |
} |
||||||
|
|
||||||
const hasStats = Boolean(statsComponents.length); |
return statsComponents; |
||||||
|
|
||||||
return ( |
|
||||||
<Stack direction="row"> |
|
||||||
{hasStats && ( |
|
||||||
<div> |
|
||||||
<Stack gap={0.5}>{statsComponents}</Stack> |
|
||||||
</div> |
|
||||||
)} |
|
||||||
{evaluationInterval && ( |
|
||||||
<> |
|
||||||
<div>|</div> |
|
||||||
<Badge text={evaluationInterval} icon="clock-nine" color={'blue'} /> |
|
||||||
</> |
|
||||||
)} |
|
||||||
</Stack> |
|
||||||
); |
|
||||||
}; |
|
||||||
|
|
||||||
function ruleHasError(rule: CombinedRule) { |
|
||||||
return rule.promRule?.health === 'err' || rule.promRule?.health === 'error'; |
|
||||||
} |
} |
||||||
|
Loading…
Reference in new issue