mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Support for re-ordering alert rules in a group (#53318)
Co-authored-by: Peter Holmberg <peterholmberg@users.noreply.github.com>
This commit is contained in:
parent
1f442b419b
commit
fb93b60fcc
@ -15,7 +15,7 @@ import { initialAsyncRequestState } from '../../utils/redux';
|
|||||||
import { durationValidationPattern } from '../../utils/time';
|
import { durationValidationPattern } from '../../utils/time';
|
||||||
import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning';
|
import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning';
|
||||||
|
|
||||||
interface Props {
|
interface ModalProps {
|
||||||
namespace: CombinedRuleNamespace;
|
namespace: CombinedRuleNamespace;
|
||||||
group: CombinedRuleGroup;
|
group: CombinedRuleGroup;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -27,7 +27,7 @@ interface FormValues {
|
|||||||
groupInterval: string;
|
groupInterval: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditCloudGroupModal(props: Props): React.ReactElement {
|
export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
||||||
const { namespace, group, onClose } = props;
|
const { namespace, group, onClose } = props;
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
import { reorder } from './ReorderRuleGroupModal';
|
||||||
|
|
||||||
|
describe('test reorder', () => {
|
||||||
|
it('should reorder arrays', () => {
|
||||||
|
const original = [1, 2, 3];
|
||||||
|
const expected = [1, 3, 2];
|
||||||
|
|
||||||
|
expect(reorder(original, 1, 2)).toEqual(expected);
|
||||||
|
expect(original).not.toEqual(expected); // make sure we've not mutated the original
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,230 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { compact } from 'lodash';
|
||||||
|
import React, { FC, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
DragDropContext,
|
||||||
|
Draggable,
|
||||||
|
DraggableProvided,
|
||||||
|
Droppable,
|
||||||
|
DroppableProvided,
|
||||||
|
DropResult,
|
||||||
|
} from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Badge, Icon, Modal, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
|
import { dispatch } from 'app/store/store';
|
||||||
|
import { CombinedRule, CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { updateRulesOrder } from '../../state/actions';
|
||||||
|
import { getRulesSourceName, isCloudRulesSource } from '../../utils/datasource';
|
||||||
|
import { hashRulerRule } from '../../utils/rule-id';
|
||||||
|
import { isAlertingRule, isRecordingRule } from '../../utils/rules';
|
||||||
|
|
||||||
|
import { AlertStateTag } from './AlertStateTag';
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
namespace: CombinedRuleNamespace;
|
||||||
|
group: CombinedRuleGroup;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CombinedRuleWithUID = { uid: string } & CombinedRule;
|
||||||
|
|
||||||
|
export const ReorderCloudGroupModal: FC<ModalProps> = (props) => {
|
||||||
|
const { group, namespace, onClose } = props;
|
||||||
|
const [pending, setPending] = useState<boolean>(false);
|
||||||
|
const [rulesList, setRulesList] = useState<CombinedRule[]>(group.rules);
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const onDragEnd = useCallback(
|
||||||
|
(result: DropResult) => {
|
||||||
|
// check for no-ops so we don't update the group unless we have changes
|
||||||
|
if (!result.destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sameIndex = result.destination.index === result.source.index;
|
||||||
|
if (sameIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newOrderedRules = reorder(rulesList, result.source.index, result.destination.index);
|
||||||
|
setRulesList(newOrderedRules); // optimistically update the new rules list
|
||||||
|
|
||||||
|
const rulesSourceName = getRulesSourceName(namespace.rulesSource);
|
||||||
|
const rulerRules = compact(newOrderedRules.map((rule) => rule.rulerRule));
|
||||||
|
|
||||||
|
setPending(true);
|
||||||
|
dispatch(
|
||||||
|
updateRulesOrder({
|
||||||
|
namespaceName: namespace.name,
|
||||||
|
groupName: group.name,
|
||||||
|
rulesSourceName: rulesSourceName,
|
||||||
|
newRules: rulerRules,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.finally(() => {
|
||||||
|
setPending(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[group.name, namespace.name, namespace.rulesSource, rulesList]
|
||||||
|
);
|
||||||
|
|
||||||
|
// assign unique but stable identifiers to each (alerting / recording) rule
|
||||||
|
const rulesWithUID: CombinedRuleWithUID[] = rulesList.map((rule) => ({
|
||||||
|
...rule,
|
||||||
|
uid: String(hashRulerRule(rule.rulerRule!)), // TODO fix this coercion?
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className={styles.modal}
|
||||||
|
isOpen={true}
|
||||||
|
title={<ModalHeader namespace={namespace} group={group} />}
|
||||||
|
onDismiss={onClose}
|
||||||
|
onClickBackdrop={onClose}
|
||||||
|
>
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable
|
||||||
|
droppableId="alert-list"
|
||||||
|
mode="standard"
|
||||||
|
renderClone={(provided, _snapshot, rubric) => (
|
||||||
|
<ListItem provided={provided} rule={rulesWithUID[rubric.source.index]} isClone />
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(droppableProvided: DroppableProvided) => (
|
||||||
|
<div
|
||||||
|
ref={droppableProvided.innerRef}
|
||||||
|
className={cx(styles.listContainer, pending && styles.disabled)}
|
||||||
|
{...droppableProvided.droppableProps}
|
||||||
|
>
|
||||||
|
{rulesWithUID.map((rule, index) => (
|
||||||
|
<Draggable key={rule.uid} draggableId={rule.uid} index={index} isDragDisabled={pending}>
|
||||||
|
{(provided: DraggableProvided) => <ListItem key={rule.uid} provided={provided} rule={rule} />}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
{droppableProvided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ListItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
provided: DraggableProvided;
|
||||||
|
rule: CombinedRule;
|
||||||
|
isClone?: boolean;
|
||||||
|
isDragging?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItem = ({ provided, rule, isClone = false, isDragging = false }: ListItemProps) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(styles.listItem, isClone && 'isClone', isDragging && 'isDragging')}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
>
|
||||||
|
{isAlertingRule(rule.promRule) && <AlertStateTag state={rule.promRule.state} />}
|
||||||
|
{isRecordingRule(rule.promRule) && <Badge text={'Recording'} color={'blue'} />}
|
||||||
|
<div className={styles.listItemName}>{rule.name}</div>
|
||||||
|
<Icon name={'draggabledots'} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ModalHeaderProps {
|
||||||
|
namespace: CombinedRuleNamespace;
|
||||||
|
group: CombinedRuleGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalHeader: FC<ModalHeaderProps> = ({ namespace, group }) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Icon name="folder" />
|
||||||
|
{isCloudRulesSource(namespace.rulesSource) && (
|
||||||
|
<Tooltip content={namespace.rulesSource.name} placement="top">
|
||||||
|
<img
|
||||||
|
alt={namespace.rulesSource.meta.name}
|
||||||
|
className={styles.dataSourceIcon}
|
||||||
|
src={namespace.rulesSource.meta.info.logos.small}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<span>{namespace.name}</span>
|
||||||
|
<Icon name="angle-right" />
|
||||||
|
<span>{group.name}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
modal: css`
|
||||||
|
max-width: 640px;
|
||||||
|
max-height: 80%;
|
||||||
|
overflow: hidden;
|
||||||
|
`,
|
||||||
|
listItem: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: ${theme.spacing()};
|
||||||
|
|
||||||
|
background: ${theme.colors.background.primary};
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
|
||||||
|
border-bottom: solid 1px ${theme.colors.border.medium};
|
||||||
|
padding: ${theme.spacing(1)} ${theme.spacing(2)};
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isClone {
|
||||||
|
border: solid 1px ${theme.colors.primary.shade};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
listContainer: css`
|
||||||
|
user-select: none;
|
||||||
|
border: solid 1px ${theme.colors.border.medium};
|
||||||
|
`,
|
||||||
|
disabled: css`
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
`,
|
||||||
|
listItemName: css`
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`,
|
||||||
|
header: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
dataSourceIcon: css`
|
||||||
|
width: ${theme.spacing(2)};
|
||||||
|
height: ${theme.spacing(2)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function reorder<T>(rules: T[], startIndex: number, endIndex: number): T[] {
|
||||||
|
const result = Array.from(rules);
|
||||||
|
const [removed] = result.splice(startIndex, 1);
|
||||||
|
result.splice(endIndex, 0, removed);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
@ -19,6 +19,7 @@ import { RuleLocation } from '../RuleLocation';
|
|||||||
|
|
||||||
import { ActionIcon } from './ActionIcon';
|
import { ActionIcon } from './ActionIcon';
|
||||||
import { EditCloudGroupModal } from './EditRuleGroupModal';
|
import { EditCloudGroupModal } from './EditRuleGroupModal';
|
||||||
|
import { ReorderCloudGroupModal } from './ReorderRuleGroupModal';
|
||||||
import { RuleStats } from './RuleStats';
|
import { RuleStats } from './RuleStats';
|
||||||
import { RulesTable } from './RulesTable';
|
import { RulesTable } from './RulesTable';
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll,
|
|||||||
|
|
||||||
const [isEditingGroup, setIsEditingGroup] = useState(false);
|
const [isEditingGroup, setIsEditingGroup] = useState(false);
|
||||||
const [isDeletingGroup, setIsDeletingGroup] = useState(false);
|
const [isDeletingGroup, setIsDeletingGroup] = useState(false);
|
||||||
|
const [isReorderingGroup, setIsReorderingGroup] = useState(false);
|
||||||
const [isCollapsed, setIsCollapsed] = useState(!expandAll);
|
const [isCollapsed, setIsCollapsed] = useState(!expandAll);
|
||||||
|
|
||||||
const { canEditRules } = useRulesAccess();
|
const { canEditRules } = useRulesAccess();
|
||||||
@ -95,6 +97,17 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll,
|
|||||||
onClick={() => setIsEditingGroup(true)}
|
onClick={() => setIsEditingGroup(true)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
actionIcons.push(
|
||||||
|
<ActionIcon
|
||||||
|
aria-label="re-order rules"
|
||||||
|
data-testid="reorder-group"
|
||||||
|
key="reorder"
|
||||||
|
icon="exchange-alt"
|
||||||
|
tooltip="reorder rules"
|
||||||
|
className={styles.rotate90}
|
||||||
|
onClick={() => setIsReorderingGroup(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (isListView) {
|
if (isListView) {
|
||||||
actionIcons.push(
|
actionIcons.push(
|
||||||
@ -134,6 +147,17 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll,
|
|||||||
onClick={() => setIsEditingGroup(true)}
|
onClick={() => setIsEditingGroup(true)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
actionIcons.push(
|
||||||
|
<ActionIcon
|
||||||
|
aria-label="re-order rules"
|
||||||
|
data-testid="reorder-group"
|
||||||
|
key="reorder"
|
||||||
|
icon="exchange-alt"
|
||||||
|
tooltip="re-order rules"
|
||||||
|
className={styles.rotate90}
|
||||||
|
onClick={() => setIsReorderingGroup(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
actionIcons.push(
|
actionIcons.push(
|
||||||
@ -194,6 +218,9 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll,
|
|||||||
{isEditingGroup && (
|
{isEditingGroup && (
|
||||||
<EditCloudGroupModal group={group} namespace={namespace} onClose={() => setIsEditingGroup(false)} />
|
<EditCloudGroupModal group={group} namespace={namespace} onClose={() => setIsEditingGroup(false)} />
|
||||||
)}
|
)}
|
||||||
|
{isReorderingGroup && (
|
||||||
|
<ReorderCloudGroupModal group={group} namespace={namespace} onClose={() => setIsReorderingGroup(false)} />
|
||||||
|
)}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isDeletingGroup}
|
isOpen={isDeletingGroup}
|
||||||
title="Delete group"
|
title="Delete group"
|
||||||
@ -278,4 +305,7 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
rulesTable: css`
|
rulesTable: css`
|
||||||
margin-top: ${theme.spacing(3)};
|
margin-top: ${theme.spacing(3)};
|
||||||
`,
|
`,
|
||||||
|
rotate90: css`
|
||||||
|
transform: rotate(90deg);
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
@ -25,7 +25,12 @@ import {
|
|||||||
RuleWithLocation,
|
RuleWithLocation,
|
||||||
StateHistoryItem,
|
StateHistoryItem,
|
||||||
} from 'app/types/unified-alerting';
|
} from 'app/types/unified-alerting';
|
||||||
import { PromApplication, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
import {
|
||||||
|
PostableRulerRuleGroupDTO,
|
||||||
|
PromApplication,
|
||||||
|
RulerRuleDTO,
|
||||||
|
RulerRulesConfigDTO,
|
||||||
|
} from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { backendSrv } from '../../../../core/services/backend_srv';
|
import { backendSrv } from '../../../../core/services/backend_srv';
|
||||||
import {
|
import {
|
||||||
@ -751,6 +756,48 @@ export const updateLotexNamespaceAndGroupAction = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
interface UpdateRulesOrderOptions {
|
||||||
|
rulesSourceName: string;
|
||||||
|
namespaceName: string;
|
||||||
|
groupName: string;
|
||||||
|
newRules: RulerRuleDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateRulesOrder = createAsyncThunk(
|
||||||
|
'unifiedalerting/updateRulesOrderForGroup',
|
||||||
|
async (options: UpdateRulesOrderOptions, thunkAPI): Promise<void> => {
|
||||||
|
return withAppEvents(
|
||||||
|
withSerializedError(
|
||||||
|
(async () => {
|
||||||
|
const { rulesSourceName, namespaceName, groupName, newRules } = options;
|
||||||
|
|
||||||
|
const rulerConfig = getDataSourceRulerConfig(thunkAPI.getState, rulesSourceName);
|
||||||
|
const rulesResult = await fetchRulerRules(rulerConfig);
|
||||||
|
|
||||||
|
const existingGroup = rulesResult[namespaceName].find((group) => group.name === groupName);
|
||||||
|
if (!existingGroup) {
|
||||||
|
throw new Error(`Group "${groupName}" not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: PostableRulerRuleGroupDTO = {
|
||||||
|
name: existingGroup.name,
|
||||||
|
interval: existingGroup.interval,
|
||||||
|
rules: newRules,
|
||||||
|
};
|
||||||
|
|
||||||
|
await setRulerRuleGroup(rulerConfig, namespaceName, payload);
|
||||||
|
|
||||||
|
await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName }));
|
||||||
|
})()
|
||||||
|
),
|
||||||
|
{
|
||||||
|
errorMessage: 'Failed to update namespace / group',
|
||||||
|
successMessage: 'Update successful',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const addExternalAlertmanagersAction = createAsyncThunk(
|
export const addExternalAlertmanagersAction = createAsyncThunk(
|
||||||
'unifiedAlerting/addExternalAlertmanagers',
|
'unifiedAlerting/addExternalAlertmanagers',
|
||||||
async (alertmanagerConfig: ExternalAlertmanagerConfig, thunkAPI): Promise<void> => {
|
async (alertmanagerConfig: ExternalAlertmanagerConfig, thunkAPI): Promise<void> => {
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`hashRulerRule should hash alerting rule 1`] = `"1465866290"`;
|
||||||
|
|
||||||
|
exports[`hashRulerRule should hash recording rules 1`] = `"2044193757"`;
|
62
public/app/features/alerting/unified/utils/rule-id.test.ts
Normal file
62
public/app/features/alerting/unified/utils/rule-id.test.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
GrafanaAlertStateDecision,
|
||||||
|
GrafanaRuleDefinition,
|
||||||
|
RulerAlertingRuleDTO,
|
||||||
|
RulerGrafanaRuleDTO,
|
||||||
|
RulerRecordingRuleDTO,
|
||||||
|
} from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { hashRulerRule } from './rule-id';
|
||||||
|
|
||||||
|
describe('hashRulerRule', () => {
|
||||||
|
it('should not hash unknown rule types', () => {
|
||||||
|
const unknownRule = {};
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
hashRulerRule(unknownRule);
|
||||||
|
}).toThrowError();
|
||||||
|
});
|
||||||
|
it('should hash recording rules', () => {
|
||||||
|
const recordingRule: RulerRecordingRuleDTO = {
|
||||||
|
record: 'instance:node_num_cpu:sum',
|
||||||
|
expr: 'count without (cpu) (count without (mode) (node_cpu_seconds_total{job="integrations/node_exporter"}))',
|
||||||
|
labels: { type: 'cpu' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(hashRulerRule(recordingRule)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hash alerting rule', () => {
|
||||||
|
const alertingRule: RulerAlertingRuleDTO = {
|
||||||
|
alert: 'always-alerting',
|
||||||
|
expr: 'vector(20) > 7',
|
||||||
|
labels: { type: 'cpu' },
|
||||||
|
annotations: { description: 'CPU usage too high' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(hashRulerRule(alertingRule)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hash Grafana Managed rules', () => {
|
||||||
|
const RULE_UID = 'abcdef12345';
|
||||||
|
const grafanaAlertDefinition: GrafanaRuleDefinition = {
|
||||||
|
uid: RULE_UID,
|
||||||
|
namespace_uid: 'namespace',
|
||||||
|
namespace_id: 0,
|
||||||
|
title: 'my rule',
|
||||||
|
condition: '',
|
||||||
|
data: [],
|
||||||
|
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||||
|
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||||
|
};
|
||||||
|
const grafanaRule: RulerGrafanaRuleDTO = {
|
||||||
|
grafana_alert: grafanaAlertDefinition,
|
||||||
|
for: '30s',
|
||||||
|
labels: { type: 'cpu' },
|
||||||
|
annotations: { description: 'CPU usage too high' },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(hashRulerRule(grafanaRule)).toBe(RULE_UID);
|
||||||
|
});
|
||||||
|
});
|
@ -107,11 +107,11 @@ export function parse(value: string, decodeFromUri = false): RuleIdentifier {
|
|||||||
const [prefix, ruleSourceName, namespace, groupName, hash] = parts.map(unesacapeDollars);
|
const [prefix, ruleSourceName, namespace, groupName, hash] = parts.map(unesacapeDollars);
|
||||||
|
|
||||||
if (prefix === cloudRuleIdentifierPrefix) {
|
if (prefix === cloudRuleIdentifierPrefix) {
|
||||||
return { ruleSourceName, namespace, groupName, rulerRuleHash: Number(hash) };
|
return { ruleSourceName, namespace, groupName, rulerRuleHash: hash };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefix === prometheusRuleIdentifierPrefix) {
|
if (prefix === prometheusRuleIdentifierPrefix) {
|
||||||
return { ruleSourceName, namespace, groupName, ruleHash: Number(hash) };
|
return { ruleSourceName, namespace, groupName, ruleHash: hash };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,10 +173,10 @@ function hash(value: string): number {
|
|||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is used to identify lotex rules, as they do not have a unique identifier
|
// this is used to identify rules, mimir / loki rules do not have a unique identifier
|
||||||
function hashRulerRule(rule: RulerRuleDTO): number {
|
export function hashRulerRule(rule: RulerRuleDTO): string {
|
||||||
if (isRecordingRulerRule(rule)) {
|
if (isRecordingRulerRule(rule)) {
|
||||||
return hash(JSON.stringify([rule.record, rule.expr, hashLabelsOrAnnotations(rule.labels)]));
|
return hash(JSON.stringify([rule.record, rule.expr, hashLabelsOrAnnotations(rule.labels)])).toString();
|
||||||
} else if (isAlertingRulerRule(rule)) {
|
} else if (isAlertingRulerRule(rule)) {
|
||||||
return hash(
|
return hash(
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
@ -185,15 +185,17 @@ function hashRulerRule(rule: RulerRuleDTO): number {
|
|||||||
hashLabelsOrAnnotations(rule.annotations),
|
hashLabelsOrAnnotations(rule.annotations),
|
||||||
hashLabelsOrAnnotations(rule.labels),
|
hashLabelsOrAnnotations(rule.labels),
|
||||||
])
|
])
|
||||||
);
|
).toString();
|
||||||
|
} else if (isGrafanaRulerRule(rule)) {
|
||||||
|
return rule.grafana_alert.uid;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('only recording and alerting ruler rules can be hashed');
|
throw new Error('only recording and alerting ruler rules can be hashed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hashRule(rule: Rule): number {
|
function hashRule(rule: Rule): string {
|
||||||
if (isRecordingRule(rule)) {
|
if (isRecordingRule(rule)) {
|
||||||
return hash(JSON.stringify([rule.type, rule.query, hashLabelsOrAnnotations(rule.labels)]));
|
return hash(JSON.stringify([rule.type, rule.query, hashLabelsOrAnnotations(rule.labels)])).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAlertingRule(rule)) {
|
if (isAlertingRule(rule)) {
|
||||||
@ -204,7 +206,7 @@ function hashRule(rule: Rule): number {
|
|||||||
hashLabelsOrAnnotations(rule.annotations),
|
hashLabelsOrAnnotations(rule.annotations),
|
||||||
hashLabelsOrAnnotations(rule.labels),
|
hashLabelsOrAnnotations(rule.labels),
|
||||||
])
|
])
|
||||||
);
|
).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('only recording and alerting rules can be hashed');
|
throw new Error('only recording and alerting rules can be hashed');
|
||||||
|
@ -35,8 +35,8 @@ export function isAlertingRule(rule: Rule | undefined): rule is AlertingRule {
|
|||||||
return typeof rule === 'object' && rule.type === PromRuleType.Alerting;
|
return typeof rule === 'object' && rule.type === PromRuleType.Alerting;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRecordingRule(rule: Rule): rule is RecordingRule {
|
export function isRecordingRule(rule: Rule | undefined): rule is RecordingRule {
|
||||||
return rule.type === PromRuleType.Recording;
|
return typeof rule === 'object' && rule.type === PromRuleType.Recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAlertingRulerRule(rule?: RulerRuleDTO): rule is RulerAlertingRuleDTO {
|
export function isAlertingRulerRule(rule?: RulerRuleDTO): rule is RulerAlertingRuleDTO {
|
||||||
|
@ -122,7 +122,7 @@ export interface CloudRuleIdentifier {
|
|||||||
ruleSourceName: string;
|
ruleSourceName: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
groupName: string;
|
groupName: string;
|
||||||
rulerRuleHash: number;
|
rulerRuleHash: string;
|
||||||
}
|
}
|
||||||
export interface GrafanaRuleIdentifier {
|
export interface GrafanaRuleIdentifier {
|
||||||
ruleSourceName: 'grafana';
|
ruleSourceName: 'grafana';
|
||||||
@ -134,7 +134,7 @@ export interface PrometheusRuleIdentifier {
|
|||||||
ruleSourceName: string;
|
ruleSourceName: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
groupName: string;
|
groupName: string;
|
||||||
ruleHash: number;
|
ruleHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RuleIdentifier = CloudRuleIdentifier | GrafanaRuleIdentifier | PrometheusRuleIdentifier;
|
export type RuleIdentifier = CloudRuleIdentifier | GrafanaRuleIdentifier | PrometheusRuleIdentifier;
|
||||||
|
Loading…
Reference in New Issue
Block a user