[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:
Zacqary Adam Xeper 2023-09-27 17:28:03 -05:00 committed by GitHub
parent 4fc3a43f79
commit 107239c333
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 453 additions and 58 deletions

View file

@ -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>
);

View file

@ -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;

View file

@ -40,4 +40,5 @@ export interface AlertStatus {
activeStartDate?: string;
flapping: boolean;
maintenanceWindowIds?: string[];
tracked: boolean;
}

View file

@ -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'
);
});
});
});
}
});

View file

@ -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}'
}`;

View file

@ -232,4 +232,8 @@ export class LegacyAlertsClient<
}
public async persistAlerts() {}
public async setAlertStatusToUntracked() {
return;
}
}

View file

@ -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,

View file

@ -56,6 +56,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
maxScheduledPerMinute: 1000,
internalSavedObjectsRepository,
};

View file

@ -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) => ({

View file

@ -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();

View file

@ -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(() => {

View file

@ -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 = (

View file

@ -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",
},
},

View file

@ -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;

View file

@ -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({

View file

@ -52,6 +52,8 @@ const rulesClientParams: jest.Mocked<RulesClientContext> = {
fieldsToExcludeFromPublicApi: [],
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
const username = 'test';

View file

@ -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';

View file

@ -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}`
);
}
});

View file

@ -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

View file

@ -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)
);
}
}
}

View file

@ -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"
);
});
});

View file

@ -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(() => {

View file

@ -69,6 +69,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
eventLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
describe('clearExpiredSnoozes()', () => {

View file

@ -71,6 +71,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -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();
});

View file

@ -70,6 +70,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
setGlobalDate();

View file

@ -63,6 +63,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -60,6 +60,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -59,6 +59,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -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",
},
},

View file

@ -60,6 +60,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -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>([

View file

@ -55,6 +55,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -60,6 +60,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -53,6 +53,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
setGlobalDate();

View file

@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -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(() => {

View file

@ -58,6 +58,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
getAlertIndicesAlias: jest.fn(),
alertsService: null,
};
beforeEach(() => {

View file

@ -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'>;

View file

@ -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:

View file

@ -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,
});
});

View file

@ -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;

View file

@ -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 =

View file

@ -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) {

View file

@ -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 {

View file

@ -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) {

View file

@ -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,
};

View file

@ -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]}
/>
),

View file

@ -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,
});
}

View file

@ -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',

View file

@ -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: {

View file

@ -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 } : {}),
};
}

View file

@ -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} />
);

View file

@ -170,6 +170,7 @@ function mockRuleSummary(overloads: Partial<any> = {}): any {
status: 'OK',
muted: false,
flapping: false,
tracked: true,
},
},
executionDuration: {

View file

@ -104,6 +104,7 @@ export function mockRuleSummary(overloads: Partial<RuleSummary> = {}): RuleSumma
muted: false,
actionGroupId: 'testActionGroup',
flapping: false,
tracked: true,
},
},
executionDuration: {

View file

@ -15,6 +15,7 @@ export interface AlertListItem {
sortPriority: number;
flapping: boolean;
maintenanceWindowIds?: string[];
tracked: boolean;
}
export interface RefreshToken {

View file

@ -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,

View file

@ -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

View file

@ -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);