mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[RAM] Mark disabled alerts as Untracked in both Stack and o11y (#164788)
## Summary Part of #164059 Implements the `Untracked` lifecycle status, and applies it to alerts when their corresponding rule is disabled. <img width="1034" alt="Screenshot 2023-08-24 at 4 24 45 PM" src="4d31545d
-9fc0-4eb3-9972-72685107184d"> <img width="904" alt="Screenshot 2023-08-24 at 4 56 32 PM" src="3d7cfa19
-5aca-4148-a9bc-d0d0c949d84b"> <img width="820" alt="Screenshot 2023-08-24 at 4 56 17 PM" src="e59870c8
-4140-4588-893a-f3f54170f78a"> ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
4fc3a43f79
commit
107239c333
63 changed files with 453 additions and 58 deletions
|
@ -9,7 +9,7 @@
|
|||
import React, { memo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBadge, EuiBadgeProps } from '@elastic/eui';
|
||||
import { AlertStatus, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
|
||||
import { AlertStatus, ALERT_STATUS_RECOVERED, ALERT_STATUS_UNTRACKED } from '@kbn/rule-data-utils';
|
||||
|
||||
export interface AlertLifecycleStatusBadgeProps {
|
||||
alertStatus: AlertStatus;
|
||||
|
@ -37,15 +37,27 @@ const FLAPPING_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const UNTRACKED_LABEL = i18n.translate(
|
||||
'alertsUIShared.components.alertLifecycleStatusBadge.untrackedLabel',
|
||||
{
|
||||
defaultMessage: 'Untracked',
|
||||
}
|
||||
);
|
||||
|
||||
interface BadgeProps {
|
||||
label: string;
|
||||
color: string;
|
||||
isDisabled?: boolean;
|
||||
iconProps?: {
|
||||
iconType: EuiBadgeProps['iconType'];
|
||||
};
|
||||
}
|
||||
|
||||
const getBadgeProps = (alertStatus: AlertStatus, flapping: boolean | undefined): BadgeProps => {
|
||||
if (alertStatus === ALERT_STATUS_UNTRACKED) {
|
||||
return { label: UNTRACKED_LABEL, color: 'default', isDisabled: true };
|
||||
}
|
||||
|
||||
// Prefer recovered over flapping
|
||||
if (alertStatus === ALERT_STATUS_RECOVERED) {
|
||||
return {
|
||||
|
@ -82,10 +94,15 @@ export const AlertLifecycleStatusBadge = memo((props: AlertLifecycleStatusBadgeP
|
|||
|
||||
const castedFlapping = castFlapping(flapping);
|
||||
|
||||
const { label, color, iconProps } = getBadgeProps(alertStatus, castedFlapping);
|
||||
const { label, color, iconProps, isDisabled } = getBadgeProps(alertStatus, castedFlapping);
|
||||
|
||||
return (
|
||||
<EuiBadge data-test-subj="alertLifecycleStatusBadge" color={color} {...iconProps}>
|
||||
<EuiBadge
|
||||
data-test-subj="alertLifecycleStatusBadge"
|
||||
isDisabled={isDisabled}
|
||||
color={color}
|
||||
{...iconProps}
|
||||
>
|
||||
{label}
|
||||
</EuiBadge>
|
||||
);
|
||||
|
|
|
@ -8,5 +8,9 @@
|
|||
|
||||
export const ALERT_STATUS_ACTIVE = 'active';
|
||||
export const ALERT_STATUS_RECOVERED = 'recovered';
|
||||
export const ALERT_STATUS_UNTRACKED = 'untracked';
|
||||
|
||||
export type AlertStatus = typeof ALERT_STATUS_ACTIVE | typeof ALERT_STATUS_RECOVERED;
|
||||
export type AlertStatus =
|
||||
| typeof ALERT_STATUS_ACTIVE
|
||||
| typeof ALERT_STATUS_RECOVERED
|
||||
| typeof ALERT_STATUS_UNTRACKED;
|
||||
|
|
|
@ -40,4 +40,5 @@ export interface AlertStatus {
|
|||
activeStartDate?: string;
|
||||
flapping: boolean;
|
||||
maintenanceWindowIds?: string[];
|
||||
tracked: boolean;
|
||||
}
|
||||
|
|
|
@ -3026,6 +3026,53 @@ describe('Alerts Client', () => {
|
|||
expect(recoveredAlert.hit).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAlertStatusToUntracked()', () => {
|
||||
test('should call updateByQuery on provided ruleIds', async () => {
|
||||
const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(
|
||||
alertsClientParams
|
||||
);
|
||||
|
||||
const opts = {
|
||||
maxAlerts,
|
||||
ruleLabel: `test: rule-name`,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
activeAlertsFromState: {},
|
||||
recoveredAlertsFromState: {},
|
||||
};
|
||||
await alertsClient.initializeExecution(opts);
|
||||
|
||||
await alertsClient.setAlertStatusToUntracked(['test-index'], ['test-rule']);
|
||||
|
||||
expect(clusterClient.updateByQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should retry updateByQuery on failure', async () => {
|
||||
clusterClient.updateByQuery.mockResponseOnce({
|
||||
total: 10,
|
||||
updated: 8,
|
||||
});
|
||||
const alertsClient = new AlertsClient<{}, {}, {}, 'default', 'recovered'>(
|
||||
alertsClientParams
|
||||
);
|
||||
|
||||
const opts = {
|
||||
maxAlerts,
|
||||
ruleLabel: `test: rule-name`,
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
activeAlertsFromState: {},
|
||||
recoveredAlertsFromState: {},
|
||||
};
|
||||
await alertsClient.initializeExecution(opts);
|
||||
|
||||
await alertsClient.setAlertStatusToUntracked(['test-index'], ['test-rule']);
|
||||
|
||||
expect(clusterClient.updateByQuery).toHaveBeenCalledTimes(2);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
'Attempt 1: Failed to untrack 2 of 10; indices test-index, ruleIds test-rule'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
*/
|
||||
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { ALERT_RULE_UUID, ALERT_UUID } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_STATUS,
|
||||
ALERT_STATUS_UNTRACKED,
|
||||
ALERT_STATUS_ACTIVE,
|
||||
ALERT_UUID,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { chunk, flatMap, isEmpty, keys } from 'lodash';
|
||||
import { SearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { Alert } from '@kbn/alerts-as-data-utils';
|
||||
|
@ -198,6 +204,51 @@ export class AlertsClient<
|
|||
return { hits, total };
|
||||
}
|
||||
|
||||
public async setAlertStatusToUntracked(indices: string[], ruleIds: string[]) {
|
||||
const esClient = await this.options.elasticsearchClientPromise;
|
||||
const terms: Array<{ term: Record<string, { value: string }> }> = ruleIds.map((ruleId) => ({
|
||||
term: {
|
||||
[ALERT_RULE_UUID]: { value: ruleId },
|
||||
},
|
||||
}));
|
||||
terms.push({
|
||||
term: {
|
||||
[ALERT_STATUS]: { value: ALERT_STATUS_ACTIVE },
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Retry this updateByQuery up to 3 times to make sure the number of documents
|
||||
// updated equals the number of documents matched
|
||||
for (let retryCount = 0; retryCount < 3; retryCount++) {
|
||||
const response = await esClient.updateByQuery({
|
||||
index: indices,
|
||||
allow_no_indices: true,
|
||||
body: {
|
||||
conflicts: 'proceed',
|
||||
script: {
|
||||
source: UNTRACK_UPDATE_PAINLESS_SCRIPT,
|
||||
lang: 'painless',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
must: terms,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (response.total === response.updated) break;
|
||||
this.options.logger.warn(
|
||||
`Attempt ${retryCount + 1}: Failed to untrack ${
|
||||
(response.total ?? 0) - (response.updated ?? 0)
|
||||
} of ${response.total}; indices ${indices}, ruleIds ${ruleIds}`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
this.options.logger.error(`Error marking ${ruleIds} as untracked - ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public report(
|
||||
alert: ReportedAlert<
|
||||
AlertData,
|
||||
|
@ -562,3 +613,11 @@ export class AlertsClient<
|
|||
return this._isUsingDataStreams;
|
||||
}
|
||||
}
|
||||
|
||||
const UNTRACK_UPDATE_PAINLESS_SCRIPT = `
|
||||
// Certain rule types don't flatten their AAD values, apply the ALERT_STATUS key to them directly
|
||||
if (!ctx._source.containsKey('${ALERT_STATUS}') || ctx._source['${ALERT_STATUS}'].empty) {
|
||||
ctx._source.${ALERT_STATUS} = '${ALERT_STATUS_UNTRACKED}';
|
||||
} else {
|
||||
ctx._source['${ALERT_STATUS}'] = '${ALERT_STATUS_UNTRACKED}'
|
||||
}`;
|
||||
|
|
|
@ -232,4 +232,8 @@ export class LegacyAlertsClient<
|
|||
}
|
||||
|
||||
public async persistAlerts() {}
|
||||
|
||||
public async setAlertStatusToUntracked() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ export interface IAlertsClient<
|
|||
alertsToReturn: Record<string, RawAlertInstance>;
|
||||
recoveredAlertsToReturn: Record<string, RawAlertInstance>;
|
||||
};
|
||||
setAlertStatusToUntracked(indices: string[], ruleIds: string[]): Promise<void>;
|
||||
factory(): PublicAlertFactory<
|
||||
State,
|
||||
Context,
|
||||
|
|
|
@ -56,6 +56,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
maxScheduledPerMinute: 1000,
|
||||
internalSavedObjectsRepository,
|
||||
};
|
||||
|
|
|
@ -79,6 +79,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
const getBulkOperationStatusErrorResponse = (statusCode: number) => ({
|
||||
|
|
|
@ -102,6 +102,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: isAuthenticationTypeApiKeyMock,
|
||||
getAuthenticationAPIKey: getAuthenticationApiKeyMock,
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
const paramsModifier = jest.fn();
|
||||
|
||||
|
|
|
@ -82,6 +82,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -53,6 +53,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
const getMockAggregationResult = (
|
||||
|
|
|
@ -127,6 +127,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
"tracked": true,
|
||||
"uuid": undefined,
|
||||
},
|
||||
"alert-2": Object {
|
||||
|
@ -135,6 +136,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
"tracked": true,
|
||||
"uuid": undefined,
|
||||
},
|
||||
},
|
||||
|
@ -241,6 +243,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
|
@ -283,6 +286,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
|
@ -324,6 +328,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
|
@ -366,6 +371,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
|
@ -408,6 +414,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
|
@ -450,6 +457,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
|
@ -490,6 +498,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
|
@ -534,6 +543,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": true,
|
||||
"status": "Active",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"alert-2": Object {
|
||||
|
@ -542,6 +552,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
},
|
||||
|
@ -593,6 +604,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"alert-2": Object {
|
||||
|
@ -601,6 +613,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
},
|
||||
|
@ -639,6 +652,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": true,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -105,6 +105,8 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams)
|
|||
status.activeStartDate = undefined;
|
||||
status.actionGroupId = undefined;
|
||||
}
|
||||
|
||||
status.tracked = action !== EVENT_LOG_ACTIONS.untrackedInstance;
|
||||
}
|
||||
|
||||
for (const event of executionEvents.reverse()) {
|
||||
|
@ -169,6 +171,7 @@ function getAlertStatus(
|
|||
actionGroupId: undefined,
|
||||
activeStartDate: undefined,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
};
|
||||
alerts.set(alertId, status);
|
||||
return status;
|
||||
|
|
|
@ -111,6 +111,7 @@ export const EVENT_LOG_ACTIONS = {
|
|||
recoveredInstance: 'recovered-instance',
|
||||
activeInstance: 'active-instance',
|
||||
executeTimeout: 'execute-timeout',
|
||||
untrackedInstance: 'untracked-instance',
|
||||
};
|
||||
export const LEGACY_EVENT_LOG_ACTIONS = {
|
||||
resolvedInstance: 'resolved-instance',
|
||||
|
@ -494,6 +495,8 @@ export class AlertingPlugin {
|
|||
eventLogger: this.eventLogger,
|
||||
minimumScheduleInterval: this.config.rules.minimumScheduleInterval,
|
||||
maxScheduledPerMinute: this.config.rules.maxScheduledPerMinute,
|
||||
getAlertIndicesAlias: createGetAlertIndicesAliasFn(this.ruleTypeRegistry!),
|
||||
alertsService: this.alertsService,
|
||||
});
|
||||
|
||||
rulesSettingsClientFactory.initialize({
|
||||
|
|
|
@ -52,6 +52,8 @@ const rulesClientParams: jest.Mocked<RulesClientContext> = {
|
|||
fieldsToExcludeFromPublicApi: [],
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
const username = 'test';
|
||||
|
|
|
@ -14,7 +14,7 @@ export { getAuthorizationFilter } from './get_authorization_filter';
|
|||
export { checkAuthorizationAndGetTotal } from './check_authorization_and_get_total';
|
||||
export { scheduleTask } from './schedule_task';
|
||||
export { createNewAPIKeySet } from './create_new_api_key_set';
|
||||
export { recoverRuleAlerts } from './recover_rule_alerts';
|
||||
export { untrackRuleAlerts } from './untrack_rule_alerts';
|
||||
export { migrateLegacyActions } from './siem_legacy_actions/migrate_legacy_actions';
|
||||
export { formatLegacyActions } from './siem_legacy_actions/format_legacy_actions';
|
||||
export { addGeneratedActionValues } from './add_generated_action_values';
|
||||
|
|
|
@ -15,40 +15,50 @@ import { EVENT_LOG_ACTIONS } from '../../plugin';
|
|||
import { createAlertEventLogRecordObject } from '../../lib/create_alert_event_log_record_object';
|
||||
import { RulesClientContext } from '../types';
|
||||
|
||||
export const recoverRuleAlerts = async (
|
||||
export const untrackRuleAlerts = async (
|
||||
context: RulesClientContext,
|
||||
id: string,
|
||||
attributes: RawRule
|
||||
) => {
|
||||
return withSpan({ name: 'recoverRuleAlerts', type: 'rules' }, async () => {
|
||||
return withSpan({ name: 'untrackRuleAlerts', type: 'rules' }, async () => {
|
||||
if (!context.eventLogger || !attributes.scheduledTaskId) return;
|
||||
try {
|
||||
const { state } = taskInstanceToAlertTaskInstance(
|
||||
const taskInstance = taskInstanceToAlertTaskInstance(
|
||||
await context.taskManager.get(attributes.scheduledTaskId),
|
||||
attributes as unknown as SanitizedRule
|
||||
);
|
||||
|
||||
const recoveredAlerts = mapValues<Record<string, RawAlert>, Alert>(
|
||||
const { state } = taskInstance;
|
||||
|
||||
const untrackedAlerts = mapValues<Record<string, RawAlert>, Alert>(
|
||||
state.alertInstances ?? {},
|
||||
(rawAlertInstance, alertId) => new Alert(alertId, rawAlertInstance)
|
||||
);
|
||||
const recoveredAlertIds = Object.keys(recoveredAlerts);
|
||||
|
||||
for (const alertId of recoveredAlertIds) {
|
||||
const { group: actionGroup } = recoveredAlerts[alertId].getLastScheduledActions() ?? {};
|
||||
const instanceState = recoveredAlerts[alertId].getState();
|
||||
const message = `instance '${alertId}' has recovered due to the rule was disabled`;
|
||||
const alertUuid = recoveredAlerts[alertId].getUuid();
|
||||
const untrackedAlertIds = Object.keys(untrackedAlerts);
|
||||
|
||||
const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId);
|
||||
|
||||
const { autoRecoverAlerts: isLifecycleAlert } = ruleType;
|
||||
|
||||
// Untrack Stack alerts
|
||||
// TODO: Replace this loop with an Alerts As Data implmentation when Stack Rules use Alerts As Data
|
||||
// instead of the Kibana Event Log
|
||||
for (const alertId of untrackedAlertIds) {
|
||||
const { group: actionGroup } = untrackedAlerts[alertId].getLastScheduledActions() ?? {};
|
||||
const instanceState = untrackedAlerts[alertId].getState();
|
||||
const message = `instance '${alertId}' has been untracked because the rule was disabled`;
|
||||
const alertUuid = untrackedAlerts[alertId].getUuid();
|
||||
|
||||
const event = createAlertEventLogRecordObject({
|
||||
ruleId: id,
|
||||
ruleName: attributes.name,
|
||||
ruleRevision: attributes.revision,
|
||||
ruleType: context.ruleTypeRegistry.get(attributes.alertTypeId),
|
||||
ruleType,
|
||||
consumer: attributes.consumer,
|
||||
instanceId: alertId,
|
||||
alertUuid,
|
||||
action: EVENT_LOG_ACTIONS.recoveredInstance,
|
||||
action: EVENT_LOG_ACTIONS.untrackedInstance,
|
||||
message,
|
||||
state: instanceState,
|
||||
group: actionGroup,
|
||||
|
@ -65,10 +75,32 @@ export const recoverRuleAlerts = async (
|
|||
});
|
||||
context.eventLogger.logEvent(event);
|
||||
}
|
||||
|
||||
// Untrack Lifecycle alerts (Alerts As Data-enabled)
|
||||
if (isLifecycleAlert) {
|
||||
const alertsClient = await context.alertsService?.createAlertsClient({
|
||||
namespace: context.namespace!,
|
||||
rule: {
|
||||
id,
|
||||
name: attributes.name,
|
||||
consumer: attributes.consumer,
|
||||
revision: attributes.revision,
|
||||
spaceId: context.spaceId,
|
||||
tags: attributes.tags,
|
||||
parameters: attributes.parameters,
|
||||
executionId: '',
|
||||
},
|
||||
ruleType,
|
||||
logger: context.logger,
|
||||
});
|
||||
if (!alertsClient) throw new Error('Could not create alertsClient');
|
||||
const indices = context.getAlertIndicesAlias([ruleType.id], context.spaceId);
|
||||
await alertsClient.setAlertStatusToUntracked(indices, [id]);
|
||||
}
|
||||
} catch (error) {
|
||||
// this should not block the rest of the disable process
|
||||
context.logger.warn(
|
||||
`rulesClient.disable('${id}') - Could not write recovery events - ${error.message}`
|
||||
`rulesClient.disable('${id}') - Could not write untrack events - ${error.message}`
|
||||
);
|
||||
}
|
||||
});
|
|
@ -22,7 +22,7 @@ import {
|
|||
getAuthorizationFilter,
|
||||
checkAuthorizationAndGetTotal,
|
||||
getAlertFromRaw,
|
||||
recoverRuleAlerts,
|
||||
untrackRuleAlerts,
|
||||
updateMeta,
|
||||
migrateLegacyActions,
|
||||
} from '../lib';
|
||||
|
@ -58,11 +58,12 @@ export const bulkDisableRules = async (context: RulesClientContext, options: Bul
|
|||
})
|
||||
);
|
||||
|
||||
const [taskIdsToDisable, taskIdsToDelete] = accListSpecificForBulkOperation;
|
||||
const [taskIdsToDisable, taskIdsToDelete, taskIdsToClearState] = accListSpecificForBulkOperation;
|
||||
|
||||
await Promise.allSettled([
|
||||
tryToDisableTasks({
|
||||
taskIdsToDisable,
|
||||
taskIdsToClearState,
|
||||
logger: context.logger,
|
||||
taskManager: context.taskManager,
|
||||
}),
|
||||
|
@ -114,7 +115,7 @@ const bulkDisableRulesWithOCC = async (
|
|||
for await (const response of rulesFinder.find()) {
|
||||
await pMap(response.saved_objects, async (rule) => {
|
||||
try {
|
||||
await recoverRuleAlerts(context, rule.id, rule.attributes);
|
||||
await untrackRuleAlerts(context, rule.id, rule.attributes);
|
||||
|
||||
if (rule.attributes.name) {
|
||||
ruleNameToRuleIdMapping[rule.id] = rule.attributes.name;
|
||||
|
@ -193,6 +194,7 @@ const bulkDisableRulesWithOCC = async (
|
|||
|
||||
const taskIdsToDisable: string[] = [];
|
||||
const taskIdsToDelete: string[] = [];
|
||||
const taskIdsToClearState: string[] = [];
|
||||
const disabledRules: Array<SavedObjectsBulkUpdateObject<RawRule>> = [];
|
||||
|
||||
result.saved_objects.forEach((rule) => {
|
||||
|
@ -202,6 +204,12 @@ const bulkDisableRulesWithOCC = async (
|
|||
taskIdsToDelete.push(rule.attributes.scheduledTaskId);
|
||||
} else {
|
||||
taskIdsToDisable.push(rule.attributes.scheduledTaskId);
|
||||
if (rule.attributes.alertTypeId) {
|
||||
const { autoRecoverAlerts: isLifecycleAlert } = context.ruleTypeRegistry.get(
|
||||
rule.attributes.alertTypeId
|
||||
);
|
||||
if (isLifecycleAlert) taskIdsToClearState.push(rule.attributes.scheduledTaskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
disabledRules.push(rule);
|
||||
|
@ -221,23 +229,28 @@ const bulkDisableRulesWithOCC = async (
|
|||
errors,
|
||||
// TODO: delete the casting when we do versioning of bulk disable api
|
||||
rules: disabledRules as Array<SavedObjectsBulkUpdateObject<RuleAttributes>>,
|
||||
accListSpecificForBulkOperation: [taskIdsToDisable, taskIdsToDelete],
|
||||
accListSpecificForBulkOperation: [taskIdsToDisable, taskIdsToDelete, taskIdsToClearState],
|
||||
};
|
||||
};
|
||||
|
||||
const tryToDisableTasks = async ({
|
||||
taskIdsToDisable,
|
||||
taskIdsToClearState,
|
||||
logger,
|
||||
taskManager,
|
||||
}: {
|
||||
taskIdsToDisable: string[];
|
||||
taskIdsToClearState: string[];
|
||||
logger: Logger;
|
||||
taskManager: TaskManagerStartContract;
|
||||
}) => {
|
||||
return await withSpan({ name: 'taskManager.bulkDisable', type: 'rules' }, async () => {
|
||||
if (taskIdsToDisable.length > 0) {
|
||||
try {
|
||||
const resultFromDisablingTasks = await taskManager.bulkDisable(taskIdsToDisable);
|
||||
const resultFromDisablingTasks = await taskManager.bulkDisable(
|
||||
taskIdsToDisable,
|
||||
taskIdsToClearState
|
||||
);
|
||||
if (resultFromDisablingTasks.tasks.length) {
|
||||
logger.debug(
|
||||
`Successfully disabled schedules for underlying tasks: ${resultFromDisablingTasks.tasks
|
||||
|
|
|
@ -11,7 +11,7 @@ import { WriteOperations, AlertingAuthorizationEntity } from '../../authorizatio
|
|||
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import { RulesClientContext } from '../types';
|
||||
import { recoverRuleAlerts, updateMeta, migrateLegacyActions } from '../lib';
|
||||
import { untrackRuleAlerts, updateMeta, migrateLegacyActions } from '../lib';
|
||||
|
||||
export async function disable(context: RulesClientContext, { id }: { id: string }): Promise<void> {
|
||||
return await retryIfConflicts(
|
||||
|
@ -43,7 +43,7 @@ async function disableWithOCC(context: RulesClientContext, { id }: { id: string
|
|||
references = alert.references;
|
||||
}
|
||||
|
||||
await recoverRuleAlerts(context, id, attributes);
|
||||
await untrackRuleAlerts(context, id, attributes);
|
||||
|
||||
try {
|
||||
await context.authorization.ensureAuthorized({
|
||||
|
@ -102,6 +102,9 @@ async function disableWithOCC(context: RulesClientContext, { id }: { id: string
|
|||
: {}),
|
||||
}
|
||||
);
|
||||
const { autoRecoverAlerts: isLifecycleAlert } = context.ruleTypeRegistry.get(
|
||||
attributes.alertTypeId
|
||||
);
|
||||
|
||||
// If the scheduledTaskId does not match the rule id, we should
|
||||
// remove the task, otherwise mark the task as disabled
|
||||
|
@ -109,7 +112,10 @@ async function disableWithOCC(context: RulesClientContext, { id }: { id: string
|
|||
if (attributes.scheduledTaskId !== id) {
|
||||
await context.taskManager.removeIfExists(attributes.scheduledTaskId);
|
||||
} else {
|
||||
await context.taskManager.bulkDisable([attributes.scheduledTaskId]);
|
||||
await context.taskManager.bulkDisable(
|
||||
[attributes.scheduledTaskId],
|
||||
Boolean(isLifecycleAlert)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,6 +87,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -251,7 +253,7 @@ describe('bulkDisableRules', () => {
|
|||
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(4);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledTimes(1);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['id1']);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['id1'], []);
|
||||
expect(result).toStrictEqual({
|
||||
errors: [{ message: 'UPS', rule: { id: 'id2', name: 'fakeName' }, status: 409 }],
|
||||
rules: [returnedDisabledRule1],
|
||||
|
@ -388,7 +390,7 @@ describe('bulkDisableRules', () => {
|
|||
await rulesClient.bulkDisableRules({ filter: 'fake_filter' });
|
||||
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledTimes(1);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['id1', 'id2']);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['id1', 'id2'], []);
|
||||
|
||||
expect(logger.debug).toBeCalledTimes(1);
|
||||
expect(logger.debug).toBeCalledWith(
|
||||
|
@ -451,7 +453,7 @@ describe('bulkDisableRules', () => {
|
|||
await rulesClient.bulkDisableRules({ filter: 'fake_filter' });
|
||||
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledTimes(1);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['id1']);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['id1'], []);
|
||||
|
||||
expect(logger.debug).toBeCalledTimes(1);
|
||||
expect(logger.debug).toBeCalledWith(
|
||||
|
@ -477,7 +479,7 @@ describe('bulkDisableRules', () => {
|
|||
await rulesClient.bulkDisableRules({ filter: 'fake_filter' });
|
||||
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledTimes(1);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['id1']);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['id1'], []);
|
||||
});
|
||||
|
||||
test('should not throw an error if taskManager.bulkDisable throw an error', async () => {
|
||||
|
@ -611,7 +613,7 @@ describe('bulkDisableRules', () => {
|
|||
|
||||
expect(logger.warn).toHaveBeenCalledTimes(2);
|
||||
expect(logger.warn).toHaveBeenLastCalledWith(
|
||||
"rulesClient.disable('id2') - Could not write recovery events - UPS"
|
||||
"rulesClient.disable('id2') - Could not write untrack events - UPS"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -82,6 +82,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -69,6 +69,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
eventLogger,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
describe('clearExpiredSnoozes()', () => {
|
||||
|
|
|
@ -71,6 +71,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -73,6 +73,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
eventLogger,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -255,11 +257,11 @@ describe('disable()', () => {
|
|||
version: '123',
|
||||
}
|
||||
);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['1']);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['1'], false);
|
||||
expect(taskManager.removeIfExists).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('disables the rule with calling event log to "recover" the alert instances from the task state', async () => {
|
||||
test('disables the rule with calling event log to untrack the alert instances from the task state', async () => {
|
||||
const scheduledTaskId = '1';
|
||||
taskManager.get.mockResolvedValue({
|
||||
id: scheduledTaskId,
|
||||
|
@ -329,13 +331,13 @@ describe('disable()', () => {
|
|||
version: '123',
|
||||
}
|
||||
);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['1']);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['1'], false);
|
||||
expect(taskManager.removeIfExists).not.toHaveBeenCalledWith();
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(1);
|
||||
expect(eventLogger.logEvent.mock.calls[0][0]).toStrictEqual({
|
||||
event: {
|
||||
action: 'recovered-instance',
|
||||
action: 'untracked-instance',
|
||||
category: ['alerts'],
|
||||
kind: 'alert',
|
||||
},
|
||||
|
@ -363,7 +365,7 @@ describe('disable()', () => {
|
|||
],
|
||||
space_ids: ['default'],
|
||||
},
|
||||
message: "instance '1' has recovered due to the rule was disabled",
|
||||
message: "instance '1' has been untracked because the rule was disabled",
|
||||
rule: {
|
||||
category: '123',
|
||||
id: '1',
|
||||
|
@ -373,7 +375,7 @@ describe('disable()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('disables the rule even if unable to retrieve task manager doc to generate recovery event log events', async () => {
|
||||
test('disables the rule even if unable to retrieve task manager doc to generate untrack event log events', async () => {
|
||||
taskManager.get.mockRejectedValueOnce(new Error('Fail'));
|
||||
await rulesClient.disable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
|
||||
|
@ -414,12 +416,12 @@ describe('disable()', () => {
|
|||
version: '123',
|
||||
}
|
||||
);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['1']);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['1'], false);
|
||||
expect(taskManager.removeIfExists).not.toHaveBeenCalledWith();
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(0);
|
||||
expect(rulesClientParams.logger.warn).toHaveBeenCalledWith(
|
||||
`rulesClient.disable('1') - Could not write recovery events - Fail`
|
||||
`rulesClient.disable('1') - Could not write untrack events - Fail`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -459,7 +461,7 @@ describe('disable()', () => {
|
|||
version: '123',
|
||||
}
|
||||
);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['1']);
|
||||
expect(taskManager.bulkDisable).toHaveBeenCalledWith(['1'], false);
|
||||
expect(taskManager.removeIfExists).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
|
|
|
@ -70,6 +70,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
setGlobalDate();
|
||||
|
|
|
@ -63,6 +63,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -60,6 +60,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -59,6 +59,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -57,6 +57,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -170,6 +172,7 @@ describe('getAlertSummary()', () => {
|
|||
"flapping": true,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"alert-muted-no-activity": Object {
|
||||
|
@ -178,6 +181,7 @@ describe('getAlertSummary()', () => {
|
|||
"flapping": false,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
"tracked": true,
|
||||
"uuid": undefined,
|
||||
},
|
||||
"alert-previously-active": Object {
|
||||
|
@ -186,6 +190,7 @@ describe('getAlertSummary()', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
"tracked": true,
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -60,6 +60,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -54,6 +54,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
const listedTypes = new Set<RegistryRuleType>([
|
||||
|
|
|
@ -55,6 +55,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -60,6 +60,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -53,6 +53,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
setGlobalDate();
|
||||
|
|
|
@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -90,6 +90,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -58,6 +58,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -32,6 +32,8 @@ import {
|
|||
} from '../types';
|
||||
import { AlertingAuthorization } from '../authorization';
|
||||
import { AlertingRulesConfig } from '../config';
|
||||
import { GetAlertIndicesAlias } from '../lib';
|
||||
import { AlertsService } from '../alerts_service';
|
||||
|
||||
export type {
|
||||
BulkEditOperation,
|
||||
|
@ -74,6 +76,8 @@ export interface RulesClientContext {
|
|||
readonly fieldsToExcludeFromPublicApi: Array<keyof SanitizedRule>;
|
||||
readonly isAuthenticationTypeAPIKey: () => boolean;
|
||||
readonly getAuthenticationAPIKey: (name: string) => CreateAPIKeyResult;
|
||||
readonly getAlertIndicesAlias: GetAlertIndicesAlias;
|
||||
readonly alertsService: AlertsService | null;
|
||||
}
|
||||
|
||||
export type NormalizedAlertAction = Omit<RuleAction, 'actionTypeId'>;
|
||||
|
|
|
@ -66,6 +66,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
};
|
||||
|
||||
// this suite consists of two suites running tests against mutable RulesClient APIs:
|
||||
|
|
|
@ -46,6 +46,8 @@ const rulesClientFactoryParams: jest.Mocked<RulesClientFactoryOpts> = {
|
|||
ruleTypeRegistry: ruleTypeRegistryMock.create(),
|
||||
getSpaceId: jest.fn(),
|
||||
spaceIdToNamespace: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
maxScheduledPerMinute: 10000,
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
internalSavedObjectsRepository,
|
||||
|
@ -112,6 +114,8 @@ test('creates a rules client with proper constructor arguments when security is
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: expect.any(Function),
|
||||
getAuthenticationAPIKey: expect.any(Function),
|
||||
getAlertIndicesAlias: expect.any(Function),
|
||||
alertsService: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -154,6 +158,8 @@ test('creates a rules client with proper constructor arguments', async () => {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: expect.any(Function),
|
||||
getAuthenticationAPIKey: expect.any(Function),
|
||||
getAlertIndicesAlias: expect.any(Function),
|
||||
alertsService: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ import { RuleTypeRegistry, SpaceIdToNamespaceFunction } from './types';
|
|||
import { RulesClient } from './rules_client';
|
||||
import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory';
|
||||
import { AlertingRulesConfig } from './config';
|
||||
import { GetAlertIndicesAlias } from './lib';
|
||||
import { AlertsService } from './alerts_service/alerts_service';
|
||||
export interface RulesClientFactoryOpts {
|
||||
logger: Logger;
|
||||
taskManager: TaskManagerStartContract;
|
||||
|
@ -43,6 +45,8 @@ export interface RulesClientFactoryOpts {
|
|||
eventLogger?: IEventLogger;
|
||||
minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval'];
|
||||
maxScheduledPerMinute: AlertingRulesConfig['maxScheduledPerMinute'];
|
||||
getAlertIndicesAlias: GetAlertIndicesAlias;
|
||||
alertsService: AlertsService | null;
|
||||
}
|
||||
|
||||
export class RulesClientFactory {
|
||||
|
@ -63,6 +67,8 @@ export class RulesClientFactory {
|
|||
private eventLogger?: IEventLogger;
|
||||
private minimumScheduleInterval!: AlertingRulesConfig['minimumScheduleInterval'];
|
||||
private maxScheduledPerMinute!: AlertingRulesConfig['maxScheduledPerMinute'];
|
||||
private getAlertIndicesAlias!: GetAlertIndicesAlias;
|
||||
private alertsService!: AlertsService | null;
|
||||
|
||||
public initialize(options: RulesClientFactoryOpts) {
|
||||
if (this.isInitialized) {
|
||||
|
@ -85,6 +91,8 @@ export class RulesClientFactory {
|
|||
this.eventLogger = options.eventLogger;
|
||||
this.minimumScheduleInterval = options.minimumScheduleInterval;
|
||||
this.maxScheduledPerMinute = options.maxScheduledPerMinute;
|
||||
this.getAlertIndicesAlias = options.getAlertIndicesAlias;
|
||||
this.alertsService = options.alertsService;
|
||||
}
|
||||
|
||||
public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): RulesClient {
|
||||
|
@ -113,6 +121,8 @@ export class RulesClientFactory {
|
|||
internalSavedObjectsRepository: this.internalSavedObjectsRepository,
|
||||
encryptedSavedObjectsClient: this.encryptedSavedObjectsClient,
|
||||
auditLogger: securityPluginSetup?.audit.asScoped(request),
|
||||
getAlertIndicesAlias: this.getAlertIndicesAlias,
|
||||
alertsService: this.alertsService,
|
||||
async getUserName() {
|
||||
if (!securityPluginStart) {
|
||||
return null;
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
ALERT_STATUS,
|
||||
ALERT_STATUS_ACTIVE,
|
||||
ALERT_STATUS_RECOVERED,
|
||||
ALERT_STATUS_UNTRACKED,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import type { AlertStatusFilter } from './types';
|
||||
|
||||
export const ALERT_STATUS_ALL = 'all';
|
||||
|
@ -46,9 +51,24 @@ export const RECOVERED_ALERTS: AlertStatusFilter = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const UNTRACKED_ALERTS: AlertStatusFilter = {
|
||||
status: ALERT_STATUS_UNTRACKED,
|
||||
query: {
|
||||
term: {
|
||||
[ALERT_STATUS]: {
|
||||
value: ALERT_STATUS_UNTRACKED,
|
||||
},
|
||||
},
|
||||
},
|
||||
label: i18n.translate('xpack.infra.hostsViewPage.tabs.alerts.alertStatusFilter.untracked', {
|
||||
defaultMessage: 'Untracked',
|
||||
}),
|
||||
};
|
||||
|
||||
export const ALERT_STATUS_QUERY = {
|
||||
[ACTIVE_ALERTS.status]: ACTIVE_ALERTS.query,
|
||||
[RECOVERED_ALERTS.status]: RECOVERED_ALERTS.query,
|
||||
[UNTRACKED_ALERTS.status]: UNTRACKED_ALERTS.query,
|
||||
};
|
||||
|
||||
export const ALERTS_DOC_HREF =
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
ACTIVE_ALERTS,
|
||||
ALL_ALERTS,
|
||||
RECOVERED_ALERTS,
|
||||
UNTRACKED_ALERTS,
|
||||
} from '../../../../../../common/alerts/constants';
|
||||
export interface AlertStatusFilterProps {
|
||||
status: AlertStatus;
|
||||
|
@ -38,6 +39,12 @@ const options: EuiButtonGroupOptionProps[] = [
|
|||
value: RECOVERED_ALERTS.query,
|
||||
'data-test-subj': 'hostsView-alert-status-filter-recovered-button',
|
||||
},
|
||||
{
|
||||
id: UNTRACKED_ALERTS.status,
|
||||
label: UNTRACKED_ALERTS.label,
|
||||
value: UNTRACKED_ALERTS.query,
|
||||
'data-test-subj': 'hostsView-alert-status-filter-untracked-button',
|
||||
},
|
||||
];
|
||||
|
||||
export function AlertsStatusFilter({ status, onChange }: AlertStatusFilterProps) {
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
ALERT_STATUS_ACTIVE,
|
||||
ALERT_STATUS_RECOVERED,
|
||||
ALERT_STATUS_UNTRACKED,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { ALERT_STATUS_ALL } from './constants';
|
||||
|
||||
export type Maybe<T> = T | null | undefined;
|
||||
|
@ -29,6 +33,7 @@ export interface ApmIndicesConfig {
|
|||
export type AlertStatus =
|
||||
| typeof ALERT_STATUS_ACTIVE
|
||||
| typeof ALERT_STATUS_RECOVERED
|
||||
| typeof ALERT_STATUS_UNTRACKED
|
||||
| typeof ALERT_STATUS_ALL;
|
||||
|
||||
export interface AlertStatusFilter {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ALL_ALERTS, ACTIVE_ALERTS, RECOVERED_ALERTS } from '../constants';
|
||||
import { ALL_ALERTS, ACTIVE_ALERTS, RECOVERED_ALERTS, UNTRACKED_ALERTS } from '../constants';
|
||||
import { AlertStatusFilterProps } from '../types';
|
||||
import { AlertStatus } from '../../../../common/typings';
|
||||
|
||||
|
@ -31,6 +31,12 @@ const options: EuiButtonGroupOptionProps[] = [
|
|||
value: RECOVERED_ALERTS.query,
|
||||
'data-test-subj': 'alert-status-filter-recovered-button',
|
||||
},
|
||||
{
|
||||
id: UNTRACKED_ALERTS.status,
|
||||
label: UNTRACKED_ALERTS.label,
|
||||
value: UNTRACKED_ALERTS.query,
|
||||
'data-test-subj': 'alert-status-filter-untracked-button',
|
||||
},
|
||||
];
|
||||
|
||||
export function AlertsStatusFilter({ status, onChange }: AlertStatusFilterProps) {
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
ALERT_STATUS_ACTIVE,
|
||||
ALERT_STATUS_RECOVERED,
|
||||
ALERT_STATUS_UNTRACKED,
|
||||
ALERT_STATUS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { AlertStatusFilter } from '../../../common/typings';
|
||||
import { ALERT_STATUS_ALL } from '../../../common/constants';
|
||||
|
||||
|
@ -38,7 +43,16 @@ export const RECOVERED_ALERTS: AlertStatusFilter = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const UNTRACKED_ALERTS: AlertStatusFilter = {
|
||||
status: ALERT_STATUS_UNTRACKED,
|
||||
query: `${ALERT_STATUS}: "${ALERT_STATUS_UNTRACKED}"`,
|
||||
label: i18n.translate('xpack.observability.alerts.alertStatusFilter.untracked', {
|
||||
defaultMessage: 'Untracked',
|
||||
}),
|
||||
};
|
||||
|
||||
export const ALERT_STATUS_QUERY = {
|
||||
[ACTIVE_ALERTS.status]: ACTIVE_ALERTS.query,
|
||||
[RECOVERED_ALERTS.status]: RECOVERED_ALERTS.query,
|
||||
[UNTRACKED_ALERTS.status]: UNTRACKED_ALERTS.query,
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
EuiFlyoutBody,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
AlertStatus,
|
||||
ALERT_DURATION,
|
||||
ALERT_EVALUATION_THRESHOLD,
|
||||
ALERT_EVALUATION_VALUE,
|
||||
|
@ -23,8 +24,7 @@ import {
|
|||
ALERT_RULE_CATEGORY,
|
||||
ALERT_RULE_TYPE_ID,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_STATUS_ACTIVE,
|
||||
ALERT_STATUS_RECOVERED,
|
||||
ALERT_STATUS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AlertLifecycleStatusBadge } from '@kbn/alerts-ui-shared';
|
||||
|
@ -64,7 +64,7 @@ export function AlertsFlyoutBody({ alert, id: pageId }: FlyoutProps) {
|
|||
}),
|
||||
description: (
|
||||
<AlertLifecycleStatusBadge
|
||||
alertStatus={alert.active ? ALERT_STATUS_ACTIVE : ALERT_STATUS_RECOVERED}
|
||||
alertStatus={alert.fields[ALERT_STATUS] as AlertStatus}
|
||||
flapping={alert.fields[ALERT_FLAPPING]}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -152,13 +152,20 @@ export class TaskScheduling {
|
|||
return await this.store.bulkSchedule(modifiedTasks);
|
||||
}
|
||||
|
||||
public async bulkDisable(taskIds: string[]) {
|
||||
public async bulkDisable(taskIds: string[], clearStateIdsOrBoolean?: string[] | boolean) {
|
||||
return await retryableBulkUpdate({
|
||||
taskIds,
|
||||
store: this.store,
|
||||
getTasks: async (ids) => await this.bulkGetTasksHelper(ids),
|
||||
filter: (task) => !!task.enabled,
|
||||
map: (task) => ({ ...task, enabled: false }),
|
||||
map: (task) => ({
|
||||
...task,
|
||||
enabled: false,
|
||||
...((Array.isArray(clearStateIdsOrBoolean) && clearStateIdsOrBoolean.includes(task.id)) ||
|
||||
clearStateIdsOrBoolean === true
|
||||
? { state: {} }
|
||||
: {}),
|
||||
}),
|
||||
validate: false,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ describe('loadRuleSummary', () => {
|
|||
flapping: true,
|
||||
status: 'OK',
|
||||
muted: false,
|
||||
tracked: true,
|
||||
},
|
||||
},
|
||||
consumer: 'alerts',
|
||||
|
@ -47,6 +48,7 @@ describe('loadRuleSummary', () => {
|
|||
flapping: true,
|
||||
status: 'OK',
|
||||
muted: false,
|
||||
tracked: true,
|
||||
},
|
||||
},
|
||||
consumer: 'alerts',
|
||||
|
|
|
@ -128,12 +128,14 @@ describe('rules', () => {
|
|||
muted: false,
|
||||
actionGroupId: 'default',
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
second_rule: {
|
||||
status: 'Active',
|
||||
muted: false,
|
||||
actionGroupId: 'action group id unknown',
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -192,11 +194,13 @@ describe('rules', () => {
|
|||
status: 'OK',
|
||||
muted: false,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
['us-east']: {
|
||||
status: 'OK',
|
||||
muted: false,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -228,8 +232,8 @@ describe('rules', () => {
|
|||
mutedInstanceIds: ['us-west', 'us-east'],
|
||||
});
|
||||
const ruleType = mockRuleType();
|
||||
const ruleUsWest: AlertStatus = { status: 'OK', muted: false, flapping: false };
|
||||
const ruleUsEast: AlertStatus = { status: 'OK', muted: false, flapping: false };
|
||||
const ruleUsWest: AlertStatus = { status: 'OK', muted: false, flapping: false, tracked: true };
|
||||
const ruleUsEast: AlertStatus = { status: 'OK', muted: false, flapping: false, tracked: true };
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleComponentWithProvider
|
||||
|
@ -243,11 +247,13 @@ describe('rules', () => {
|
|||
status: 'OK',
|
||||
muted: false,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
'us-east': {
|
||||
status: 'OK',
|
||||
muted: false,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
},
|
||||
})}
|
||||
|
@ -275,6 +281,7 @@ describe('alertToListItem', () => {
|
|||
activeStartDate: fake2MinutesAgo.toISOString(),
|
||||
actionGroupId: 'testing',
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
};
|
||||
|
||||
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||
|
@ -285,6 +292,7 @@ describe('alertToListItem', () => {
|
|||
sortPriority: 0,
|
||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||
isMuted: false,
|
||||
tracked: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -295,6 +303,7 @@ describe('alertToListItem', () => {
|
|||
muted: false,
|
||||
activeStartDate: fake2MinutesAgo.toISOString(),
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
};
|
||||
|
||||
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||
|
@ -305,6 +314,7 @@ describe('alertToListItem', () => {
|
|||
sortPriority: 0,
|
||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||
isMuted: false,
|
||||
tracked: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -316,6 +326,7 @@ describe('alertToListItem', () => {
|
|||
activeStartDate: fake2MinutesAgo.toISOString(),
|
||||
actionGroupId: 'default',
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
};
|
||||
|
||||
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||
|
@ -326,6 +337,7 @@ describe('alertToListItem', () => {
|
|||
sortPriority: 0,
|
||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||
isMuted: true,
|
||||
tracked: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -335,6 +347,7 @@ describe('alertToListItem', () => {
|
|||
muted: false,
|
||||
actionGroupId: 'default',
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
};
|
||||
|
||||
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||
|
@ -345,6 +358,7 @@ describe('alertToListItem', () => {
|
|||
duration: 0,
|
||||
sortPriority: 0,
|
||||
isMuted: false,
|
||||
tracked: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -354,6 +368,7 @@ describe('alertToListItem', () => {
|
|||
muted: true,
|
||||
actionGroupId: 'default',
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
};
|
||||
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||
alert: 'id',
|
||||
|
@ -363,6 +378,7 @@ describe('alertToListItem', () => {
|
|||
duration: 0,
|
||||
sortPriority: 1,
|
||||
isMuted: true,
|
||||
tracked: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -457,12 +473,14 @@ describe('tabbed content', () => {
|
|||
muted: false,
|
||||
actionGroupId: 'default',
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
second_rule: {
|
||||
status: 'Active',
|
||||
muted: false,
|
||||
actionGroupId: 'action group id unknown',
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -544,6 +562,7 @@ function mockRuleSummary(overloads: Partial<RuleSummary> = {}): RuleSummary {
|
|||
muted: false,
|
||||
actionGroupId: 'testActionGroup',
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
},
|
||||
executionDuration: {
|
||||
|
|
|
@ -190,6 +190,7 @@ export function alertToListItem(
|
|||
const start = alert?.activeStartDate ? new Date(alert.activeStartDate) : undefined;
|
||||
const duration = start ? durationEpoch - start.valueOf() : 0;
|
||||
const sortPriority = getSortPriorityByStatus(alert?.status);
|
||||
const tracked = !!alert?.tracked;
|
||||
return {
|
||||
alert: alertId,
|
||||
status,
|
||||
|
@ -198,6 +199,7 @@ export function alertToListItem(
|
|||
isMuted,
|
||||
sortPriority,
|
||||
flapping: alert.flapping,
|
||||
tracked,
|
||||
...(alert.maintenanceWindowIds ? { maintenanceWindowIds: alert.maintenanceWindowIds } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,7 +10,12 @@ import moment, { Duration } from 'moment';
|
|||
import { padStart, chunk } from 'lodash';
|
||||
import { EuiBasicTable, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AlertStatus, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
AlertStatus,
|
||||
ALERT_STATUS_ACTIVE,
|
||||
ALERT_STATUS_RECOVERED,
|
||||
ALERT_STATUS_UNTRACKED,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { AlertStatusValues, MaintenanceWindow } from '@kbn/alerting-plugin/common';
|
||||
import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants';
|
||||
import { Pagination } from '../../../../types';
|
||||
|
@ -20,7 +25,13 @@ import { AlertLifecycleStatusBadge } from '../../../components/alert_lifecycle_s
|
|||
import { useBulkGetMaintenanceWindows } from '../../alerts_table/hooks/use_bulk_get_maintenance_windows';
|
||||
import { MaintenanceWindowBaseCell } from '../../alerts_table/maintenance_windows/cell';
|
||||
|
||||
export const getConvertedAlertStatus = (status: AlertStatusValues): AlertStatus => {
|
||||
export const getConvertedAlertStatus = (
|
||||
status: AlertStatusValues,
|
||||
alert: AlertListItem
|
||||
): AlertStatus => {
|
||||
if (!alert.tracked) {
|
||||
return ALERT_STATUS_UNTRACKED;
|
||||
}
|
||||
if (status === 'Active') {
|
||||
return ALERT_STATUS_ACTIVE;
|
||||
}
|
||||
|
@ -151,7 +162,7 @@ export const RuleAlertList = (props: RuleAlertListProps) => {
|
|||
),
|
||||
width: '15%',
|
||||
render: (value: AlertStatusValues, alert: AlertListItem) => {
|
||||
const convertedStatus = getConvertedAlertStatus(value);
|
||||
const convertedStatus = getConvertedAlertStatus(value, alert);
|
||||
return (
|
||||
<AlertLifecycleStatusBadge alertStatus={convertedStatus} flapping={alert.flapping} />
|
||||
);
|
||||
|
|
|
@ -170,6 +170,7 @@ function mockRuleSummary(overloads: Partial<any> = {}): any {
|
|||
status: 'OK',
|
||||
muted: false,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
},
|
||||
executionDuration: {
|
||||
|
|
|
@ -104,6 +104,7 @@ export function mockRuleSummary(overloads: Partial<RuleSummary> = {}): RuleSumma
|
|||
muted: false,
|
||||
actionGroupId: 'testActionGroup',
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
},
|
||||
executionDuration: {
|
||||
|
|
|
@ -15,6 +15,7 @@ export interface AlertListItem {
|
|||
sortPriority: number;
|
||||
flapping: boolean;
|
||||
maintenanceWindowIds?: string[];
|
||||
tracked: boolean;
|
||||
}
|
||||
|
||||
export interface RefreshToken {
|
||||
|
|
|
@ -95,7 +95,7 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex
|
|||
});
|
||||
});
|
||||
|
||||
it('should create recovered-instance events for all alerts', async () => {
|
||||
it('should create untracked-instance events for all alerts', async () => {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -138,7 +138,7 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex
|
|||
provider: 'alerting',
|
||||
actions: new Map([
|
||||
// make sure the counts of the # of events per type are as expected
|
||||
['recovered-instance', { equal: 2 }],
|
||||
['untracked-instance', { equal: 2 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
@ -151,7 +151,7 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex
|
|||
savedObjects: [
|
||||
{ type: 'alert', id: ruleId, rel: 'primary', type_id: 'test.cumulative-firing' },
|
||||
],
|
||||
message: "instance 'instance-0' has recovered due to the rule was disabled",
|
||||
message: "instance 'instance-0' has been untracked because the rule was disabled",
|
||||
shouldHaveEventEnd: false,
|
||||
shouldHaveTask: false,
|
||||
ruleTypeId: createdRule.rule_type_id,
|
||||
|
|
|
@ -24,6 +24,7 @@ const InstanceActions = new Set<string | undefined>([
|
|||
'new-instance',
|
||||
'active-instance',
|
||||
'recovered-instance',
|
||||
'untracked-instance',
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -183,6 +183,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo
|
|||
status: 'OK',
|
||||
muted: true,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -248,11 +249,13 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo
|
|||
actionGroupId: 'default',
|
||||
activeStartDate: actualAlerts.alertA.activeStartDate,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
alertB: {
|
||||
status: 'OK',
|
||||
muted: false,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
alertC: {
|
||||
status: 'Active',
|
||||
|
@ -260,11 +263,13 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo
|
|||
actionGroupId: 'default',
|
||||
activeStartDate: actualAlerts.alertC.activeStartDate,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
alertD: {
|
||||
status: 'OK',
|
||||
muted: true,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
};
|
||||
expect(actualAlerts).to.eql(expectedAlerts);
|
||||
|
@ -332,12 +337,14 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo
|
|||
actionGroupId: 'default',
|
||||
activeStartDate: actualAlerts.alertA.activeStartDate,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
maintenanceWindowIds: [createdMaintenanceWindow.id],
|
||||
},
|
||||
alertB: {
|
||||
status: 'OK',
|
||||
muted: false,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
maintenanceWindowIds: [createdMaintenanceWindow.id],
|
||||
},
|
||||
alertC: {
|
||||
|
@ -346,12 +353,14 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo
|
|||
actionGroupId: 'default',
|
||||
activeStartDate: actualAlerts.alertC.activeStartDate,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
maintenanceWindowIds: [createdMaintenanceWindow.id],
|
||||
},
|
||||
alertD: {
|
||||
status: 'OK',
|
||||
muted: true,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
};
|
||||
expect(actualAlerts).to.eql(expectedAlerts);
|
||||
|
@ -398,11 +407,13 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo
|
|||
actionGroupId: 'default',
|
||||
activeStartDate: actualAlerts.alertA.activeStartDate,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
alertB: {
|
||||
status: 'OK',
|
||||
muted: false,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
alertC: {
|
||||
status: 'Active',
|
||||
|
@ -410,11 +421,13 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo
|
|||
actionGroupId: 'default',
|
||||
activeStartDate: actualAlerts.alertC.activeStartDate,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
alertD: {
|
||||
status: 'OK',
|
||||
muted: true,
|
||||
flapping: false,
|
||||
tracked: true,
|
||||
},
|
||||
};
|
||||
expect(actualAlerts).to.eql(expectedAlerts);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue