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 React, { Fragment, useState } from 'react'; |
||||
import { useDebounce } from 'react-use'; |
||||
import React, { Fragment } from 'react'; |
||||
|
||||
import { Stack } from '@grafana/experimental'; |
||||
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 { isAlertingRule, isRecordingRule, isRecordingRulerRule, isGrafanaRulerRulePaused } from '../../utils/rules'; |
||||
|
||||
interface Props { |
||||
includeTotal?: boolean; |
||||
group?: CombinedRuleGroup; |
||||
namespaces?: CombinedRuleNamespace[]; |
||||
namespaces: CombinedRuleNamespace[]; |
||||
} |
||||
|
||||
const emptyStats = { |
||||
total: 0, |
||||
// All available states for a rule need to be initialized to prevent NaN values when adding a number and undefined
|
||||
const emptyStats: Required<AlertGroupTotals> = { |
||||
recording: 0, |
||||
[PromAlertingRuleState.Firing]: 0, |
||||
alerting: 0, |
||||
[PromAlertingRuleState.Pending]: 0, |
||||
[PromAlertingRuleState.Inactive]: 0, |
||||
paused: 0, |
||||
error: 0, |
||||
} as const; |
||||
nodata: 0, |
||||
}; |
||||
|
||||
export const RuleStats = ({ group, namespaces, includeTotal }: 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( |
||||
() => { |
||||
export const RuleStats = ({ namespaces }: Props) => { |
||||
const stats = { ...emptyStats }; |
||||
|
||||
const calcRule = (rule: CombinedRule) => { |
||||
if (rule.promRule && isAlertingRule(rule.promRule)) { |
||||
if (isGrafanaRulerRulePaused(rule)) { |
||||
stats.paused += 1; |
||||
} |
||||
stats[rule.promRule.state] += 1; |
||||
} |
||||
if (ruleHasError(rule)) { |
||||
stats.error += 1; |
||||
// sum all totals for all namespaces
|
||||
namespaces.forEach(({ groups }) => { |
||||
groups.forEach((group) => { |
||||
const groupTotals = omitBy(group.totals, isUndefined); |
||||
for (let key in groupTotals) { |
||||
// @ts-ignore
|
||||
stats[key] += groupTotals[key]; |
||||
} |
||||
if ( |
||||
(rule.promRule && isRecordingRule(rule.promRule)) || |
||||
(rule.rulerRule && isRecordingRulerRule(rule.rulerRule)) |
||||
) { |
||||
stats.recording += 1; |
||||
} |
||||
stats.total += 1; |
||||
}; |
||||
}); |
||||
}); |
||||
|
||||
if (group) { |
||||
group.rules.forEach(calcRule); |
||||
} |
||||
const statsComponents = getComponentsFromStats(stats); |
||||
const hasStats = Boolean(statsComponents.length); |
||||
|
||||
if (namespaces) { |
||||
namespaces.forEach((namespace) => namespace.groups.forEach((group) => group.rules.forEach(calcRule))); |
||||
} |
||||
const total = sum(Object.values(stats)); |
||||
|
||||
setCalculated(stats); |
||||
}, |
||||
400, |
||||
[group, namespaces] |
||||
statsComponents.unshift( |
||||
<Fragment key="total"> |
||||
{total} {pluralize('rule', total)} |
||||
</Fragment> |
||||
); |
||||
|
||||
const statsComponents: React.ReactNode[] = []; |
||||
return ( |
||||
<Stack direction="row"> |
||||
{hasStats && ( |
||||
<div> |
||||
<Stack gap={0.5}>{statsComponents}</Stack> |
||||
</div> |
||||
)} |
||||
</Stack> |
||||
); |
||||
}; |
||||
|
||||
if (includeTotal) { |
||||
statsComponents.push( |
||||
<Fragment key="total"> |
||||
{calculated.total} {pluralize('rule', calculated.total)} |
||||
</Fragment> |
||||
interface RuleGroupStatsProps { |
||||
group: CombinedRuleGroup; |
||||
} |
||||
|
||||
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]) { |
||||
statsComponents.push( |
||||
<Badge color="red" key="firing" text={`${calculated[PromAlertingRuleState.Firing]} firing`} /> |
||||
); |
||||
if (stats.error) { |
||||
statsComponents.push(<Badge color="red" key="errors" text={`${stats.error} errors`} />); |
||||
} |
||||
|
||||
if (calculated.error) { |
||||
statsComponents.push(<Badge color="red" key="errors" text={`${calculated.error} errors`} />); |
||||
if (stats.nodata) { |
||||
statsComponents.push(<Badge color="blue" key="nodata" text={`${stats.nodata} no data`} />); |
||||
} |
||||
|
||||
if (calculated[PromAlertingRuleState.Pending]) { |
||||
if (stats[AlertInstanceTotalState.Pending]) { |
||||
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( |
||||
<Badge |
||||
color="green" |
||||
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( |
||||
<Badge color="green" key="inactive" text={`${calculated[PromAlertingRuleState.Inactive]} normal`} /> |
||||
<Badge color="green" key="inactive" text={`${stats[AlertInstanceTotalState.Normal]} normal`} /> |
||||
); |
||||
} |
||||
|
||||
if (calculated.recording) { |
||||
statsComponents.push(<Badge color="purple" key="recording" text={`${calculated.recording} recording`} />); |
||||
if (stats.recording) { |
||||
statsComponents.push(<Badge color="purple" key="recording" text={`${stats.recording} recording`} />); |
||||
} |
||||
|
||||
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> |
||||
); |
||||
}; |
||||
|
||||
function ruleHasError(rule: CombinedRule) { |
||||
return rule.promRule?.health === 'err' || rule.promRule?.health === 'error'; |
||||
return statsComponents; |
||||
} |
||||
|
Loading…
Reference in new issue