Alerting: Add support for "alerting.notifications.routes.conflictingMatchers" (#107687)

add support for "alerting.notifications.routes.conflictingMatchers"
This commit is contained in:
Gilles De Mey
2025-07-07 14:56:31 +02:00
committed by GitHub
parent 28ef5672c4
commit ddbba7b460
6 changed files with 119 additions and 31 deletions
@@ -4,23 +4,12 @@ import { Alert } from '@grafana/ui';
import { stringifyErrorLike } from '../../utils/misc';
export const NotificationPoliciesErrorAlert = ({ error }: { error: unknown }) => {
const title = t('alerting.policies.update-errors.title', 'Error saving notification policy');
const title = t('alerting.policies.update-errors.title', 'Failed to add or update notification policy');
const errMessage = stringifyErrorLike(error);
return (
<Alert title={title} severity="error">
<div>
<Trans i18nKey="alerting.policies.update-errors.fallback">
Something went wrong when updating your notification policies.
</Trans>
</div>
<div>
{errMessage || (
<Trans i18nKey="alerting.policies.update-errors.error-code" values={{ error }}>
Error message: "{{ error }}"
</Trans>
)}
</div>
<div>{errMessage}</div>
<Trans i18nKey="alerting.policies.update-errors.suffix">Please refresh the page and try again.</Trans>
</Alert>
@@ -0,0 +1,45 @@
import {
ApiMachineryErrorResponse,
ERROR_ROUTES_MATCHER_CONFLICT,
getErrorMessageFromApiMachineryErrorResponse,
} from './errors';
describe('getErrorMessageFromCode', () => {
it(`should handle ${ERROR_ROUTES_MATCHER_CONFLICT}`, () => {
const error: ApiMachineryErrorResponse = {
status: 400,
config: { url: '' },
data: {
kind: 'Status',
apiVersion: 'v1',
metadata: {},
status: 'Failure',
message: 'this is ignored',
reason: 'BadRequest',
details: {
uid: 'alerting.notifications.routes.conflictingMatchers',
causes: [
{
message: '"[foo=\\"bar\\" baz=\\"qux\\"]"',
field: 'Matchers',
},
{
message: '"[foo2=\\"bar2\\" baz2=\\"qux2\\"]"',
field: 'Matchers',
},
],
},
code: 400,
},
};
expect(getErrorMessageFromApiMachineryErrorResponse(error)).toBe(
'Cannot add or update route: matchers conflict with an external routing tree if we merged matchers \"[foo=\\\"bar\\\" baz=\\\"qux\\\"]\", \"[foo2=\\\"bar2\\\" baz2=\\\"qux2\\\"]\". This would make the route unreachable.'
);
delete error.data.details?.causes;
expect(getErrorMessageFromApiMachineryErrorResponse(error)).toBe(
'Cannot add or update route: matchers conflict with an external routing tree if we merged matchers <unknown matchers>. This would make the route unreachable.'
);
});
});
@@ -3,19 +3,56 @@ import { get } from 'lodash';
import { t } from '@grafana/i18n';
import { FetchError, isFetchError } from '@grafana/runtime';
export type SupportedErrors = 'alerting.notifications.conflict' | string;
import { getErrorCode } from '../misc';
export const ERROR_NEWER_CONFIGURATION = 'alerting.notifications.conflict';
export const ERROR_NEWER_CONFIGURATION = 'alerting.notifications.conflict' as const;
export const ERROR_ROUTES_MATCHER_CONFLICT = 'alerting.notifications.routes.conflictingMatchers' as const;
/** This function gives us the opportunity to translate or transform error codes that are returned from the Kubernetes APIs */
export type ApiMachineryErrorResponse = FetchError<ApiMachineryError>;
// these are known error IDs, used by both Kubernetes API and the front-end (using error `cause`).
export type KnownErrorCodes = typeof ERROR_NEWER_CONFIGURATION;
// Kubernetes API Machinery errors are a superset of supported errors codes.
export type KnownMachineryErrorCodes = KnownErrorCodes | typeof ERROR_ROUTES_MATCHER_CONFLICT;
/**
* This function gives us the opportunity to translate or transform error codes that are returned from the Kubernetes APIs
*/
export function getErrorMessageFromApiMachineryErrorResponse(error: ApiMachineryErrorResponse): string | undefined {
const code = getErrorCode(error);
if (!code) {
return error.data.message;
}
const errorMessageMap: Record<KnownMachineryErrorCodes, string | undefined> = {
[ERROR_NEWER_CONFIGURATION]: getErrorMessageFromCode(code),
[ERROR_ROUTES_MATCHER_CONFLICT]: t(
'alerting.policies.update-errors.routes.conflictingMatchers',
'Cannot add or update route: matchers conflict with an external routing tree if we merged matchers {{-matchers}}. This would make the route unreachable.',
{
matchers:
error.data.details?.causes?.map((cause) => cause.message).join(', ') ??
t('alerting.policies.update-errors.routes.unknownMatchers', '<unknown matchers>'),
}
),
};
// @ts-expect-error this allows the typechecker to warn us about forgetting to handle some KnownMachineryErrors and still return "undefined" for unknown codes;
return errorMessageMap[code];
}
/**
* This function gives us the opportunity to translate or transform error codes
*/
export function getErrorMessageFromCode(code: string): string | undefined {
const errorMessageMap: Record<SupportedErrors, string> = {
const errorMessageMap: Record<KnownErrorCodes, string> = {
[ERROR_NEWER_CONFIGURATION]: t(
'alerting.policies.update-errors.conflict',
'The notification policy tree has been updated by another user.'
),
};
// @ts-expect-error this allows the typechecker to warn us about forgetting to handle some KnownErrors and still return "undefined" for unknown codes;
return errorMessageMap[code];
}
@@ -29,7 +66,12 @@ export type ApiMachineryError = {
group?: string;
kind?: string;
retryAfterSeconds?: number;
causes?: []; // @TODO type this, see apimachinery@v0.31.1/pkg/apis/meta/v1/types.go
// https://github.com/kubernetes/apimachinery/blob/v0.33.2/pkg/apis/meta/v1/types.go#L1020-L1040
causes?: Array<{
message?: string;
field?: string;
reason?: string;
}>;
};
status: 'Failure';
metadata?: Record<string, unknown>;
@@ -37,6 +79,6 @@ export type ApiMachineryError = {
reason: string;
};
export function isApiMachineryError(error: unknown): error is FetchError<ApiMachineryError> {
export function isApiMachineryError(error: unknown): error is ApiMachineryErrorResponse {
return isFetchError(error) && get(error.data, 'kind') === 'Status' && get(error.data, 'status') === 'Failure';
}
@@ -125,12 +125,12 @@ describe('create links', () => {
describe('stringifyErrorLike', () => {
it('should stringify error with cause', () => {
const error = new Error('Something went strong', { cause: new Error('database did not respond') });
expect(stringifyErrorLike(error)).toBe('Something went strong, cause: database did not respond');
const error = new Error('Something went wrong', { cause: new Error('database did not respond') });
expect(stringifyErrorLike(error)).toBe('Something went wrong, cause: database did not respond');
});
it('should stringify error with cause being a code', () => {
const error = new Error('Something went strong', { cause: ERROR_NEWER_CONFIGURATION });
const error = new Error('Something went wrong', { cause: ERROR_NEWER_CONFIGURATION });
expect(stringifyErrorLike(error)).toBe(getErrorMessageFromCode(ERROR_NEWER_CONFIGURATION));
});
@@ -1,4 +1,4 @@
import { sortBy } from 'lodash';
import { isString, sortBy } from 'lodash';
import { Labels, UrlQueryMap } from '@grafana/data';
import { GrafanaEdition } from '@grafana/data/internal';
@@ -34,7 +34,12 @@ import {
import { ALERTMANAGER_NAME_QUERY_KEY } from './constants';
import { getRulesSourceName } from './datasource';
import { SupportedErrors, getErrorMessageFromCode, isApiMachineryError } from './k8s/errors';
import {
KnownErrorCodes,
getErrorMessageFromApiMachineryErrorResponse,
getErrorMessageFromCode,
isApiMachineryError,
} from './k8s/errors';
import { getMatcherQueryParams } from './matchers';
import { rulesNav } from './navigation';
import * as ruleId from './rule-id';
@@ -282,15 +287,20 @@ export function isErrorLike(error: unknown): error is Error {
return Boolean(error && typeof error === 'object' && 'message' in error);
}
export function getErrorCode(error: Error): unknown {
export function getErrorCode(error: unknown): string | undefined {
if (isApiMachineryError(error) && error.data.details) {
return error.data.details.uid;
}
return error.cause;
if (isErrorLike(error) && isString(error.cause)) {
return error.cause;
}
return;
}
/* this function will check if the error passed as the first argument contains an error code */
export function isErrorMatchingCode(error: Error | undefined, code: SupportedErrors): boolean {
export function isErrorMatchingCode(error: Error | undefined, code: KnownErrorCodes): boolean {
if (!error) {
return false;
}
@@ -301,8 +311,8 @@ export function isErrorMatchingCode(error: Error | undefined, code: SupportedErr
export function stringifyErrorLike(error: unknown): string {
const fetchError = isFetchError(error);
if (fetchError) {
if (isApiMachineryError(error) && error.data.details) {
const message = getErrorMessageFromCode(error.data.details.uid);
if (isApiMachineryError(error)) {
const message = getErrorMessageFromApiMachineryErrorResponse(error);
if (message) {
return message;
}
+4 -2
View File
@@ -2095,9 +2095,11 @@
"update-errors": {
"conflict": "The notification policy tree has been updated by another user.",
"error-code": "Error message: \"{{error}}\"",
"fallback": "Something went wrong when updating your notification policies.",
"routes": {
"conflictingMatchers": "Cannot add or update route: matchers conflict with an external routing tree if we merged matchers {{-matchers}}. This would make the route unreachable."
},
"suffix": "Please refresh the page and try again.",
"title": "Error saving notification policy"
"title": "Failed to add or update notification policy"
}
},
"policy": {