mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[ResponseOps] move alert UUID generation from rule registry to the alerting framework (#143489)
resolves https://github.com/elastic/kibana/issues/142874 The alerting framework now generates an alert UUID for every alert it creates. The UUID will be reused for alerts which continue to be active on subsequent runs, until the alert recovers. When the same alert (alert instance id) becomes active again, a new UUID will be generated. These UUIDs then identify a "span" of events for a single alert. The rule registry plugin was already adding these UUIDs to it's own alerts-as-data indices, and that code has now been changed to make use of the new UUID the alerting framework generates. - adds property in the rule task state `alertInstances[alertInstanceId].meta.uuid`; this is where the alert UUID is persisted across runs - adds a new `Alert` method getUuid(): string` that can be used by rule executors to obtain the UUID of the alert they just retrieved from the factory; the rule registry uses this to get the UUID generated by the alerting framework - for the event log, adds the property `kibana.alert.uuid` to `*-instance` event log events; this is the same field the rule registry writes into the alerts-as-data indices - various changes to tests to accommodate new UUID data / methods - migrates the UUID previous stored with lifecycle alerts in the alert state, via the rule registry *INTO* the new `meta.uuid` field in the existing alert state.
This commit is contained in:
parent
ab30f3f123
commit
cd727fa190
72 changed files with 1792 additions and 246 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -18,6 +18,7 @@ x-pack/test/alerting_api_integration/common/plugins/alerts @elastic/response-ops
|
|||
x-pack/examples/alerting_example @elastic/response-ops
|
||||
x-pack/test/functional_with_es_ssl/plugins/alerts @elastic/response-ops
|
||||
x-pack/plugins/alerting @elastic/response-ops
|
||||
x-pack/packages/kbn-alerting-state-types @elastic/response-ops
|
||||
packages/kbn-alerts @elastic/security-solution
|
||||
packages/kbn-alerts-as-data-utils @elastic/response-ops
|
||||
x-pack/test/alerting_api_integration/common/plugins/alerts_restricted @elastic/response-ops
|
||||
|
|
|
@ -100,6 +100,7 @@ If the rule's action frequency is not a summary of alerts, it passes the followi
|
|||
`alert.actionSubgroup`:: The action subgroup of the alert that scheduled the action.
|
||||
`alert.flapping`:: A flag on the alert that indicates whether the alert status is changing repeatedly.
|
||||
`alert.id`:: The ID of the alert that scheduled the action.
|
||||
`alert.uuid`:: A universally unique identifier for the alert. While the alert is active, the UUID value remains unchanged each time the rule runs. preview:[]
|
||||
|
||||
[float]
|
||||
[[defining-rules-actions-variable-context]]
|
||||
|
|
|
@ -133,6 +133,7 @@
|
|||
"@kbn/alerting-example-plugin": "link:x-pack/examples/alerting_example",
|
||||
"@kbn/alerting-fixture-plugin": "link:x-pack/test/functional_with_es_ssl/plugins/alerts",
|
||||
"@kbn/alerting-plugin": "link:x-pack/plugins/alerting",
|
||||
"@kbn/alerting-state-types": "link:x-pack/packages/kbn-alerting-state-types",
|
||||
"@kbn/alerts": "link:packages/kbn-alerts",
|
||||
"@kbn/alerts-as-data-utils": "link:packages/kbn-alerts-as-data-utils",
|
||||
"@kbn/alerts-restricted-fixtures-plugin": "link:x-pack/test/alerting_api_integration/common/plugins/alerts_restricted",
|
||||
|
|
|
@ -141,7 +141,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"synthetics-param": "9776c9b571d35f0d0397e8915e035ea1dc026db7",
|
||||
"synthetics-privates-locations": "7d032fc788905e32152029ae7ab3d6038c48ae44",
|
||||
"tag": "87f21f07df9cc37001b15a26e413c18f50d1fbfe",
|
||||
"task": "ebcc113df12f14bf627dbd335ba78507187b48a3",
|
||||
"task": "ff760534a44c4cfabcf4baf8cfe8283f717cab02",
|
||||
"telemetry": "561b329aaed3c15b91aaf2075645be3097247612",
|
||||
"ui-metric": "410a8ad28e0f44b161c960ff0ce950c712b17c52",
|
||||
"upgrade-assistant-ml-upgrade-operation": "d8816e5ce32649e7a3a43e2c406c632319ff84bb",
|
||||
|
|
|
@ -30,6 +30,8 @@
|
|||
"@kbn/alerting-fixture-plugin/*": ["x-pack/test/functional_with_es_ssl/plugins/alerts/*"],
|
||||
"@kbn/alerting-plugin": ["x-pack/plugins/alerting"],
|
||||
"@kbn/alerting-plugin/*": ["x-pack/plugins/alerting/*"],
|
||||
"@kbn/alerting-state-types": ["x-pack/packages/kbn-alerting-state-types"],
|
||||
"@kbn/alerting-state-types/*": ["x-pack/packages/kbn-alerting-state-types/*"],
|
||||
"@kbn/alerts": ["packages/kbn-alerts"],
|
||||
"@kbn/alerts/*": ["packages/kbn-alerts/*"],
|
||||
"@kbn/alerts-as-data-utils": ["packages/kbn-alerts-as-data-utils"],
|
||||
|
|
8
x-pack/packages/kbn-alerting-state-types/README.md
Normal file
8
x-pack/packages/kbn-alerting-state-types/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# @kbn/alerting-state-types
|
||||
|
||||
Contains type information for the alerting data persisted in task
|
||||
manager documents as state.
|
||||
|
||||
Because task manager migrations sometimes need this data, it needs
|
||||
to be in a package outside of alerting.
|
||||
|
24
x-pack/packages/kbn-alerting-state-types/index.ts
Normal file
24
x-pack/packages/kbn-alerting-state-types/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export type {
|
||||
ThrottledActions,
|
||||
LastScheduledActions,
|
||||
AlertInstanceMeta,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
RawAlertInstance,
|
||||
} from './src/alert_instance';
|
||||
export { rawAlertInstance } from './src/alert_instance';
|
||||
|
||||
export { DateFromString } from './src/date_from_string';
|
||||
|
||||
export type { TrackedLifecycleAlertState, WrappedLifecycleRuleState } from './src/lifecycle_state';
|
||||
export { wrappedStateRt } from './src/lifecycle_state';
|
||||
|
||||
export type { RuleTaskState, RuleTaskParams } from './src/rule_task_instance';
|
||||
export { ActionsCompletion, ruleStateSchema, ruleParamsSchema } from './src/rule_task_instance';
|
12
x-pack/packages/kbn-alerting-state-types/jest.config.js
Normal file
12
x-pack/packages/kbn-alerting-state-types/jest.config.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/x-pack/packages/kbn-alerting-state-types'],
|
||||
};
|
5
x-pack/packages/kbn-alerting-state-types/kibana.jsonc
Normal file
5
x-pack/packages/kbn-alerting-state-types/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/alerting-state-types",
|
||||
"owner": "@elastic/response-ops"
|
||||
}
|
6
x-pack/packages/kbn-alerting-state-types/package.json
Normal file
6
x-pack/packages/kbn-alerting-state-types/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/alerting-state-types",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0"
|
||||
}
|
|
@ -37,6 +37,7 @@ const metaSchema = t.partial({
|
|||
// flapping flag that indicates whether the alert is flapping
|
||||
flapping: t.boolean,
|
||||
pendingRecoveredCount: t.number,
|
||||
uuid: t.string,
|
||||
});
|
||||
export type AlertInstanceMeta = t.TypeOf<typeof metaSchema>;
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
const trackedAlertStateRt = t.type({
|
||||
alertId: t.string,
|
||||
alertUuid: t.string,
|
||||
started: t.string,
|
||||
// an array used to track changes in alert state, the order is based on the rule executions
|
||||
// true - alert has changed from active/recovered
|
||||
// false - alert is new or the status has remained either active or recovered
|
||||
flappingHistory: t.array(t.boolean),
|
||||
// flapping flag that indicates whether the alert is flapping
|
||||
flapping: t.boolean,
|
||||
pendingRecoveredCount: t.number,
|
||||
});
|
||||
|
||||
export type TrackedLifecycleAlertState = t.TypeOf<typeof trackedAlertStateRt>;
|
||||
|
||||
type RuleTypeState = Record<string, unknown>;
|
||||
|
||||
export const alertTypeStateRt = <State extends RuleTypeState>() =>
|
||||
t.record(t.string, t.unknown) as t.Type<State, State, unknown>;
|
||||
|
||||
export const wrappedStateRt = <State extends RuleTypeState>() =>
|
||||
t.type({
|
||||
wrapped: alertTypeStateRt<State>(),
|
||||
// tracks the active alerts
|
||||
trackedAlerts: t.record(t.string, trackedAlertStateRt),
|
||||
// tracks the recovered alerts
|
||||
trackedAlertsRecovered: t.record(t.string, trackedAlertStateRt),
|
||||
});
|
||||
|
||||
/**
|
||||
* This is redefined instead of derived from above `wrappedStateRt` because
|
||||
* there's no easy way to instantiate generic values such as the runtime type
|
||||
* factory function.
|
||||
*/
|
||||
export type WrappedLifecycleRuleState<State extends RuleTypeState> = RuleTypeState & {
|
||||
wrapped: State;
|
||||
trackedAlerts: Record<string, TrackedLifecycleAlertState>;
|
||||
trackedAlertsRecovered: Record<string, TrackedLifecycleAlertState>;
|
||||
};
|
17
x-pack/packages/kbn-alerting-state-types/tsconfig.json
Normal file
17
x-pack/packages/kbn-alerting-state-types/tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": []
|
||||
}
|
|
@ -766,6 +766,7 @@ This factory returns an instance of `Alert`. The `Alert` class has the following
|
|||
|
||||
|Method|Description|
|
||||
|---|---|
|
||||
|getUuid()|Get the UUID of the alert.|
|
||||
|getState()|Get the current state of the alert.|
|
||||
|scheduleActions(actionGroup, context)|Call this to schedule the execution of actions. The actionGroup is a string `id` that relates to the group of alert `actions` to execute and the context will be used for templating purposes. `scheduleActions` should only be called once per alert.|
|
||||
|replaceState(state)|Used to replace the current state of the alert. This doesn't work like React, the entire state must be provided. Use this feature as you see fit. The state that is set will persist between rule executions whenever you re-create an alert with the same id. The alert state will be erased when `scheduleActions`isn't called during an execution.|
|
||||
|
@ -790,6 +791,8 @@ When an alert executes, the first argument is the `group` of actions to execute
|
|||
|
||||
The templating engine is [mustache]. General definition for the [mustache variable] is a double-brace {{}}. All variables are HTML-escaped by default and if there is a requirement to render unescaped HTML, it should be applied with the triple mustache: `{{{name}}}`. Also, `&` can be used to unescape a variable.
|
||||
|
||||
The complete list of variables available has grown, and difficult to keep in synch here as well, so refer to the published documentation for the variables available: https://www.elastic.co/guide/en/kibana/master/rule-action-variables.html
|
||||
|
||||
### Examples
|
||||
|
||||
The following code would be within a rule type. As you can see `cpuUsage` will replace the state of the alert and `server` is the context for the alert to execute. The difference between the two is that `cpuUsage` will be accessible at the next execution.
|
||||
|
|
|
@ -32,6 +32,7 @@ export interface AlertSummary {
|
|||
}
|
||||
|
||||
export interface AlertStatus {
|
||||
uuid?: string;
|
||||
status: AlertStatusValues;
|
||||
muted: boolean;
|
||||
actionGroupId?: string;
|
||||
|
|
|
@ -13,8 +13,26 @@ import { AlertsHealth } from './rule';
|
|||
export * from './rule';
|
||||
export * from './rules_settings';
|
||||
export * from './rule_type';
|
||||
export * from './rule_task_instance';
|
||||
export * from './alert_instance';
|
||||
export type {
|
||||
ThrottledActions,
|
||||
LastScheduledActions,
|
||||
AlertInstanceMeta,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
RawAlertInstance,
|
||||
TrackedLifecycleAlertState,
|
||||
WrappedLifecycleRuleState,
|
||||
RuleTaskState,
|
||||
RuleTaskParams,
|
||||
} from '@kbn/alerting-state-types';
|
||||
export {
|
||||
rawAlertInstance,
|
||||
DateFromString,
|
||||
wrappedStateRt,
|
||||
ActionsCompletion,
|
||||
ruleStateSchema,
|
||||
ruleParamsSchema,
|
||||
} from '@kbn/alerting-state-types';
|
||||
export * from './alert_summary';
|
||||
export * from './builtin_action_groups';
|
||||
export * from './bulk_edit';
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import sinon from 'sinon';
|
||||
import { Alert } from './alert';
|
||||
import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common';
|
||||
import { alertWithAnyUUID } from '../test_utils';
|
||||
|
||||
let clock: sinon.SinonFakeTimers;
|
||||
|
||||
|
@ -231,6 +232,23 @@ describe('getState()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getUUID()', () => {
|
||||
test('returns a UUID for a new alert', () => {
|
||||
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1');
|
||||
const uuid = alert.getUuid();
|
||||
expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
||||
});
|
||||
|
||||
test('returns same uuid from previous run of alert', () => {
|
||||
const uuid = 'previous-uuid';
|
||||
const meta = { uuid };
|
||||
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1', {
|
||||
meta,
|
||||
});
|
||||
expect(alert.getUuid()).toEqual(uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scheduleActions()', () => {
|
||||
test('makes hasScheduledActions() return true', () => {
|
||||
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1', {
|
||||
|
@ -320,6 +338,7 @@ describe('updateLastScheduledActions()', () => {
|
|||
expect(alert.toJSON()).toEqual({
|
||||
state: {},
|
||||
meta: {
|
||||
uuid: expect.any(String),
|
||||
lastScheduledActions: {
|
||||
date: new Date().toISOString(),
|
||||
group: 'default',
|
||||
|
@ -338,6 +357,7 @@ describe('updateLastScheduledActions()', () => {
|
|||
state: {},
|
||||
meta: {
|
||||
flappingHistory: [],
|
||||
uuid: expect.any(String),
|
||||
lastScheduledActions: {
|
||||
date: new Date().toISOString(),
|
||||
group: 'default',
|
||||
|
@ -367,6 +387,7 @@ describe('updateLastScheduledActions()', () => {
|
|||
state: {},
|
||||
meta: {
|
||||
flappingHistory: [],
|
||||
uuid: expect.any(String),
|
||||
lastScheduledActions: {
|
||||
date: new Date().toISOString(),
|
||||
group: 'default',
|
||||
|
@ -468,9 +489,22 @@ describe('toJSON', () => {
|
|||
},
|
||||
}
|
||||
);
|
||||
expect(JSON.stringify(alertInstance)).toEqual(
|
||||
'{"state":{"foo":true},"meta":{"lastScheduledActions":{"date":"1970-01-01T00:00:00.000Z","group":"default"},"flappingHistory":[false,true],"flapping":false,"pendingRecoveredCount":2}}'
|
||||
);
|
||||
|
||||
expect(alertInstance).toMatchObject({
|
||||
state: {
|
||||
foo: true,
|
||||
},
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: expect.any(Date),
|
||||
group: 'default',
|
||||
},
|
||||
uuid: expect.any(String),
|
||||
flappingHistory: [false, true],
|
||||
flapping: false,
|
||||
pendingRecoveredCount: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -514,6 +548,7 @@ describe('toRaw', () => {
|
|||
meta: {
|
||||
flappingHistory: [false, true, true],
|
||||
flapping: false,
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -529,12 +564,13 @@ describe('setFlappingHistory', () => {
|
|||
);
|
||||
alertInstance.setFlappingHistory([false]);
|
||||
expect(alertInstance.getFlappingHistory()).toEqual([false]);
|
||||
expect(alertInstance.toRaw()).toMatchInlineSnapshot(`
|
||||
expect(alertWithAnyUUID(alertInstance.toRaw())).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"meta": Object {
|
||||
"flappingHistory": Array [
|
||||
false,
|
||||
],
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
}
|
||||
|
@ -561,11 +597,12 @@ describe('setFlapping', () => {
|
|||
);
|
||||
alertInstance.setFlapping(false);
|
||||
expect(alertInstance.getFlapping()).toEqual(false);
|
||||
expect(alertInstance.toRaw()).toMatchInlineSnapshot(`
|
||||
expect(alertWithAnyUUID(alertInstance.toRaw())).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"meta": Object {
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
import { ALERT_INSTANCE_ID } from '@kbn/rule-data-utils';
|
||||
import { CombinedSummarizedAlerts } from '../types';
|
||||
|
@ -36,7 +37,13 @@ export type PublicAlert<
|
|||
ActionGroupIds extends string = DefaultActionGroupId
|
||||
> = Pick<
|
||||
Alert<State, Context, ActionGroupIds>,
|
||||
'getState' | 'replaceState' | 'scheduleActions' | 'setContext' | 'getContext' | 'hasContext'
|
||||
| 'getContext'
|
||||
| 'getState'
|
||||
| 'getUuid'
|
||||
| 'hasContext'
|
||||
| 'replaceState'
|
||||
| 'scheduleActions'
|
||||
| 'setContext'
|
||||
>;
|
||||
|
||||
export class Alert<
|
||||
|
@ -55,6 +62,7 @@ export class Alert<
|
|||
this.state = (state || {}) as State;
|
||||
this.context = {} as Context;
|
||||
this.meta = meta;
|
||||
this.meta.uuid = meta.uuid ?? uuidV4();
|
||||
|
||||
if (!this.meta.flappingHistory) {
|
||||
this.meta.flappingHistory = [];
|
||||
|
@ -65,6 +73,10 @@ export class Alert<
|
|||
return this.id;
|
||||
}
|
||||
|
||||
getUuid() {
|
||||
return this.meta.uuid!;
|
||||
}
|
||||
|
||||
hasScheduledActions() {
|
||||
return this.scheduledExecutionOptions !== undefined;
|
||||
}
|
||||
|
@ -214,11 +226,12 @@ export class Alert<
|
|||
toRaw(recovered: boolean = false): RawAlertInstance {
|
||||
return recovered
|
||||
? {
|
||||
// for a recovered alert, we only care to track the flappingHistory
|
||||
// and the flapping flag
|
||||
// for a recovered alert, we only care to track the flappingHistory,
|
||||
// the flapping flag, and the UUID
|
||||
meta: {
|
||||
flappingHistory: this.meta.flappingHistory,
|
||||
flapping: this.meta.flapping,
|
||||
uuid: this.meta.uuid,
|
||||
},
|
||||
}
|
||||
: {
|
||||
|
|
|
@ -33,14 +33,15 @@ describe('createAlertFactory()', () => {
|
|||
autoRecoverAlerts: true,
|
||||
});
|
||||
const result = alertFactory.create('1');
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"meta": Object {
|
||||
"flappingHistory": Array [],
|
||||
},
|
||||
"state": Object {},
|
||||
}
|
||||
`);
|
||||
expect(result).toMatchObject({
|
||||
meta: {
|
||||
uuid: expect.any(String),
|
||||
flappingHistory: [],
|
||||
},
|
||||
state: {},
|
||||
context: {},
|
||||
id: '1',
|
||||
});
|
||||
// @ts-expect-error
|
||||
expect(result.getId()).toEqual('1');
|
||||
});
|
||||
|
@ -48,7 +49,7 @@ describe('createAlertFactory()', () => {
|
|||
test('reuses existing alerts', () => {
|
||||
const alert = new Alert('1', {
|
||||
state: { foo: true },
|
||||
meta: { lastScheduledActions: { group: 'default', date: new Date() } },
|
||||
meta: { lastScheduledActions: { group: 'default', date: new Date() }, uuid: 'uuid-previous' },
|
||||
});
|
||||
const alertFactory = createAlertFactory({
|
||||
alerts: {
|
||||
|
@ -59,20 +60,19 @@ describe('createAlertFactory()', () => {
|
|||
autoRecoverAlerts: true,
|
||||
});
|
||||
const result = alertFactory.create('1');
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"meta": Object {
|
||||
"flappingHistory": Array [],
|
||||
"lastScheduledActions": Object {
|
||||
"date": "1970-01-01T00:00:00.000Z",
|
||||
"group": "default",
|
||||
},
|
||||
expect(result).toMatchObject({
|
||||
meta: {
|
||||
uuid: 'uuid-previous',
|
||||
flappingHistory: [],
|
||||
lastScheduledActions: {
|
||||
date: expect.any(Date),
|
||||
group: 'default',
|
||||
},
|
||||
"state": Object {
|
||||
"foo": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
},
|
||||
state: { foo: true },
|
||||
context: {},
|
||||
id: '1',
|
||||
});
|
||||
});
|
||||
|
||||
test('mutates given alerts', () => {
|
||||
|
@ -84,16 +84,17 @@ describe('createAlertFactory()', () => {
|
|||
autoRecoverAlerts: true,
|
||||
});
|
||||
alertFactory.create('1');
|
||||
expect(alerts).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"flappingHistory": Array [],
|
||||
},
|
||||
"state": Object {},
|
||||
expect(alerts).toMatchObject({
|
||||
1: {
|
||||
meta: {
|
||||
uuid: expect.any(String),
|
||||
flappingHistory: [],
|
||||
},
|
||||
}
|
||||
`);
|
||||
state: {},
|
||||
context: {},
|
||||
id: '1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('throws error and sets flag when more alerts are created than allowed', () => {
|
||||
|
@ -124,9 +125,10 @@ describe('createAlertFactory()', () => {
|
|||
autoRecoverAlerts: true,
|
||||
});
|
||||
const result = alertFactory.create('1');
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
meta: {
|
||||
flappingHistory: [],
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
state: {},
|
||||
context: {},
|
||||
|
@ -166,9 +168,10 @@ describe('createAlertFactory()', () => {
|
|||
autoRecoverAlerts: true,
|
||||
});
|
||||
const result = alertFactory.create('1');
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
meta: {
|
||||
flappingHistory: [],
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
state: {},
|
||||
context: {},
|
||||
|
@ -193,9 +196,10 @@ describe('createAlertFactory()', () => {
|
|||
autoRecoverAlerts: true,
|
||||
});
|
||||
const result = alertFactory.create('1');
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
meta: {
|
||||
flappingHistory: [],
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
state: {},
|
||||
context: {},
|
||||
|
@ -219,9 +223,10 @@ describe('createAlertFactory()', () => {
|
|||
autoRecoverAlerts: true,
|
||||
});
|
||||
const result = alertFactory.create('1');
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
meta: {
|
||||
flappingHistory: [],
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
state: {},
|
||||
context: {},
|
||||
|
@ -244,9 +249,10 @@ describe('createAlertFactory()', () => {
|
|||
autoRecoverAlerts: true,
|
||||
});
|
||||
const result = alertFactory.create('1');
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
meta: {
|
||||
flappingHistory: [],
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
state: {},
|
||||
context: {},
|
||||
|
@ -323,6 +329,7 @@ describe('createAlertFactory()', () => {
|
|||
expect(result).toEqual({
|
||||
meta: {
|
||||
flappingHistory: [],
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
state: {},
|
||||
context: {},
|
||||
|
|
|
@ -125,6 +125,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
"uuid": undefined,
|
||||
},
|
||||
"alert-2": Object {
|
||||
"actionGroupId": undefined,
|
||||
|
@ -132,6 +133,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
"uuid": undefined,
|
||||
},
|
||||
},
|
||||
"lastRun": undefined,
|
||||
|
@ -211,11 +213,11 @@ describe('alertSummaryFromEventLog', () => {
|
|||
const eventsFactory = new EventsFactory();
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addNewAlert('alert-1')
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addNewAlert('alert-1', 'uuid-1')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addRecoveredAlert('alert-1')
|
||||
.addRecoveredAlert('alert-1', 'uuid-1')
|
||||
.getEvents();
|
||||
const executionEvents = eventsFactory.getEvents();
|
||||
|
||||
|
@ -237,6 +239,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
"lastRun": "2020-06-18T00:00:10.000Z",
|
||||
|
@ -252,11 +255,11 @@ describe('alertSummaryFromEventLog', () => {
|
|||
const eventsFactory = new EventsFactory();
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addNewAlert('alert-1')
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addNewAlert('alert-1', 'uuid-1')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addLegacyResolvedAlert('alert-1')
|
||||
.addLegacyResolvedAlert('alert-1', 'uuid-1')
|
||||
.getEvents();
|
||||
const executionEvents = eventsFactory.getEvents();
|
||||
|
||||
|
@ -278,6 +281,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
"lastRun": "2020-06-18T00:00:10.000Z",
|
||||
|
@ -293,10 +297,10 @@ describe('alertSummaryFromEventLog', () => {
|
|||
const eventsFactory = new EventsFactory();
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addRecoveredAlert('alert-1')
|
||||
.addRecoveredAlert('alert-1', 'uuid-1')
|
||||
.getEvents();
|
||||
const executionEvents = eventsFactory.getEvents();
|
||||
|
||||
|
@ -318,6 +322,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
"lastRun": "2020-06-18T00:00:10.000Z",
|
||||
|
@ -333,11 +338,11 @@ describe('alertSummaryFromEventLog', () => {
|
|||
const eventsFactory = new EventsFactory();
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addNewAlert('alert-1')
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addNewAlert('alert-1', 'uuid-1')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.getEvents();
|
||||
const executionEvents = eventsFactory.getEvents();
|
||||
|
||||
|
@ -359,6 +364,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
"lastRun": "2020-06-18T00:00:10.000Z",
|
||||
|
@ -374,11 +380,11 @@ describe('alertSummaryFromEventLog', () => {
|
|||
const eventsFactory = new EventsFactory();
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addNewAlert('alert-1')
|
||||
.addActiveAlert('alert-1', undefined)
|
||||
.addNewAlert('alert-1', 'uuid-1')
|
||||
.addActiveAlert('alert-1', undefined, 'uuid-1')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', undefined)
|
||||
.addActiveAlert('alert-1', undefined, 'uuid-1')
|
||||
.getEvents();
|
||||
const executionEvents = eventsFactory.getEvents();
|
||||
|
||||
|
@ -400,6 +406,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
"lastRun": "2020-06-18T00:00:10.000Z",
|
||||
|
@ -415,11 +422,11 @@ describe('alertSummaryFromEventLog', () => {
|
|||
const eventsFactory = new EventsFactory();
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addNewAlert('alert-1')
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addNewAlert('alert-1', 'uuid-1')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', 'action group B')
|
||||
.addActiveAlert('alert-1', 'action group B', 'uuid-1')
|
||||
.getEvents();
|
||||
const executionEvents = eventsFactory.getEvents();
|
||||
|
||||
|
@ -441,6 +448,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
"lastRun": "2020-06-18T00:00:10.000Z",
|
||||
|
@ -456,10 +464,10 @@ describe('alertSummaryFromEventLog', () => {
|
|||
const eventsFactory = new EventsFactory();
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.getEvents();
|
||||
const executionEvents = eventsFactory.getEvents();
|
||||
|
||||
|
@ -480,6 +488,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
"lastRun": "2020-06-18T00:00:10.000Z",
|
||||
|
@ -495,14 +504,14 @@ describe('alertSummaryFromEventLog', () => {
|
|||
const eventsFactory = new EventsFactory();
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addNewAlert('alert-1')
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addNewAlert('alert-2')
|
||||
.addActiveAlert('alert-2', 'action group B')
|
||||
.addNewAlert('alert-1', 'uuid-1')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.addNewAlert('alert-2', 'uuid-2')
|
||||
.addActiveAlert('alert-2', 'action group B', 'uuid-2')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addRecoveredAlert('alert-2')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.addRecoveredAlert('alert-2', 'uuid-2')
|
||||
.getEvents();
|
||||
const executionEvents = eventsFactory.getEvents();
|
||||
|
||||
|
@ -523,6 +532,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": true,
|
||||
"status": "Active",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"alert-2": Object {
|
||||
"actionGroupId": undefined,
|
||||
|
@ -530,6 +540,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
},
|
||||
"lastRun": "2020-06-18T00:00:10.000Z",
|
||||
|
@ -545,20 +556,20 @@ describe('alertSummaryFromEventLog', () => {
|
|||
const eventsFactory = new EventsFactory();
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addNewAlert('alert-1')
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addNewAlert('alert-2')
|
||||
.addActiveAlert('alert-2', 'action group B')
|
||||
.addNewAlert('alert-1', 'uuid-1')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.addNewAlert('alert-2', 'uuid-2')
|
||||
.addActiveAlert('alert-2', 'action group B', 'uuid-2')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', 'action group A')
|
||||
.addRecoveredAlert('alert-2')
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1')
|
||||
.addRecoveredAlert('alert-2', 'uuid-2')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', 'action group B')
|
||||
.addActiveAlert('alert-1', 'action group B', 'uuid-1')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', 'action group B')
|
||||
.addActiveAlert('alert-1', 'action group B', 'uuid-1')
|
||||
.getEvents();
|
||||
const executionEvents = eventsFactory.getEvents();
|
||||
|
||||
|
@ -580,6 +591,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"alert-2": Object {
|
||||
"actionGroupId": undefined,
|
||||
|
@ -587,6 +599,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
},
|
||||
"lastRun": "2020-06-18T00:00:30.000Z",
|
||||
|
@ -602,7 +615,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
const eventsFactory = new EventsFactory();
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addActiveAlert('alert-1', 'action group A', true)
|
||||
.addActiveAlert('alert-1', 'action group A', 'uuid-1', true)
|
||||
.getEvents();
|
||||
|
||||
const executionEvents = eventsFactory.getEvents();
|
||||
|
@ -624,6 +637,7 @@ describe('alertSummaryFromEventLog', () => {
|
|||
"flapping": true,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
},
|
||||
"lastRun": "2020-06-18T00:00:00.000Z",
|
||||
|
@ -695,6 +709,7 @@ export class EventsFactory {
|
|||
addActiveAlert(
|
||||
alertId: string,
|
||||
actionGroupId: string | undefined,
|
||||
uuid: string,
|
||||
flapping = false
|
||||
): EventsFactory {
|
||||
const kibanaAlerting = actionGroupId
|
||||
|
@ -706,43 +721,43 @@ export class EventsFactory {
|
|||
provider: EVENT_LOG_PROVIDER,
|
||||
action: EVENT_LOG_ACTIONS.activeInstance,
|
||||
},
|
||||
kibana: { alerting: kibanaAlerting, alert: { flapping } },
|
||||
kibana: { alerting: kibanaAlerting, alert: { flapping, uuid } },
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
addNewAlert(alertId: string): EventsFactory {
|
||||
addNewAlert(alertId: string, uuid: string): EventsFactory {
|
||||
this.events.push({
|
||||
'@timestamp': this.date,
|
||||
event: {
|
||||
provider: EVENT_LOG_PROVIDER,
|
||||
action: EVENT_LOG_ACTIONS.newInstance,
|
||||
},
|
||||
kibana: { alerting: { instance_id: alertId } },
|
||||
kibana: { alerting: { instance_id: alertId }, alert: { uuid } },
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
addRecoveredAlert(alertId: string): EventsFactory {
|
||||
addRecoveredAlert(alertId: string, uuid: string): EventsFactory {
|
||||
this.events.push({
|
||||
'@timestamp': this.date,
|
||||
event: {
|
||||
provider: EVENT_LOG_PROVIDER,
|
||||
action: EVENT_LOG_ACTIONS.recoveredInstance,
|
||||
},
|
||||
kibana: { alerting: { instance_id: alertId } },
|
||||
kibana: { alerting: { instance_id: alertId }, alert: { uuid } },
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
addLegacyResolvedAlert(alertId: string): EventsFactory {
|
||||
addLegacyResolvedAlert(alertId: string, uuid: string): EventsFactory {
|
||||
this.events.push({
|
||||
'@timestamp': this.date,
|
||||
event: {
|
||||
provider: EVENT_LOG_PROVIDER,
|
||||
action: LEGACY_EVENT_LOG_ACTIONS.resolvedInstance,
|
||||
},
|
||||
kibana: { alerting: { instance_id: alertId } },
|
||||
kibana: { alerting: { instance_id: alertId }, alert: { uuid } },
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -79,7 +79,8 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams)
|
|||
const alertId = event?.kibana?.alerting?.instance_id;
|
||||
if (alertId === undefined) continue;
|
||||
|
||||
const status = getAlertStatus(alerts, alertId);
|
||||
const alertUuid = event?.kibana?.alert?.uuid;
|
||||
const status = getAlertStatus(alerts, alertId, alertUuid);
|
||||
|
||||
if (event?.kibana?.alert?.flapping) {
|
||||
status.flapping = true;
|
||||
|
@ -149,10 +150,15 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams)
|
|||
}
|
||||
|
||||
// return an alert status object, creating and adding to the map if needed
|
||||
function getAlertStatus(alerts: Map<string, AlertStatus>, alertId: string): AlertStatus {
|
||||
function getAlertStatus(
|
||||
alerts: Map<string, AlertStatus>,
|
||||
alertId: string,
|
||||
alertUuid?: string
|
||||
): AlertStatus {
|
||||
if (alerts.has(alertId)) return alerts.get(alertId)!;
|
||||
|
||||
const status: AlertStatus = {
|
||||
uuid: alertUuid,
|
||||
status: 'OK',
|
||||
muted: false,
|
||||
actionGroupId: undefined,
|
||||
|
|
|
@ -59,6 +59,7 @@ const contextWithName = { ...contextWithScheduleDelay, ruleName: 'my-super-cool-
|
|||
const alert = {
|
||||
action: EVENT_LOG_ACTIONS.activeInstance,
|
||||
id: 'aaabbb',
|
||||
uuid: 'u-u-i-d',
|
||||
message: `.test-rule-type:123: 'my rule' active alert: 'aaabbb' in actionGroup: 'aGroup';`,
|
||||
group: 'aGroup',
|
||||
state: {
|
||||
|
@ -1089,6 +1090,7 @@ describe('createAlertRecord', () => {
|
|||
expect(record.event?.provider).toBeUndefined();
|
||||
expect(record.event?.outcome).toBeUndefined();
|
||||
expect(record.kibana?.alert?.rule?.execution?.metrics).toBeUndefined();
|
||||
expect(record.kibana?.alert?.uuid).toBe(alert.uuid);
|
||||
expect(record.kibana?.server_uuid).toBeUndefined();
|
||||
expect(record.kibana?.task).toBeUndefined();
|
||||
expect(record.kibana?.version).toBeUndefined();
|
||||
|
|
|
@ -45,6 +45,7 @@ interface DoneOpts {
|
|||
interface AlertOpts {
|
||||
action: string;
|
||||
id: string;
|
||||
uuid: string;
|
||||
message: string;
|
||||
group?: string;
|
||||
state?: AlertInstanceState;
|
||||
|
@ -239,6 +240,7 @@ export function createAlertRecord(context: RuleContextOpts, alert: AlertOpts) {
|
|||
namespace: context.namespace,
|
||||
spaceId: context.spaceId,
|
||||
executionId: context.executionId,
|
||||
alertUuid: alert.uuid,
|
||||
action: alert.action,
|
||||
state: alert.state,
|
||||
instanceId: alert.id,
|
||||
|
|
|
@ -25,6 +25,7 @@ interface CreateAlertEventLogRecordParams {
|
|||
group?: string;
|
||||
namespace?: string;
|
||||
timestamp?: string;
|
||||
alertUuid?: string;
|
||||
task?: {
|
||||
scheduled?: string;
|
||||
scheduleDelay?: number;
|
||||
|
@ -57,6 +58,7 @@ export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecor
|
|||
consumer,
|
||||
spaceId,
|
||||
flapping,
|
||||
alertUuid,
|
||||
alertSummary,
|
||||
} = params;
|
||||
const alerting =
|
||||
|
@ -90,6 +92,7 @@ export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecor
|
|||
kibana: {
|
||||
alert: {
|
||||
...(flapping !== undefined ? { flapping } : {}),
|
||||
...(alertUuid ? { uuid: alertUuid } : {}),
|
||||
rule: {
|
||||
rule_type_id: ruleType.id,
|
||||
...(consumer ? { consumer } : {}),
|
||||
|
|
|
@ -8,12 +8,15 @@
|
|||
import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings';
|
||||
import { getAlertsForNotification } from '.';
|
||||
import { Alert } from '../alert';
|
||||
import { alertsWithAnyUUID } from '../test_utils';
|
||||
import { RuleNotifyWhen } from '../types';
|
||||
|
||||
describe('getAlertsForNotification', () => {
|
||||
test('should set pendingRecoveredCount to zero for all active alerts', () => {
|
||||
const alert1 = new Alert('1', { meta: { flapping: true, pendingRecoveredCount: 3 } });
|
||||
const alert2 = new Alert('2', { meta: { flapping: false } });
|
||||
const alert1 = new Alert('1', {
|
||||
meta: { flapping: true, pendingRecoveredCount: 3, uuid: 'uuid-1' },
|
||||
});
|
||||
const alert2 = new Alert('2', { meta: { flapping: false, uuid: 'uuid-2' } });
|
||||
|
||||
const { newAlerts, activeAlerts } = getAlertsForNotification(
|
||||
DEFAULT_FLAPPING_SETTINGS,
|
||||
|
@ -36,6 +39,7 @@ describe('getAlertsForNotification', () => {
|
|||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -48,6 +52,7 @@ describe('getAlertsForNotification', () => {
|
|||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -56,6 +61,7 @@ describe('getAlertsForNotification', () => {
|
|||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -92,14 +98,15 @@ describe('getAlertsForNotification', () => {
|
|||
}
|
||||
);
|
||||
|
||||
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
|
||||
expect(activeAlerts).toMatchInlineSnapshot(`
|
||||
expect(alertsWithAnyUUID(newAlerts)).toMatchInlineSnapshot(`Object {}`);
|
||||
expect(alertsWithAnyUUID(activeAlerts)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"3": Object {
|
||||
"meta": Object {
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"pendingRecoveredCount": 1,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -115,13 +122,14 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
]
|
||||
`);
|
||||
expect(currentActiveAlerts).toMatchInlineSnapshot(`
|
||||
expect(alertsWithAnyUUID(currentActiveAlerts)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"3": Object {
|
||||
"meta": Object {
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"pendingRecoveredCount": 1,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -137,13 +145,14 @@ describe('getAlertsForNotification', () => {
|
|||
},
|
||||
]
|
||||
`);
|
||||
expect(recoveredAlerts).toMatchInlineSnapshot(`
|
||||
expect(alertsWithAnyUUID(recoveredAlerts)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -151,18 +160,20 @@ describe('getAlertsForNotification', () => {
|
|||
"meta": Object {
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(currentRecoveredAlerts).toMatchInlineSnapshot(`
|
||||
expect(alertsWithAnyUUID(currentRecoveredAlerts)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -170,6 +181,7 @@ describe('getAlertsForNotification', () => {
|
|||
"meta": Object {
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -209,7 +221,7 @@ describe('getAlertsForNotification', () => {
|
|||
|
||||
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
|
||||
expect(activeAlerts).toMatchInlineSnapshot(`Object {}`);
|
||||
expect(recoveredAlerts).toMatchInlineSnapshot(`
|
||||
expect(alertsWithAnyUUID(recoveredAlerts)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
|
@ -220,6 +232,7 @@ describe('getAlertsForNotification', () => {
|
|||
true,
|
||||
],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -232,6 +245,7 @@ describe('getAlertsForNotification', () => {
|
|||
true,
|
||||
],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -244,12 +258,13 @@ describe('getAlertsForNotification', () => {
|
|||
true,
|
||||
],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(currentRecoveredAlerts).toMatchInlineSnapshot(`
|
||||
expect(alertsWithAnyUUID(currentRecoveredAlerts)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
|
@ -260,6 +275,7 @@ describe('getAlertsForNotification', () => {
|
|||
true,
|
||||
],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -272,6 +288,7 @@ describe('getAlertsForNotification', () => {
|
|||
true,
|
||||
],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -284,6 +301,7 @@ describe('getAlertsForNotification', () => {
|
|||
true,
|
||||
],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -321,13 +339,14 @@ describe('getAlertsForNotification', () => {
|
|||
);
|
||||
|
||||
expect(newAlerts).toMatchInlineSnapshot(`Object {}`);
|
||||
expect(activeAlerts).toMatchInlineSnapshot(`
|
||||
expect(alertsWithAnyUUID(activeAlerts)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"3": Object {
|
||||
"meta": Object {
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"pendingRecoveredCount": 1,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -347,13 +366,14 @@ describe('getAlertsForNotification', () => {
|
|||
expect(
|
||||
Object.values(currentActiveAlerts).map((a) => a.getScheduledActionOptions())
|
||||
).toMatchInlineSnapshot(`Array []`);
|
||||
expect(recoveredAlerts).toMatchInlineSnapshot(`
|
||||
expect(alertsWithAnyUUID(recoveredAlerts)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -361,18 +381,20 @@ describe('getAlertsForNotification', () => {
|
|||
"meta": Object {
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(currentRecoveredAlerts).toMatchInlineSnapshot(`
|
||||
expect(alertsWithAnyUUID(currentRecoveredAlerts)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"flapping": true,
|
||||
"flappingHistory": Array [],
|
||||
"pendingRecoveredCount": 0,
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -380,6 +402,7 @@ describe('getAlertsForNotification', () => {
|
|||
"meta": Object {
|
||||
"flapping": false,
|
||||
"flappingHistory": Array [],
|
||||
"uuid": Any<String>,
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
|
|
@ -719,7 +719,9 @@ describe('processAlerts', () => {
|
|||
|
||||
describe('updating flappingHistory', () => {
|
||||
test('if new alert, set flapping state to true', () => {
|
||||
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1');
|
||||
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { uuid: 'uuid-1' },
|
||||
});
|
||||
|
||||
const alerts = cloneDeep({ '1': activeAlert });
|
||||
alerts['1'].scheduleActions('default' as never, { foo: '1' });
|
||||
|
@ -741,6 +743,7 @@ describe('processAlerts', () => {
|
|||
"flappingHistory": Array [
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -756,6 +759,7 @@ describe('processAlerts', () => {
|
|||
"flappingHistory": Array [
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -769,7 +773,7 @@ describe('processAlerts', () => {
|
|||
|
||||
test('if alert is still active, set flapping state to false', () => {
|
||||
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-1' },
|
||||
});
|
||||
|
||||
const alerts = cloneDeep({ '1': activeAlert });
|
||||
|
@ -793,6 +797,7 @@ describe('processAlerts', () => {
|
|||
false,
|
||||
false,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -803,9 +808,11 @@ describe('processAlerts', () => {
|
|||
});
|
||||
|
||||
test('if alert is active and previously recovered, set flapping state to true', () => {
|
||||
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1');
|
||||
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { uuid: 'uuid-1' },
|
||||
});
|
||||
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-2' },
|
||||
});
|
||||
|
||||
const alerts = cloneDeep({ '1': activeAlert });
|
||||
|
@ -830,6 +837,7 @@ describe('processAlerts', () => {
|
|||
false,
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -846,6 +854,7 @@ describe('processAlerts', () => {
|
|||
false,
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -859,11 +868,11 @@ describe('processAlerts', () => {
|
|||
|
||||
test('if alert is recovered and previously active, set flapping state to true', () => {
|
||||
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-1' },
|
||||
});
|
||||
activeAlert.scheduleActions('default' as never, { foo: '1' });
|
||||
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-1' },
|
||||
});
|
||||
|
||||
const alerts = cloneDeep({ '1': recoveredAlert });
|
||||
|
@ -888,6 +897,7 @@ describe('processAlerts', () => {
|
|||
false,
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -897,7 +907,7 @@ describe('processAlerts', () => {
|
|||
|
||||
test('if alert is still recovered, set flapping state to false', () => {
|
||||
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-1' },
|
||||
});
|
||||
|
||||
const alerts = cloneDeep({ '1': recoveredAlert });
|
||||
|
@ -922,6 +932,7 @@ describe('processAlerts', () => {
|
|||
false,
|
||||
false,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -930,14 +941,16 @@ describe('processAlerts', () => {
|
|||
});
|
||||
|
||||
test('if setFlapping is false should not update flappingHistory', () => {
|
||||
const activeAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1');
|
||||
const activeAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { uuid: 'uuid-1' },
|
||||
});
|
||||
activeAlert1.scheduleActions('default' as never, { foo: '1' });
|
||||
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('2', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-2' },
|
||||
});
|
||||
activeAlert2.scheduleActions('default' as never, { foo: '1' });
|
||||
const recoveredAlert = new Alert<AlertInstanceState, AlertInstanceContext>('3', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-3' },
|
||||
});
|
||||
|
||||
const previouslyRecoveredAlerts = cloneDeep({ '3': recoveredAlert });
|
||||
|
@ -959,6 +972,7 @@ describe('processAlerts', () => {
|
|||
"1": Object {
|
||||
"meta": Object {
|
||||
"flappingHistory": Array [],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -970,6 +984,7 @@ describe('processAlerts', () => {
|
|||
"flappingHistory": Array [
|
||||
false,
|
||||
],
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -980,6 +995,7 @@ describe('processAlerts', () => {
|
|||
"1": Object {
|
||||
"meta": Object {
|
||||
"flappingHistory": Array [],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -995,6 +1011,7 @@ describe('processAlerts', () => {
|
|||
"flappingHistory": Array [
|
||||
false,
|
||||
],
|
||||
"uuid": "uuid-3",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -1005,7 +1022,7 @@ describe('processAlerts', () => {
|
|||
describe('when hasReachedAlertLimit is true', () => {
|
||||
test('if alert is still active, set flapping state to false', () => {
|
||||
const activeAlert = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-1' },
|
||||
});
|
||||
|
||||
const alerts = cloneDeep({ '1': activeAlert });
|
||||
|
@ -1029,6 +1046,7 @@ describe('processAlerts', () => {
|
|||
false,
|
||||
false,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
|
@ -1040,10 +1058,12 @@ describe('processAlerts', () => {
|
|||
|
||||
test('if new alert, set flapping state to true', () => {
|
||||
const activeAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-1' },
|
||||
});
|
||||
activeAlert1.scheduleActions('default' as never, { foo: '1' });
|
||||
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('1');
|
||||
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('2', {
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-2' },
|
||||
});
|
||||
activeAlert2.scheduleActions('default' as never, { foo: '1' });
|
||||
|
||||
const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 });
|
||||
|
@ -1066,14 +1086,17 @@ describe('processAlerts', () => {
|
|||
false,
|
||||
false,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
"2": Object {
|
||||
"meta": Object {
|
||||
"flappingHistory": Array [
|
||||
false,
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -1087,8 +1110,10 @@ describe('processAlerts', () => {
|
|||
"2": Object {
|
||||
"meta": Object {
|
||||
"flappingHistory": Array [
|
||||
false,
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -1102,10 +1127,12 @@ describe('processAlerts', () => {
|
|||
|
||||
test('if alert is active and previously recovered, set flapping state to true', () => {
|
||||
const activeAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-1' },
|
||||
});
|
||||
activeAlert1.scheduleActions('default' as never, { foo: '1' });
|
||||
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('1');
|
||||
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { uuid: 'uuid-2' },
|
||||
});
|
||||
activeAlert2.scheduleActions('default' as never, { foo: '1' });
|
||||
|
||||
const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 });
|
||||
|
@ -1128,6 +1155,7 @@ describe('processAlerts', () => {
|
|||
false,
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -1139,6 +1167,7 @@ describe('processAlerts', () => {
|
|||
"flappingHistory": Array [
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -1155,6 +1184,7 @@ describe('processAlerts', () => {
|
|||
false,
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -1166,6 +1196,7 @@ describe('processAlerts', () => {
|
|||
"flappingHistory": Array [
|
||||
true,
|
||||
],
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -1179,10 +1210,12 @@ describe('processAlerts', () => {
|
|||
|
||||
test('if setFlapping is false should not update flappingHistory', () => {
|
||||
const activeAlert1 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { flappingHistory: [false] },
|
||||
meta: { flappingHistory: [false], uuid: 'uuid-1' },
|
||||
});
|
||||
activeAlert1.scheduleActions('default' as never, { foo: '1' });
|
||||
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('1');
|
||||
const activeAlert2 = new Alert<AlertInstanceState, AlertInstanceContext>('1', {
|
||||
meta: { uuid: 'uuid-2' },
|
||||
});
|
||||
activeAlert2.scheduleActions('default' as never, { foo: '1' });
|
||||
|
||||
const alerts = cloneDeep({ '1': activeAlert1, '2': activeAlert2 });
|
||||
|
@ -1204,12 +1237,14 @@ describe('processAlerts', () => {
|
|||
"flappingHistory": Array [
|
||||
false,
|
||||
],
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"state": Object {},
|
||||
},
|
||||
"2": Object {
|
||||
"meta": Object {
|
||||
"flappingHistory": Array [],
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
@ -1223,6 +1258,7 @@ describe('processAlerts', () => {
|
|||
"2": Object {
|
||||
"meta": Object {
|
||||
"flappingHistory": Array [],
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
"state": Object {
|
||||
"duration": "0",
|
||||
|
|
|
@ -40,7 +40,11 @@ describe('trimRecoveredAlerts', () => {
|
|||
trimmedAlertsRecovered: { 1: alert1, 3: alert3 },
|
||||
earlyRecoveredAlerts: {
|
||||
2: new Alert('2', {
|
||||
meta: { flappingHistory: new Array(20).fill(false), flapping: false },
|
||||
meta: {
|
||||
flappingHistory: new Array(20).fill(false),
|
||||
flapping: false,
|
||||
uuid: expect.any(String),
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -80,6 +80,7 @@ export const createAlertFactoryMock = {
|
|||
getScheduledActionOptions: jest.fn(),
|
||||
unscheduleActions: jest.fn(),
|
||||
getState: jest.fn(),
|
||||
getUuid: jest.fn(),
|
||||
scheduleActions: jest.fn(),
|
||||
replaceState: jest.fn(),
|
||||
updateLastScheduledActions: jest.fn(),
|
||||
|
|
|
@ -38,6 +38,7 @@ export const recoverRuleAlerts = async (
|
|||
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 event = createAlertEventLogRecordObject({
|
||||
ruleId: id,
|
||||
|
@ -45,6 +46,7 @@ export const recoverRuleAlerts = async (
|
|||
ruleType: context.ruleTypeRegistry.get(attributes.alertTypeId),
|
||||
consumer: attributes.consumer,
|
||||
instanceId: alertId,
|
||||
alertUuid,
|
||||
action: EVENT_LOG_ACTIONS.recoveredInstance,
|
||||
message,
|
||||
state: instanceState,
|
||||
|
|
|
@ -253,6 +253,7 @@ describe('disable()', () => {
|
|||
group: 'default',
|
||||
date: new Date().toISOString(),
|
||||
},
|
||||
uuid: 'uuid-1',
|
||||
},
|
||||
state: { bar: false },
|
||||
},
|
||||
|
@ -313,6 +314,7 @@ describe('disable()', () => {
|
|||
},
|
||||
kibana: {
|
||||
alert: {
|
||||
uuid: 'uuid-1',
|
||||
rule: {
|
||||
consumer: 'myApp',
|
||||
rule_type_id: '123',
|
||||
|
|
|
@ -122,14 +122,14 @@ describe('getAlertSummary()', () => {
|
|||
const eventsFactory = new EventsFactory(mockedDateString);
|
||||
const events = eventsFactory
|
||||
.addExecute()
|
||||
.addNewAlert('alert-currently-active')
|
||||
.addNewAlert('alert-previously-active')
|
||||
.addActiveAlert('alert-currently-active', 'action group A')
|
||||
.addActiveAlert('alert-previously-active', 'action group B')
|
||||
.addNewAlert('alert-currently-active', 'uuid-1')
|
||||
.addNewAlert('alert-previously-active', 'uuid-2')
|
||||
.addActiveAlert('alert-currently-active', 'action group A', 'uuid-1')
|
||||
.addActiveAlert('alert-previously-active', 'action group B', 'uuid-2')
|
||||
.advanceTime(10000)
|
||||
.addExecute()
|
||||
.addRecoveredAlert('alert-previously-active')
|
||||
.addActiveAlert('alert-currently-active', 'action group A', true)
|
||||
.addRecoveredAlert('alert-previously-active', 'uuid-2')
|
||||
.addActiveAlert('alert-currently-active', 'action group A', 'uuid-1', true)
|
||||
.getEvents();
|
||||
const eventsResult = {
|
||||
...AlertSummaryFindEventsResult,
|
||||
|
@ -161,6 +161,7 @@ describe('getAlertSummary()', () => {
|
|||
"flapping": true,
|
||||
"muted": false,
|
||||
"status": "Active",
|
||||
"uuid": "uuid-1",
|
||||
},
|
||||
"alert-muted-no-activity": Object {
|
||||
"actionGroupId": undefined,
|
||||
|
@ -168,6 +169,7 @@ describe('getAlertSummary()', () => {
|
|||
"flapping": false,
|
||||
"muted": true,
|
||||
"status": "OK",
|
||||
"uuid": undefined,
|
||||
},
|
||||
"alert-previously-active": Object {
|
||||
"actionGroupId": undefined,
|
||||
|
@ -175,6 +177,7 @@ describe('getAlertSummary()', () => {
|
|||
"flapping": false,
|
||||
"muted": false,
|
||||
"status": "OK",
|
||||
"uuid": "uuid-2",
|
||||
},
|
||||
},
|
||||
"consumer": "rule-consumer",
|
||||
|
|
|
@ -112,6 +112,7 @@ const defaultExecutionParams = {
|
|||
apiKey,
|
||||
ruleConsumer: 'rule-consumer',
|
||||
executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28',
|
||||
alertUuid: 'uuid-1',
|
||||
ruleLabel: 'rule-label',
|
||||
request: {} as KibanaRequest,
|
||||
alertingEventLogger,
|
||||
|
|
|
@ -261,6 +261,7 @@ export class ExecutionHandler<
|
|||
spaceId,
|
||||
tags: this.rule.tags,
|
||||
alertInstanceId: executableAlert.getId(),
|
||||
alertUuid: executableAlert.getUuid(),
|
||||
alertActionGroup: actionGroup,
|
||||
alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!,
|
||||
context: executableAlert.getContext(),
|
||||
|
|
|
@ -250,6 +250,7 @@ export const generateAlertOpts = ({ action, group, state, id }: GeneratorParams
|
|||
return {
|
||||
action,
|
||||
id,
|
||||
uuid: expect.any(String),
|
||||
message,
|
||||
state,
|
||||
...(group ? { group } : {}),
|
||||
|
@ -355,6 +356,7 @@ export const generateAlertInstance = (
|
|||
) => ({
|
||||
[String(id)]: {
|
||||
meta: {
|
||||
uuid: expect.any(String),
|
||||
lastScheduledActions: {
|
||||
date: new Date(DATE_1970),
|
||||
group: 'default',
|
||||
|
|
|
@ -5,6 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('uuid', () => {
|
||||
let counter = 1;
|
||||
return {
|
||||
v4: () => `uuid-module-v4-called-${counter++}`,
|
||||
};
|
||||
});
|
||||
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import { Alert } from '../alert';
|
||||
import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock';
|
||||
|
@ -124,6 +131,8 @@ describe('logAlerts', () => {
|
|||
});
|
||||
|
||||
test('should correctly set values in ruleRunMetricsStore and call alertingEventLogger.logAlert if shouldPersistAlerts is true', () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
logAlerts({
|
||||
logger,
|
||||
alertingEventLogger,
|
||||
|
@ -131,15 +140,15 @@ describe('logAlerts', () => {
|
|||
'4': new Alert<{}, {}, DefaultActionGroupId>('4'),
|
||||
},
|
||||
activeAlerts: {
|
||||
'1': new Alert<{}, {}, DefaultActionGroupId>('1'),
|
||||
'2': new Alert<{}, {}, DefaultActionGroupId>('2'),
|
||||
'1': new Alert<{}, {}, DefaultActionGroupId>('1', { meta: { uuid: 'uuid-1' } }),
|
||||
'2': new Alert<{}, {}, DefaultActionGroupId>('2', { meta: { uuid: 'uuid-2' } }),
|
||||
'4': new Alert<{}, {}, DefaultActionGroupId>('4'),
|
||||
},
|
||||
recoveredAlerts: {
|
||||
'7': new Alert<{}, {}, DefaultActionGroupId>('7'),
|
||||
'8': new Alert<{}, {}, DefaultActionGroupId>('8'),
|
||||
'9': new Alert<{}, {}, DefaultActionGroupId>('9'),
|
||||
'10': new Alert<{}, {}, DefaultActionGroupId>('10'),
|
||||
'7': new Alert<{}, {}, DefaultActionGroupId>('7', { meta: { uuid: 'uuid-7' } }),
|
||||
'8': new Alert<{}, {}, DefaultActionGroupId>('8', { meta: { uuid: 'uuid-8' } }),
|
||||
'9': new Alert<{}, {}, DefaultActionGroupId>('9', { meta: { uuid: 'uuid-9' } }),
|
||||
'10': new Alert<{}, {}, DefaultActionGroupId>('10', { meta: { uuid: 'uuid-10' } }),
|
||||
},
|
||||
ruleLogPrefix: `test-rule-type-id:123: 'test rule'`,
|
||||
ruleRunMetricsStore,
|
||||
|
@ -159,6 +168,7 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' alert '7' has recovered",
|
||||
state: {},
|
||||
flapping: false,
|
||||
uuid: 'uuid-7',
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(2, {
|
||||
action: 'recovered-instance',
|
||||
|
@ -166,6 +176,7 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' alert '8' has recovered",
|
||||
state: {},
|
||||
flapping: false,
|
||||
uuid: 'uuid-8',
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(3, {
|
||||
action: 'recovered-instance',
|
||||
|
@ -173,6 +184,7 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' alert '9' has recovered",
|
||||
state: {},
|
||||
flapping: false,
|
||||
uuid: 'uuid-9',
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(4, {
|
||||
action: 'recovered-instance',
|
||||
|
@ -180,6 +192,7 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' alert '10' has recovered",
|
||||
state: {},
|
||||
flapping: false,
|
||||
uuid: 'uuid-10',
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(5, {
|
||||
action: 'new-instance',
|
||||
|
@ -187,6 +200,7 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' created new alert: '4'",
|
||||
state: {},
|
||||
flapping: false,
|
||||
uuid: expect.any(String),
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(6, {
|
||||
action: 'active-instance',
|
||||
|
@ -194,6 +208,7 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' active alert: '1' in actionGroup: 'undefined'",
|
||||
state: {},
|
||||
flapping: false,
|
||||
uuid: 'uuid-1',
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(7, {
|
||||
action: 'active-instance',
|
||||
|
@ -201,6 +216,7 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' active alert: '2' in actionGroup: 'undefined'",
|
||||
state: {},
|
||||
flapping: false,
|
||||
uuid: 'uuid-2',
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(8, {
|
||||
action: 'active-instance',
|
||||
|
@ -208,7 +224,14 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' active alert: '4' in actionGroup: 'undefined'",
|
||||
state: {},
|
||||
flapping: false,
|
||||
uuid: expect.any(String),
|
||||
});
|
||||
|
||||
// check the two calls for alert 4 used the same UUID
|
||||
const actualUuid1 = alertingEventLogger.logAlert.mock.calls[4][0].uuid;
|
||||
const actualUuid2 = alertingEventLogger.logAlert.mock.calls[7][0].uuid;
|
||||
expect(actualUuid1).toEqual(actualUuid2);
|
||||
expect(actualUuid1).toMatch(/^uuid-module-v4-called-\d+$/);
|
||||
});
|
||||
|
||||
test('should not call alertingEventLogger.logAlert or update ruleRunMetricsStore if shouldPersistAlerts is false', () => {
|
||||
|
@ -272,6 +295,8 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' alert '7' has recovered",
|
||||
state: {},
|
||||
flapping: false,
|
||||
group: undefined,
|
||||
uuid: expect.any(String),
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(2, {
|
||||
action: 'recovered-instance',
|
||||
|
@ -279,6 +304,8 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' alert '8' has recovered",
|
||||
state: {},
|
||||
flapping: true,
|
||||
group: undefined,
|
||||
uuid: expect.any(String),
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(3, {
|
||||
action: 'recovered-instance',
|
||||
|
@ -286,6 +313,8 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' alert '9' has recovered",
|
||||
state: {},
|
||||
flapping: false,
|
||||
group: undefined,
|
||||
uuid: expect.any(String),
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(4, {
|
||||
action: 'recovered-instance',
|
||||
|
@ -293,6 +322,8 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' alert '10' has recovered",
|
||||
state: {},
|
||||
flapping: false,
|
||||
group: undefined,
|
||||
uuid: expect.any(String),
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(5, {
|
||||
action: 'new-instance',
|
||||
|
@ -300,6 +331,8 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' created new alert: '4'",
|
||||
state: {},
|
||||
flapping: false,
|
||||
group: undefined,
|
||||
uuid: expect.any(String),
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(6, {
|
||||
action: 'active-instance',
|
||||
|
@ -307,6 +340,8 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' active alert: '1' in actionGroup: 'undefined'",
|
||||
state: {},
|
||||
flapping: true,
|
||||
group: undefined,
|
||||
uuid: expect.any(String),
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(7, {
|
||||
action: 'active-instance',
|
||||
|
@ -314,6 +349,8 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' active alert: '2' in actionGroup: 'undefined'",
|
||||
state: {},
|
||||
flapping: false,
|
||||
group: undefined,
|
||||
uuid: expect.any(String),
|
||||
});
|
||||
expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith(8, {
|
||||
action: 'active-instance',
|
||||
|
@ -321,6 +358,8 @@ describe('logAlerts', () => {
|
|||
message: "test-rule-type-id:123: 'test rule' active alert: '4' in actionGroup: 'undefined'",
|
||||
state: {},
|
||||
flapping: false,
|
||||
group: undefined,
|
||||
uuid: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -91,12 +91,15 @@ export function logAlerts<
|
|||
ruleRunMetricsStore.setNumberOfActiveAlerts(activeAlertIds.length);
|
||||
ruleRunMetricsStore.setNumberOfRecoveredAlerts(recoveredAlertIds.length);
|
||||
for (const id of recoveredAlertIds) {
|
||||
const { group: actionGroup } = recoveredAlerts[id].getLastScheduledActions() ?? {};
|
||||
const alert = recoveredAlerts[id];
|
||||
const { group: actionGroup } = alert.getLastScheduledActions() ?? {};
|
||||
const uuid = alert.getUuid();
|
||||
const state = recoveredAlerts[id].getState();
|
||||
const message = `${ruleLogPrefix} alert '${id}' has recovered`;
|
||||
alertingEventLogger.logAlert({
|
||||
action: EVENT_LOG_ACTIONS.recoveredInstance,
|
||||
id,
|
||||
uuid,
|
||||
group: actionGroup,
|
||||
message,
|
||||
state,
|
||||
|
@ -105,12 +108,15 @@ export function logAlerts<
|
|||
}
|
||||
|
||||
for (const id of newAlertIds) {
|
||||
const { actionGroup } = activeAlerts[id].getScheduledActionOptions() ?? {};
|
||||
const state = activeAlerts[id].getState();
|
||||
const alert = activeAlerts[id];
|
||||
const { actionGroup } = alert.getScheduledActionOptions() ?? {};
|
||||
const state = alert.getState();
|
||||
const uuid = alert.getUuid();
|
||||
const message = `${ruleLogPrefix} created new alert: '${id}'`;
|
||||
alertingEventLogger.logAlert({
|
||||
action: EVENT_LOG_ACTIONS.newInstance,
|
||||
id,
|
||||
uuid,
|
||||
group: actionGroup,
|
||||
message,
|
||||
state,
|
||||
|
@ -119,12 +125,15 @@ export function logAlerts<
|
|||
}
|
||||
|
||||
for (const id of activeAlertIds) {
|
||||
const { actionGroup } = activeAlerts[id].getScheduledActionOptions() ?? {};
|
||||
const state = activeAlerts[id].getState();
|
||||
const alert = activeAlerts[id];
|
||||
const { actionGroup } = alert.getScheduledActionOptions() ?? {};
|
||||
const state = alert.getState();
|
||||
const uuid = alert.getUuid();
|
||||
const message = `${ruleLogPrefix} active alert: '${id}' in actionGroup: '${actionGroup}'`;
|
||||
alertingEventLogger.logAlert({
|
||||
action: EVENT_LOG_ACTIONS.activeInstance,
|
||||
id,
|
||||
uuid,
|
||||
group: actionGroup,
|
||||
message,
|
||||
state,
|
||||
|
|
|
@ -2654,6 +2654,7 @@ describe('Task Runner', () => {
|
|||
alertInstances: {
|
||||
'1': {
|
||||
meta: {
|
||||
uuid: expect.any(String),
|
||||
lastScheduledActions: {
|
||||
date: new Date(DATE_1970),
|
||||
group: 'default',
|
||||
|
@ -2820,6 +2821,7 @@ describe('Task Runner', () => {
|
|||
alertInstances: {
|
||||
'1': {
|
||||
meta: {
|
||||
uuid: expect.any(String),
|
||||
lastScheduledActions: {
|
||||
date: new Date(DATE_1970),
|
||||
group: 'default',
|
||||
|
@ -2835,6 +2837,7 @@ describe('Task Runner', () => {
|
|||
},
|
||||
'2': {
|
||||
meta: {
|
||||
uuid: expect.any(String),
|
||||
lastScheduledActions: {
|
||||
date: new Date(DATE_1970),
|
||||
group: 'default',
|
||||
|
|
|
@ -46,6 +46,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {
|
||||
|
@ -83,6 +84,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -113,6 +115,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -142,6 +145,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -171,6 +175,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -200,6 +205,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -229,6 +235,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -257,6 +264,7 @@ describe('transformActionParams', () => {
|
|||
alertName: 'alert-name',
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -286,6 +294,7 @@ describe('transformActionParams', () => {
|
|||
tags: [],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -315,6 +324,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -344,6 +354,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -373,6 +384,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -402,6 +414,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -431,6 +444,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -446,7 +460,7 @@ describe('transformActionParams', () => {
|
|||
test('rule alert variables are passed to templates', () => {
|
||||
const actionParams = {
|
||||
message:
|
||||
'Value "{{alert.id}}", "{{alert.actionGroup}}" and "{{alert.actionGroupName}}" exist',
|
||||
'Value "{{alert.id}}", "{{alert.actionGroup}}", "{{alert.uuid}}" and "{{alert.actionGroupName}}" exist',
|
||||
};
|
||||
const result = transformActionParams({
|
||||
actionsPlugin,
|
||||
|
@ -461,6 +475,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -468,7 +483,7 @@ describe('transformActionParams', () => {
|
|||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"message": "Value \\"2\\", \\"action-group\\" and \\"Action Group\\" exist",
|
||||
"message": "Value \\"2\\", \\"action-group\\", \\"uuid-1\\" and \\"Action Group\\" exist",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -491,6 +506,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -522,6 +538,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -555,6 +572,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
@ -588,6 +606,7 @@ describe('transformActionParams', () => {
|
|||
tags: ['tag-A', 'tag-B'],
|
||||
spaceId: 'spaceId-A',
|
||||
alertInstanceId: '2',
|
||||
alertUuid: 'uuid-1',
|
||||
alertActionGroup: 'action-group',
|
||||
alertActionGroupName: 'Action Group',
|
||||
alertParams: {},
|
||||
|
|
|
@ -24,6 +24,7 @@ interface TransformActionParamsOptions {
|
|||
spaceId: string;
|
||||
tags?: string[];
|
||||
alertInstanceId: string;
|
||||
alertUuid: string;
|
||||
alertActionGroup: string;
|
||||
alertActionGroupName: string;
|
||||
actionParams: RuleActionParams;
|
||||
|
@ -64,6 +65,7 @@ export function transformActionParams({
|
|||
spaceId,
|
||||
tags,
|
||||
alertInstanceId,
|
||||
alertUuid,
|
||||
alertActionGroup,
|
||||
alertActionGroupName,
|
||||
context,
|
||||
|
@ -101,6 +103,7 @@ export function transformActionParams({
|
|||
},
|
||||
alert: {
|
||||
id: alertInstanceId,
|
||||
uuid: alertUuid,
|
||||
actionGroup: alertActionGroup,
|
||||
actionGroupName: alertActionGroupName,
|
||||
flapping,
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RawAlertInstance } from '../../common';
|
||||
|
||||
interface Resolvable<T> {
|
||||
resolve: (arg: T) => void;
|
||||
}
|
||||
|
@ -21,3 +23,25 @@ export function resolvable<T>(): Promise<T> & Resolvable<T> {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Used to convert a raw Rule's UUID to something that can be used
|
||||
// to compare with a jest snapshot.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function alertWithAnyUUID(rawAlert: Record<string, any>): Record<string, any> {
|
||||
if (!rawAlert?.meta?.uuid) return rawAlert;
|
||||
|
||||
const newAlert = JSON.parse(JSON.stringify(rawAlert));
|
||||
newAlert.meta.uuid = expect.any(String);
|
||||
return newAlert;
|
||||
}
|
||||
|
||||
export function alertsWithAnyUUID(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
rawAlerts: Record<string, any>
|
||||
): Record<string, RawAlertInstance> {
|
||||
const newAlerts: Record<string, RawAlertInstance> = {};
|
||||
for (const id of Object.keys(rawAlerts)) {
|
||||
newAlerts[id] = alertWithAnyUUID(rawAlerts[id]);
|
||||
}
|
||||
return newAlerts;
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
"@kbn/data-views-plugin",
|
||||
"@kbn/share-plugin",
|
||||
"@kbn/safer-lodash-set",
|
||||
"@kbn/alerting-state-types",
|
||||
"@kbn/alerts-as-data-utils",
|
||||
"@kbn/core-elasticsearch-client-server-mocks",
|
||||
"@kbn/core-saved-objects-utils-server",
|
||||
|
|
|
@ -38,13 +38,17 @@ export const createRuleTypeMocks = () => {
|
|||
} as AlertingPluginSetupContract;
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
const getUuid = jest.fn();
|
||||
|
||||
const services = {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
savedObjectsClient: {
|
||||
get: () => ({ attributes: { consumer: APM_SERVER_FEATURE_ID } }),
|
||||
},
|
||||
alertFactory: { create: jest.fn(() => ({ scheduleActions })), done: {} },
|
||||
alertFactory: {
|
||||
create: jest.fn(() => ({ scheduleActions, getUuid })),
|
||||
done: {},
|
||||
},
|
||||
alertWithLifecycle: jest.fn(),
|
||||
logger: loggerMock,
|
||||
shouldWriteAlerts: () => true,
|
||||
|
|
|
@ -304,6 +304,10 @@
|
|||
"flapping": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"uuid": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"rule": {
|
||||
"properties": {
|
||||
"consumer": {
|
||||
|
|
|
@ -140,6 +140,7 @@ export const EventSchema = schema.maybe(
|
|||
alert: schema.maybe(
|
||||
schema.object({
|
||||
flapping: ecsBoolean(),
|
||||
uuid: ecsString(),
|
||||
rule: schema.maybe(
|
||||
schema.object({
|
||||
consumer: ecsString(),
|
||||
|
|
|
@ -86,6 +86,10 @@ exports.EcsCustomPropertyMappings = {
|
|||
flapping: {
|
||||
type: 'boolean',
|
||||
},
|
||||
uuid: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024,
|
||||
},
|
||||
rule: {
|
||||
properties: {
|
||||
consumer: {
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import type { Logger } from '@kbn/logging';
|
||||
import type { PublicContract } from '@kbn/utility-types';
|
||||
import { getOrElse } from 'fp-ts/lib/Either';
|
||||
import * as rt from 'io-ts';
|
||||
import { v4 } from 'uuid';
|
||||
import { difference } from 'lodash';
|
||||
import {
|
||||
|
@ -20,6 +19,11 @@ import {
|
|||
RuleTypeState,
|
||||
} from '@kbn/alerting-plugin/server';
|
||||
import { isFlapping } from '@kbn/alerting-plugin/server/lib';
|
||||
import { wrappedStateRt, WrappedLifecycleRuleState } from '@kbn/alerting-state-types';
|
||||
export type {
|
||||
TrackedLifecycleAlertState,
|
||||
WrappedLifecycleRuleState,
|
||||
} from '@kbn/alerting-state-types';
|
||||
import { ParsedExperimentalFields } from '../../common/parse_experimental_fields';
|
||||
import { ParsedTechnicalFields } from '../../common/parse_technical_fields';
|
||||
import {
|
||||
|
@ -97,44 +101,6 @@ export type LifecycleRuleExecutor<
|
|||
>
|
||||
) => Promise<{ state: State }>;
|
||||
|
||||
const trackedAlertStateRt = rt.type({
|
||||
alertId: rt.string,
|
||||
alertUuid: rt.string,
|
||||
started: rt.string,
|
||||
// an array used to track changes in alert state, the order is based on the rule executions
|
||||
// true - alert has changed from active/recovered
|
||||
// false - alert is new or the status has remained either active or recovered
|
||||
flappingHistory: rt.array(rt.boolean),
|
||||
// flapping flag that indicates whether the alert is flapping
|
||||
flapping: rt.boolean,
|
||||
pendingRecoveredCount: rt.number,
|
||||
});
|
||||
|
||||
export type TrackedLifecycleAlertState = rt.TypeOf<typeof trackedAlertStateRt>;
|
||||
|
||||
const alertTypeStateRt = <State extends RuleTypeState>() =>
|
||||
rt.record(rt.string, rt.unknown) as rt.Type<State, State, unknown>;
|
||||
|
||||
const wrappedStateRt = <State extends RuleTypeState>() =>
|
||||
rt.type({
|
||||
wrapped: alertTypeStateRt<State>(),
|
||||
// tracks the active alerts
|
||||
trackedAlerts: rt.record(rt.string, trackedAlertStateRt),
|
||||
// tracks the recovered alerts
|
||||
trackedAlertsRecovered: rt.record(rt.string, trackedAlertStateRt),
|
||||
});
|
||||
|
||||
/**
|
||||
* This is redefined instead of derived from above `wrappedStateRt` because
|
||||
* there's no easy way to instantiate generic values such as the runtime type
|
||||
* factory function.
|
||||
*/
|
||||
export type WrappedLifecycleRuleState<State extends RuleTypeState> = RuleTypeState & {
|
||||
wrapped: State;
|
||||
trackedAlerts: Record<string, TrackedLifecycleAlertState>;
|
||||
trackedAlertsRecovered: Record<string, TrackedLifecycleAlertState>;
|
||||
};
|
||||
|
||||
export const createLifecycleExecutor =
|
||||
(logger: Logger, ruleDataClient: PublicContract<IRuleDataClient>) =>
|
||||
<
|
||||
|
@ -165,6 +131,7 @@ export const createLifecycleExecutor =
|
|||
services: { alertFactory, shouldWriteAlerts },
|
||||
state: previousState,
|
||||
flappingSettings,
|
||||
rule,
|
||||
} = options;
|
||||
|
||||
const ruleDataClientWriter = await ruleDataClient.getWriter();
|
||||
|
@ -180,8 +147,7 @@ export const createLifecycleExecutor =
|
|||
const commonRuleFields = getCommonAlertFields(options);
|
||||
|
||||
const currentAlerts: Record<string, ExplicitAlertFields> = {};
|
||||
|
||||
const newAlertUuids: Record<string, string> = {};
|
||||
const alertUuidMap: Map<string, string> = new Map();
|
||||
|
||||
const lifecycleAlertServices: LifecycleAlertServices<
|
||||
InstanceState,
|
||||
|
@ -190,18 +156,33 @@ export const createLifecycleExecutor =
|
|||
> = {
|
||||
alertWithLifecycle: ({ id, fields }) => {
|
||||
currentAlerts[id] = fields;
|
||||
return alertFactory.create(id);
|
||||
const alert = alertFactory.create(id);
|
||||
const uuid = alert.getUuid();
|
||||
alertUuidMap.set(id, uuid);
|
||||
return alert;
|
||||
},
|
||||
getAlertStartedDate: (alertId: string) => state.trackedAlerts[alertId]?.started ?? null,
|
||||
getAlertUuid: (alertId: string) => {
|
||||
let existingUuid = state.trackedAlerts[alertId]?.alertUuid || newAlertUuids[alertId];
|
||||
|
||||
if (!existingUuid) {
|
||||
existingUuid = v4();
|
||||
newAlertUuids[alertId] = existingUuid;
|
||||
const uuid = alertUuidMap.get(alertId);
|
||||
if (uuid) {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
return existingUuid;
|
||||
const trackedAlert = state.trackedAlerts[alertId];
|
||||
if (trackedAlert) {
|
||||
return trackedAlert.alertUuid;
|
||||
}
|
||||
|
||||
const trackedRecoveredAlert = state.trackedAlertsRecovered[alertId];
|
||||
if (trackedRecoveredAlert) {
|
||||
return trackedRecoveredAlert.alertUuid;
|
||||
}
|
||||
|
||||
const alertInfo = `alert ${alertId} of rule ${rule.ruleTypeId}:${rule.id}`;
|
||||
logger.warn(
|
||||
`[Rule Registry] requesting uuid for ${alertInfo} which is not tracked, generating dynamically`
|
||||
);
|
||||
return v4();
|
||||
},
|
||||
getAlertByAlertUuid: async (alertUuid: string) => {
|
||||
try {
|
||||
|
@ -259,13 +240,14 @@ export const createLifecycleExecutor =
|
|||
alertIds.map((alertId) => {
|
||||
const alertData = trackedAlertsDataMap[alertId];
|
||||
const currentAlertData = currentAlerts[alertId];
|
||||
const trackedAlert = state.trackedAlerts[alertId];
|
||||
|
||||
if (!alertData) {
|
||||
logger.debug(`[Rule Registry] Could not find alert data for ${alertId}`);
|
||||
}
|
||||
|
||||
const isNew = !state.trackedAlerts[alertId];
|
||||
const isRecovered = !currentAlerts[alertId];
|
||||
const isNew = !trackedAlert;
|
||||
const isRecovered = !currentAlertData;
|
||||
const isActive = !isRecovered;
|
||||
|
||||
const flappingHistory = getUpdatedFlappingHistory<State>(
|
||||
|
|
|
@ -77,10 +77,14 @@ function createRule(shouldWriteAlerts: boolean = true) {
|
|||
|
||||
const scheduleActions = jest.fn();
|
||||
|
||||
let uuidCounter = 1;
|
||||
const getUuid = jest.fn(() => `uuid-${uuidCounter++}`);
|
||||
|
||||
const alertFactory = {
|
||||
create: () => {
|
||||
return {
|
||||
scheduleActions,
|
||||
getUuid,
|
||||
} as any;
|
||||
},
|
||||
alertLimit: {
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"@kbn/logging",
|
||||
"@kbn/securitysolution-io-ts-utils",
|
||||
"@kbn/share-plugin",
|
||||
"@kbn/alerting-state-types",
|
||||
"@kbn/alerts-as-data-utils",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import type {
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
|
@ -49,4 +50,7 @@ export const alertInstanceFactoryStub = <
|
|||
hasContext() {
|
||||
return false;
|
||||
},
|
||||
getUuid() {
|
||||
return uuidV4();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -195,6 +195,7 @@ export const previewRulesRoute = async (
|
|||
| 'setContext'
|
||||
| 'getContext'
|
||||
| 'hasContext'
|
||||
| 'getUuid'
|
||||
>;
|
||||
alertLimit: {
|
||||
getValue: () => number;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
LogMeta,
|
||||
SavedObjectMigrationContext,
|
||||
|
@ -13,16 +14,22 @@ import {
|
|||
SavedObjectsUtils,
|
||||
SavedObjectUnsanitizedDoc,
|
||||
} from '@kbn/core/server';
|
||||
import type {
|
||||
RuleTaskState,
|
||||
TrackedLifecycleAlertState,
|
||||
WrappedLifecycleRuleState,
|
||||
} from '@kbn/alerting-state-types';
|
||||
|
||||
import { REMOVED_TYPES } from '../task_type_dictionary';
|
||||
import { ConcreteTaskInstance, TaskStatus } from '../task';
|
||||
import { SerializedConcreteTaskInstance, TaskStatus } from '../task';
|
||||
|
||||
interface TaskInstanceLogMeta extends LogMeta {
|
||||
migrations: { taskInstanceDocument: SavedObjectUnsanitizedDoc<ConcreteTaskInstance> };
|
||||
migrations: { taskInstanceDocument: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance> };
|
||||
}
|
||||
|
||||
type TaskInstanceMigration = (
|
||||
doc: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>
|
||||
) => SavedObjectUnsanitizedDoc<ConcreteTaskInstance>;
|
||||
doc: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>
|
||||
) => SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>;
|
||||
|
||||
export function getMigrations(): SavedObjectMigrationMap {
|
||||
return {
|
||||
|
@ -43,15 +50,19 @@ export function getMigrations(): SavedObjectMigrationMap {
|
|||
'8.2.0'
|
||||
),
|
||||
'8.5.0': executeMigrationWithErrorHandling(pipeMigrations(addEnabledField), '8.5.0'),
|
||||
'8.8.0': executeMigrationWithErrorHandling(pipeMigrations(addAlertUUID), '8.8.0'),
|
||||
};
|
||||
}
|
||||
|
||||
function executeMigrationWithErrorHandling(
|
||||
migrationFunc: SavedObjectMigrationFn<ConcreteTaskInstance, ConcreteTaskInstance>,
|
||||
migrationFunc: SavedObjectMigrationFn<
|
||||
SerializedConcreteTaskInstance,
|
||||
SerializedConcreteTaskInstance
|
||||
>,
|
||||
version: string
|
||||
) {
|
||||
return (
|
||||
doc: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>,
|
||||
doc: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>,
|
||||
context: SavedObjectMigrationContext
|
||||
) => {
|
||||
try {
|
||||
|
@ -71,8 +82,8 @@ function executeMigrationWithErrorHandling(
|
|||
}
|
||||
|
||||
function alertingTaskLegacyIdToSavedObjectIds(
|
||||
doc: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>
|
||||
): SavedObjectUnsanitizedDoc<ConcreteTaskInstance> {
|
||||
doc: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>
|
||||
): SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance> {
|
||||
if (doc.attributes.taskType.startsWith('alerting:')) {
|
||||
let params: { spaceId?: string; alertId?: string } = {};
|
||||
params = JSON.parse(doc.attributes.params as unknown as string);
|
||||
|
@ -97,8 +108,8 @@ function alertingTaskLegacyIdToSavedObjectIds(
|
|||
}
|
||||
|
||||
function actionsTasksLegacyIdToSavedObjectIds(
|
||||
doc: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>
|
||||
): SavedObjectUnsanitizedDoc<ConcreteTaskInstance> {
|
||||
doc: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>
|
||||
): SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance> {
|
||||
if (doc.attributes.taskType.startsWith('actions:')) {
|
||||
let params: { spaceId?: string; actionTaskParamsId?: string } = {};
|
||||
params = JSON.parse(doc.attributes.params as unknown as string);
|
||||
|
@ -129,7 +140,7 @@ function actionsTasksLegacyIdToSavedObjectIds(
|
|||
function moveIntervalIntoSchedule({
|
||||
attributes: { interval, ...attributes },
|
||||
...doc
|
||||
}: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>): SavedObjectUnsanitizedDoc<ConcreteTaskInstance> {
|
||||
}: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>): SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance> {
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
|
@ -146,8 +157,8 @@ function moveIntervalIntoSchedule({
|
|||
}
|
||||
|
||||
function resetUnrecognizedStatus(
|
||||
doc: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>
|
||||
): SavedObjectUnsanitizedDoc<ConcreteTaskInstance> {
|
||||
doc: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>
|
||||
): SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance> {
|
||||
const status = doc?.attributes?.status;
|
||||
if (status && status === 'unrecognized') {
|
||||
const taskType = doc.attributes.taskType;
|
||||
|
@ -162,20 +173,20 @@ function resetUnrecognizedStatus(
|
|||
...doc.attributes,
|
||||
status: 'idle',
|
||||
},
|
||||
} as SavedObjectUnsanitizedDoc<ConcreteTaskInstance>;
|
||||
} as SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>;
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
function pipeMigrations(...migrations: TaskInstanceMigration[]): TaskInstanceMigration {
|
||||
return (doc: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>) =>
|
||||
return (doc: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>) =>
|
||||
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc);
|
||||
}
|
||||
|
||||
function resetAttemptsAndStatusForTheTasksWithoutSchedule(
|
||||
doc: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>
|
||||
): SavedObjectUnsanitizedDoc<ConcreteTaskInstance> {
|
||||
doc: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>
|
||||
): SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance> {
|
||||
if (doc.attributes.taskType.startsWith('alerting:')) {
|
||||
if (
|
||||
!doc.attributes.schedule?.interval &&
|
||||
|
@ -195,7 +206,7 @@ function resetAttemptsAndStatusForTheTasksWithoutSchedule(
|
|||
return doc;
|
||||
}
|
||||
|
||||
function addEnabledField(doc: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>) {
|
||||
function addEnabledField(doc: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>) {
|
||||
if (
|
||||
doc.attributes.status === TaskStatus.Failed ||
|
||||
doc.attributes.status === TaskStatus.Unrecognized
|
||||
|
@ -211,3 +222,83 @@ function addEnabledField(doc: SavedObjectUnsanitizedDoc<ConcreteTaskInstance>) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
function addAlertUUID(doc: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance>) {
|
||||
if (!doc.attributes.taskType.startsWith('alerting:')) return doc;
|
||||
if (!doc.attributes.state) return doc;
|
||||
|
||||
const taskState: RuleTaskState = JSON.parse(doc.attributes.state);
|
||||
const ruleState = taskState?.alertTypeState;
|
||||
if (!ruleState) return doc;
|
||||
|
||||
// get existing alert uuid's from the rule registry's rule state wrapper
|
||||
const alertToTrackedMap = getAlertsToTrackedMap(ruleState);
|
||||
|
||||
// we are iterating over two collections of alerts, so in case there are
|
||||
// duplicates, keep track of all uuid's assigned, so the same one will be used
|
||||
const currentUUIDs = new Map<string, string>();
|
||||
|
||||
// add the uuids to the framework's meta object; the objects are mutated in-line
|
||||
addAlertUUIDsToAlerts(taskState.alertInstances, alertToTrackedMap, currentUUIDs);
|
||||
addAlertUUIDsToAlerts(taskState.alertRecoveredInstances, alertToTrackedMap, currentUUIDs);
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
state: JSON.stringify(taskState),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// mutates alerts passed in
|
||||
function addAlertUUIDsToAlerts(
|
||||
alerts: RuleTaskState['alertInstances'] | undefined,
|
||||
alertToTrackedMap: Map<string, TrackedLifecycleAlertState>,
|
||||
currentUUIDs: Map<string, string>
|
||||
): void {
|
||||
if (!alerts) return;
|
||||
|
||||
for (const [id, alert] of Object.entries(alerts)) {
|
||||
if (!alert.meta) alert.meta = {};
|
||||
|
||||
// get alert info from tracked map (rule registry)
|
||||
const trackedAlert = alertToTrackedMap.get(id);
|
||||
// get uuid for current alert, if we've already seen it
|
||||
const recentUUID = currentUUIDs.get(id);
|
||||
|
||||
if (trackedAlert?.alertUuid) {
|
||||
alert.meta.uuid = trackedAlert.alertUuid;
|
||||
} else if (recentUUID) {
|
||||
alert.meta.uuid = recentUUID;
|
||||
} else {
|
||||
alert.meta.uuid = uuidv4();
|
||||
}
|
||||
|
||||
currentUUIDs.set(id, alert.meta.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
// gets a map of alertId => tracked alert state, which is from the
|
||||
// rule registry wrapper, which contains the uuid and other info
|
||||
function getAlertsToTrackedMap(
|
||||
ruleState: Record<string, unknown>
|
||||
): Map<string, TrackedLifecycleAlertState> {
|
||||
const result = new Map<string, TrackedLifecycleAlertState>();
|
||||
|
||||
if (!isRuleRegistryWrappedState(ruleState)) return result;
|
||||
|
||||
return new Map([
|
||||
...Object.entries(ruleState.trackedAlerts || {}),
|
||||
...Object.entries(ruleState.trackedAlertsRecovered || {}),
|
||||
]);
|
||||
}
|
||||
|
||||
function isRuleRegistryWrappedState(
|
||||
ruleState: Record<string, unknown>
|
||||
): ruleState is WrappedLifecycleRuleState<never> {
|
||||
return (
|
||||
ruleState.wrapped != null &&
|
||||
(ruleState.trackedAlerts != null || ruleState.trackedAlertsRecovered != null)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,327 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { omit, cloneDeep } from 'lodash';
|
||||
import { SavedObjectUnsanitizedDoc } from '@kbn/core/server';
|
||||
import { migrationMocks } from '@kbn/core/server/mocks';
|
||||
import type {
|
||||
RuleTaskState,
|
||||
WrappedLifecycleRuleState,
|
||||
RawAlertInstance,
|
||||
} from '@kbn/alerting-state-types';
|
||||
|
||||
import { getMigrations } from './migrations';
|
||||
import { SerializedConcreteTaskInstance, TaskStatus } from '../task';
|
||||
|
||||
type RawAlertInstances = Record<string, RawAlertInstance>;
|
||||
|
||||
const migrationContext = migrationMocks.createContext();
|
||||
const migration880 = getMigrations()['8.8.0'];
|
||||
|
||||
describe('successful migrations for 8.8.0', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('validate test data', () => {
|
||||
// the RuleState docs are included to make it easier to read
|
||||
// the tests; we validate they are the parsed JSON here
|
||||
const mtState = JSON.parse(TaskDocMetricThreshold.task.state);
|
||||
expect(mtState).toStrictEqual(RuleStateMetricThreshold);
|
||||
|
||||
const itState = JSON.parse(TaskDocIndexThreshold.task.state);
|
||||
expect(itState).toStrictEqual(RuleStateIndexThreshold);
|
||||
});
|
||||
|
||||
describe('move rule alert uuid to framework meta', () => {
|
||||
test('does not change non-rule tasks', () => {
|
||||
const task = getMockData();
|
||||
expect(migration880(task, migrationContext)).toEqual(task);
|
||||
});
|
||||
|
||||
test('does not change rule task with no state', () => {
|
||||
const task = getMockData({ taskType: 'alerting:some-rule-id', state: undefined });
|
||||
expect(migration880(task, migrationContext)).toEqual(task);
|
||||
});
|
||||
|
||||
test('does not change rule task with empty state', () => {
|
||||
const task = getMockData({ taskType: 'alerting:some-rule-id', state: '{}' });
|
||||
expect(migration880(task, migrationContext)).toEqual(task);
|
||||
});
|
||||
|
||||
test('for non-lifecycle rules, adds new uuid to alert meta', () => {
|
||||
const task = getMockData(TaskDocIndexThreshold.task);
|
||||
const taskMigrated = migration880(task, migrationContext);
|
||||
const state = JSON.parse(taskMigrated.attributes.state) as RuleTaskState;
|
||||
|
||||
checkMetaInRuleTaskState(state, RuleStateIndexThreshold);
|
||||
});
|
||||
|
||||
test('for lifecycle rules, copies uuid to alert meta', () => {
|
||||
const task = getMockData(TaskDocMetricThreshold.task);
|
||||
const taskMigrated = migration880(task, migrationContext);
|
||||
const state = JSON.parse(taskMigrated.attributes.state);
|
||||
|
||||
checkMetaInRuleTaskStateWrapped(state, RuleStateMetricThreshold);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function checkMetaInRuleTaskState(
|
||||
actual: RuleTaskState,
|
||||
original: RuleTaskState,
|
||||
wrappedUUIDs?: Map<string, string>
|
||||
) {
|
||||
// delete the uuids from actual (a copy of it) to compare to original
|
||||
const copy = cloneDeep(actual);
|
||||
|
||||
// make sure every alertInstance element has a UUID, and that's the only change
|
||||
for (const [id, alert] of Object.entries(actual.alertInstances || {})) {
|
||||
checkAlert(id, alert, original.alertInstances);
|
||||
delete copy?.alertInstances?.[id].meta?.uuid;
|
||||
}
|
||||
|
||||
// make sure every alertRecoveredInstance element has a UUID, and that's the only change
|
||||
for (const [id, alert] of Object.entries(actual.alertRecoveredInstances || {})) {
|
||||
checkAlert(id, alert, original.alertRecoveredInstances);
|
||||
delete copy?.alertRecoveredInstances?.[id].meta?.uuid;
|
||||
}
|
||||
|
||||
// after deleting the uuids, should be same as the original
|
||||
expect(copy).toStrictEqual(original);
|
||||
|
||||
function checkAlert(id: string, alert: RawAlertInstance, instances?: RawAlertInstances) {
|
||||
expect(alert.meta?.uuid).toMatch(/^.{36}$/);
|
||||
|
||||
const expectedAlert = instances?.[id];
|
||||
expect(omit(alert, 'meta.uuid')).toStrictEqual(expectedAlert);
|
||||
|
||||
if (wrappedUUIDs) {
|
||||
expect(alert.meta?.uuid).toBe(wrappedUUIDs.get(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkMetaInRuleTaskStateWrapped(actual: RuleTaskState, expected: RuleTaskState) {
|
||||
const wrappedState = expected.alertTypeState as WrappedLifecycleRuleState<never>;
|
||||
|
||||
const wrappedUUIDs = new Map<string, string>();
|
||||
for (const [id, alert] of Object.entries(wrappedState.trackedAlerts || {})) {
|
||||
wrappedUUIDs.set(id, alert.alertUuid);
|
||||
}
|
||||
|
||||
for (const [id, alert] of Object.entries(wrappedState.trackedAlertsRecovered || {})) {
|
||||
wrappedUUIDs.set(id, alert.alertUuid);
|
||||
}
|
||||
|
||||
checkMetaInRuleTaskState(actual, expected, wrappedUUIDs);
|
||||
}
|
||||
|
||||
export function getMockData(
|
||||
overwrites: Record<string, unknown> = {}
|
||||
): SavedObjectUnsanitizedDoc<SerializedConcreteTaskInstance> {
|
||||
return {
|
||||
id: 'some-uuid',
|
||||
type: 'task',
|
||||
attributes: {
|
||||
id: 'some-id',
|
||||
status: TaskStatus.Idle,
|
||||
taskType: 'some-taskType',
|
||||
state: JSON.stringify({}),
|
||||
params: JSON.stringify({ prop: true }),
|
||||
traceparent: 'some-traceparent',
|
||||
scheduledAt: new Date().toISOString(),
|
||||
startedAt: null,
|
||||
retryAt: null,
|
||||
runAt: new Date().toISOString(),
|
||||
attempts: 0,
|
||||
ownerId: null,
|
||||
...cloneDeep(overwrites),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// data below generated by ../migrations_helpers/get-rule-task-state.js
|
||||
|
||||
const TaskDocIndexThreshold = {
|
||||
migrationVersion: { task: '8.5.0' },
|
||||
task: {
|
||||
retryAt: null,
|
||||
runAt: '2023-02-22T03:38:19.334Z',
|
||||
startedAt: null,
|
||||
params:
|
||||
'{"alertId":"4f11f730-b262-11ed-b5fa-5de11bbd3e96","spaceId":"default","consumer":"alerts"}',
|
||||
ownerId: null,
|
||||
enabled: true,
|
||||
schedule: { interval: '3s' },
|
||||
taskType: 'alerting:.index-threshold',
|
||||
scope: ['alerting'],
|
||||
traceparent: '',
|
||||
state:
|
||||
'{"alertTypeState":{},"alertInstances":{"host-C":{"state":{"start":"2023-02-22T03:38:10.433Z","duration":"6006000000"},"meta":{"flappingHistory":[true,false],"flapping":false,"pendingRecoveredCount":0,"lastScheduledActions":{"group":"threshold met","date":"2023-02-22T03:38:16.442Z"}}}},"alertRecoveredInstances":{"host-A":{"meta":{"flappingHistory":[true,false,true],"flapping":false}},"host-B":{"meta":{"flappingHistory":[true,false,true],"flapping":false}}},"summaryActions":{},"previousStartedAt":"2023-02-22T03:38:16.334Z"}',
|
||||
scheduledAt: '2023-02-22T03:38:13.331Z',
|
||||
attempts: 0,
|
||||
status: 'idle',
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2023-02-22T03:38:16.570Z',
|
||||
coreMigrationVersion: '8.7.0',
|
||||
created_at: '2023-02-22T03:38:03.160Z',
|
||||
type: 'task',
|
||||
};
|
||||
|
||||
// included just so the `state` JSON data ^^^ is readable
|
||||
const RuleStateIndexThreshold: RuleTaskState = {
|
||||
alertTypeState: {},
|
||||
alertInstances: {
|
||||
'host-C': {
|
||||
state: {
|
||||
start: '2023-02-22T03:38:10.433Z',
|
||||
duration: '6006000000',
|
||||
},
|
||||
meta: {
|
||||
flappingHistory: [true, false],
|
||||
flapping: false,
|
||||
pendingRecoveredCount: 0,
|
||||
lastScheduledActions: {
|
||||
group: 'threshold met',
|
||||
date: '2023-02-22T03:38:16.442Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
alertRecoveredInstances: {
|
||||
'host-A': {
|
||||
meta: {
|
||||
flappingHistory: [true, false, true],
|
||||
flapping: false,
|
||||
},
|
||||
},
|
||||
'host-B': {
|
||||
meta: {
|
||||
flappingHistory: [true, false, true],
|
||||
flapping: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
summaryActions: {},
|
||||
previousStartedAt: '2023-02-22T03:38:16.334Z',
|
||||
|
||||
// This cast is needed as RuleTaskState defines dates as Date, but
|
||||
// they are stored as strings. There is no "serialized" version
|
||||
// of this, it's an io-ts generated type. You can check the rest
|
||||
// of the types by deleting the "as unknown as ..." bits below.
|
||||
} as unknown as RuleTaskState;
|
||||
|
||||
const TaskDocMetricThreshold = {
|
||||
migrationVersion: { task: '8.5.0' },
|
||||
task: {
|
||||
retryAt: null,
|
||||
runAt: '2023-02-22T03:38:16.328Z',
|
||||
startedAt: null,
|
||||
params:
|
||||
'{"alertId":"4dd72d40-b262-11ed-b5fa-5de11bbd3e96","spaceId":"default","consumer":"infrastructure"}',
|
||||
ownerId: null,
|
||||
enabled: true,
|
||||
schedule: { interval: '3s' },
|
||||
taskType: 'alerting:metrics.alert.threshold',
|
||||
scope: ['alerting'],
|
||||
traceparent: '',
|
||||
state:
|
||||
'{"alertTypeState":{"wrapped":{"lastRunTimestamp":1677037093328,"missingGroups":[],"groupBy":["network.name"]},"trackedAlerts":{"host-A":{"alertId":"host-A","alertUuid":"f8b420a4-a596-4c96-8e42-6da5a167dd56","started":"2023-02-22T03:38:13.328Z","flappingHistory":[true,true,true],"flapping":false,"pendingRecoveredCount":0},"host-B":{"alertId":"host-B","alertUuid":"1ecbe90f-a196-40db-aede-094577ccbf14","started":"2023-02-22T03:38:13.328Z","flappingHistory":[true,true,true],"flapping":false,"pendingRecoveredCount":0}},"trackedAlertsRecovered":{"host-C":{"alertId":"host-C","alertUuid":"95669714-2ab6-4155-8928-107aea504f39","started":"2023-02-22T03:38:07.330Z","flappingHistory":[true,true],"flapping":false,"pendingRecoveredCount":0}}},"alertInstances":{"host-A":{"state":{"start":"2023-02-22T03:38:13.587Z","duration":"0"},"meta":{"flappingHistory":[true,true,true],"flapping":false,"pendingRecoveredCount":0,"lastScheduledActions":{"group":"metrics.threshold.fired","date":"2023-02-22T03:38:13.590Z"}}},"host-B":{"state":{"start":"2023-02-22T03:38:13.587Z","duration":"0"},"meta":{"flappingHistory":[true,true,true],"flapping":false,"pendingRecoveredCount":0,"lastScheduledActions":{"group":"metrics.threshold.fired","date":"2023-02-22T03:38:13.591Z"}}}},"alertRecoveredInstances":{"host-C":{"meta":{"flappingHistory":[true,true],"flapping":false}}},"summaryActions":{},"previousStartedAt":"2023-02-22T03:38:13.328Z"}',
|
||||
scheduledAt: '2023-02-22T03:38:10.330Z',
|
||||
attempts: 0,
|
||||
status: 'idle',
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2023-02-22T03:38:13.699Z',
|
||||
coreMigrationVersion: '8.7.0',
|
||||
created_at: '2023-02-22T03:38:01.102Z',
|
||||
type: 'task',
|
||||
};
|
||||
|
||||
// included just so the `state` JSON data ^^^ is readable
|
||||
const RuleStateMetricThreshold: RuleTaskState = {
|
||||
alertTypeState: {
|
||||
wrapped: {
|
||||
lastRunTimestamp: 1677037093328,
|
||||
missingGroups: [],
|
||||
groupBy: ['network.name'],
|
||||
},
|
||||
trackedAlerts: {
|
||||
'host-A': {
|
||||
alertId: 'host-A',
|
||||
alertUuid: 'f8b420a4-a596-4c96-8e42-6da5a167dd56',
|
||||
started: '2023-02-22T03:38:13.328Z',
|
||||
flappingHistory: [true, true, true],
|
||||
flapping: false,
|
||||
pendingRecoveredCount: 0,
|
||||
},
|
||||
'host-B': {
|
||||
alertId: 'host-B',
|
||||
alertUuid: '1ecbe90f-a196-40db-aede-094577ccbf14',
|
||||
started: '2023-02-22T03:38:13.328Z',
|
||||
flappingHistory: [true, true, true],
|
||||
flapping: false,
|
||||
pendingRecoveredCount: 0,
|
||||
},
|
||||
},
|
||||
trackedAlertsRecovered: {
|
||||
'host-C': {
|
||||
alertId: 'host-C',
|
||||
alertUuid: '95669714-2ab6-4155-8928-107aea504f39',
|
||||
started: '2023-02-22T03:38:07.330Z',
|
||||
flappingHistory: [true, true],
|
||||
flapping: false,
|
||||
pendingRecoveredCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
alertInstances: {
|
||||
'host-A': {
|
||||
state: {
|
||||
start: '2023-02-22T03:38:13.587Z',
|
||||
duration: '0',
|
||||
},
|
||||
meta: {
|
||||
flappingHistory: [true, true, true],
|
||||
flapping: false,
|
||||
pendingRecoveredCount: 0,
|
||||
lastScheduledActions: {
|
||||
group: 'metrics.threshold.fired',
|
||||
date: '2023-02-22T03:38:13.590Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
'host-B': {
|
||||
state: {
|
||||
start: '2023-02-22T03:38:13.587Z',
|
||||
duration: '0',
|
||||
},
|
||||
meta: {
|
||||
flappingHistory: [true, true, true],
|
||||
flapping: false,
|
||||
pendingRecoveredCount: 0,
|
||||
lastScheduledActions: {
|
||||
group: 'metrics.threshold.fired',
|
||||
date: '2023-02-22T03:38:13.591Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
alertRecoveredInstances: {
|
||||
'host-C': {
|
||||
meta: {
|
||||
flappingHistory: [true, true],
|
||||
flapping: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
summaryActions: {},
|
||||
previousStartedAt: '2023-02-22T03:38:13.328Z',
|
||||
// see ^^^ for the index threshold rule why this cast is needed
|
||||
} as unknown as RuleTaskState;
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/*
|
||||
Used to get data for a migration test.
|
||||
|
||||
This script will create a metric threshold and index threshold rule,
|
||||
for the purposes of looking at their task state. It arranges to get
|
||||
the rules active and then recovered, so that the alert data will show
|
||||
up in both the "active" and "recovered but tracking for flapping"
|
||||
containers.
|
||||
|
||||
It then will send data for the alerts to go active on, then recover,
|
||||
forever.
|
||||
|
||||
It will then query the task documents for the rules, waiting till it
|
||||
gets some in both of the containers. It prints the task document as
|
||||
a single line of JSON, and the task state as pretty JSON, just so you
|
||||
can "read it" - because it's stored as a JSON string in the task doc.
|
||||
|
||||
- env var `$KBN_URL` should be set to your Kibana URL, with user/pass.
|
||||
- env var `$ES_URL` should be set to your Elasticsearch URL, with user/pass.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const https = require('node:https');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const KB_URL = process.env.KB_URL || process.env.KBN_URL;
|
||||
const ES_URL = process.env.ES_URL;
|
||||
|
||||
const dataIndex = 'rule-task-state--dev';
|
||||
const dataAlias = `metrics-${dataIndex}`; // metrics rules look for metrics-* indices
|
||||
const mappings = {
|
||||
properties: {
|
||||
'@timestamp': { type: 'date' },
|
||||
'network.packets': { type: 'long' },
|
||||
'network.name': { type: 'keyword' },
|
||||
},
|
||||
};
|
||||
|
||||
let Active = true;
|
||||
let Conn;
|
||||
let MtRule;
|
||||
let ItRule;
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
const createdIndex = await putEs(dataIndex, { mappings });
|
||||
console.log(`created index: ${JSON.stringify(createdIndex)}`);
|
||||
|
||||
const alias = await putEs(`${dataIndex}/_alias/${dataAlias}`);
|
||||
console.log(`alias for metrics: ${JSON.stringify(alias)}`);
|
||||
|
||||
// write data @ 1s, alternating active / not active @ 5s
|
||||
setInterval(writeData, 1000);
|
||||
setInterval(() => (Active = !Active), 5000);
|
||||
|
||||
const createConnPayload = {
|
||||
name: 'server log for rule-task-state',
|
||||
connector_type_id: '.server-log',
|
||||
};
|
||||
|
||||
Conn = await postKbn(`api/actions/connector`, createConnPayload);
|
||||
console.log(`server log id: ${Conn.id}`);
|
||||
|
||||
MtRule = await postKbn(`api/alerting/rule`, getMtRulePayload());
|
||||
console.log(`metric threshold rule id: ${MtRule.id}`);
|
||||
|
||||
ItRule = await postKbn(`api/alerting/rule`, getItRulePayload());
|
||||
console.log(`index threshold rule id: ${ItRule.id}`);
|
||||
|
||||
setInterval(getTaskDocs, 3000);
|
||||
}
|
||||
|
||||
function writeData() {
|
||||
const date = new Date().toISOString();
|
||||
postEs(`${dataIndex}/_doc`, {
|
||||
'@timestamp': date,
|
||||
network: { name: 'host-A', packets: Active ? 1 : 0 },
|
||||
});
|
||||
postEs(`${dataIndex}/_doc`, {
|
||||
'@timestamp': date,
|
||||
network: { name: 'host-B', packets: Active ? 1 : 0 },
|
||||
});
|
||||
postEs(`${dataIndex}/_doc`, {
|
||||
'@timestamp': date,
|
||||
network: { name: 'host-C', packets: Active ? 0 : 1 },
|
||||
});
|
||||
}
|
||||
|
||||
async function getTaskDocs() {
|
||||
const { task: mtTaskState, ruleState: mtRuleState } = await getTask(MtRule.id);
|
||||
const { task: itTaskState, ruleState: itRuleState } = await getTask(ItRule.id);
|
||||
|
||||
console.log('--------------------------------------------------------');
|
||||
console.log(JSON.stringify(itTaskState._source));
|
||||
console.log(JSON.stringify(itRuleState, null, 4));
|
||||
console.log();
|
||||
console.log(JSON.stringify(mtTaskState._source));
|
||||
console.log(JSON.stringify(mtRuleState, null, 4));
|
||||
console.log();
|
||||
console.log('waiting for better task docs');
|
||||
console.log();
|
||||
|
||||
if (
|
||||
Object.keys(itRuleState.alertInstances).length > 0 &&
|
||||
Object.keys(mtRuleState.alertInstances).length > 0 &&
|
||||
Object.keys(itRuleState.alertRecoveredInstances).length > 0 &&
|
||||
Object.keys(mtRuleState.alertRecoveredInstances).length > 0
|
||||
) {
|
||||
console.log('that last one is a keeper!');
|
||||
|
||||
console.log('full docs for es archive:');
|
||||
console.log('');
|
||||
console.log(JSON.stringify(itTaskState, null, 4));
|
||||
console.log('');
|
||||
console.log(JSON.stringify(mtTaskState, null, 4));
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
function getMtRulePayload() {
|
||||
return {
|
||||
consumer: 'infrastructure',
|
||||
name: 'rule-mt',
|
||||
schedule: {
|
||||
interval: '3s',
|
||||
},
|
||||
params: {
|
||||
criteria: [
|
||||
{
|
||||
aggType: 'max',
|
||||
comparator: '>',
|
||||
threshold: [0],
|
||||
timeSize: 3,
|
||||
timeUnit: 's',
|
||||
metric: 'network.packets',
|
||||
},
|
||||
],
|
||||
sourceId: 'default',
|
||||
alertOnNoData: false,
|
||||
alertOnGroupDisappear: false,
|
||||
groupBy: ['network.name'],
|
||||
},
|
||||
rule_type_id: 'metrics.alert.threshold',
|
||||
notify_when: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'metrics.threshold.fired',
|
||||
id: Conn.id,
|
||||
params: {
|
||||
message:
|
||||
'{{alertName}} - {{context.group}} is in a state of {{context.alertState}}\n\nReason:\n{{context.reason}}\n',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getItRulePayload() {
|
||||
return {
|
||||
rule_type_id: '.index-threshold',
|
||||
name: 'rule-it',
|
||||
notify_when: 'onActiveAlert',
|
||||
consumer: 'alerts',
|
||||
schedule: { interval: '3s' },
|
||||
actions: [
|
||||
{
|
||||
group: 'threshold met',
|
||||
id: Conn.id,
|
||||
params: { message: '{{context.message}}' },
|
||||
},
|
||||
],
|
||||
params: {
|
||||
index: [dataIndex],
|
||||
timeField: '@timestamp',
|
||||
aggType: 'max',
|
||||
aggField: 'network.packets',
|
||||
groupBy: 'top',
|
||||
termSize: 100,
|
||||
termField: 'network.name',
|
||||
timeWindowSize: 3,
|
||||
timeWindowUnit: 's',
|
||||
thresholdComparator: '>',
|
||||
threshold: [0],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** @type { (id: string) => Promise<any> } */
|
||||
async function getTask(id) {
|
||||
await getURL(`${ES_URL}/_refresh`);
|
||||
|
||||
const task = await getEs(`/.kibana_task_manager/_doc/task:${id}`);
|
||||
|
||||
const ruleStateJ = task._source.task.state;
|
||||
const ruleState = JSON.parse(ruleStateJ);
|
||||
|
||||
return { task, ruleState };
|
||||
}
|
||||
|
||||
async function getEs(url) {
|
||||
return getURL(path.join(ES_URL, url));
|
||||
}
|
||||
async function postEs(url, body) {
|
||||
return postURL(path.join(ES_URL, url), body);
|
||||
}
|
||||
async function putEs(url, body) {
|
||||
return putURL(path.join(ES_URL, url), body);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function getKbn(url) {
|
||||
return getURL(path.join(KB_URL, url));
|
||||
}
|
||||
async function postKbn(url, body) {
|
||||
return postURL(path.join(KB_URL, url), body);
|
||||
}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function putKbn(url, body) {
|
||||
return putURL(path.join(KB_URL, url), body);
|
||||
}
|
||||
|
||||
async function getURL(url) {
|
||||
return sendURL(url, 'GET');
|
||||
}
|
||||
async function postURL(url, body) {
|
||||
return sendURL(url, 'POST', body);
|
||||
}
|
||||
async function putURL(url, body) {
|
||||
return sendURL(url, 'PUT', body);
|
||||
}
|
||||
|
||||
async function sendURL(urlWithPass, method, body) {
|
||||
const purl = new URL(urlWithPass);
|
||||
const userPass = `${purl.username}:${purl.password}`;
|
||||
const userPassEn = Buffer.from(userPass).toString('base64');
|
||||
const auth = `Basic ${userPassEn}`;
|
||||
const url = `${purl.origin}${purl.pathname}${purl.search}`;
|
||||
const headers = {
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': 'foo',
|
||||
authorization: auth,
|
||||
};
|
||||
|
||||
const fetchOptions = { method, headers };
|
||||
if (body) fetchOptions.body = JSON.stringify(body);
|
||||
|
||||
if (purl.protocol === 'https:') {
|
||||
fetchOptions.agent = new https.Agent({ rejectUnauthorized: false });
|
||||
}
|
||||
|
||||
// console.log(`fetch("${url}", ${JSON.stringify(fetchOptions, null, 4)}`)
|
||||
const response = await fetch(url, fetchOptions);
|
||||
const object = await response.json();
|
||||
// console.log(`fetch(...): ${JSON.stringify(object, null, 4)}`)
|
||||
return object;
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
"server/**/*.json",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/alerting-state-types",
|
||||
"@kbn/core",
|
||||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/config-schema",
|
||||
|
@ -17,7 +18,7 @@
|
|||
"@kbn/safer-lodash-set",
|
||||
"@kbn/es-types",
|
||||
"@kbn/apm-utils",
|
||||
"@kbn/core-saved-objects-common",
|
||||
"@kbn/core-saved-objects-common"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -19,6 +19,12 @@ import {
|
|||
} from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
const InstanceActions = new Set<string | undefined>([
|
||||
'new-instance',
|
||||
'active-instance',
|
||||
'recovered-instance',
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function eventLogTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -1096,6 +1102,84 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
|
|||
.map((event) => event?.kibana?.alert?.flapping);
|
||||
expect(flapping).to.eql([false, false, false, false, false, true, true, true]);
|
||||
});
|
||||
|
||||
it('should generate expected uuids for events for flapping alerts that go active while flapping and eventually recover', async () => {
|
||||
await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_flapping`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth('superuser', 'superuser')
|
||||
.send({
|
||||
enabled: true,
|
||||
look_back_window: 6,
|
||||
status_change_threshold: 4,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// flap and then recover, then active again
|
||||
const instance = [true, false, true, false, true].concat(
|
||||
...new Array(6).fill(false),
|
||||
true
|
||||
);
|
||||
const pattern = { instance };
|
||||
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
rule_type_id: 'test.patternFiring',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
params: { pattern },
|
||||
actions: [],
|
||||
notify_when: RuleNotifyWhen.CHANGE,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
const alertId = response.body.id;
|
||||
objectRemover.add(space.id, alertId, 'rule', 'alerting');
|
||||
|
||||
// get the events we're expecting
|
||||
const events = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: space.id,
|
||||
type: 'alert',
|
||||
id: alertId,
|
||||
provider: 'alerting',
|
||||
actions: new Map([
|
||||
['execute', { gte: 10 }],
|
||||
['new-instance', { gte: 4 }],
|
||||
['active-instance', { gte: 3 }],
|
||||
['recovered-instance', { gte: 3 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
let currentUuid: string | undefined;
|
||||
const seenUuids = new Set<string>();
|
||||
for (const event of events) {
|
||||
const action = event?.event?.action;
|
||||
const uuid = event?.kibana?.alert?.uuid;
|
||||
|
||||
if (!InstanceActions.has(action)) continue;
|
||||
|
||||
expect(uuid).to.be.ok();
|
||||
|
||||
if (action === 'new-instance') {
|
||||
expect(currentUuid).to.be(undefined);
|
||||
expect(seenUuids.has(uuid!)).to.be(false);
|
||||
currentUuid = uuid;
|
||||
seenUuids.add(uuid!);
|
||||
} else if (action === 'active-instance') {
|
||||
expect(uuid).to.be(currentUuid);
|
||||
} else if (action === 'recovered-instance') {
|
||||
expect(uuid).to.be(currentUuid);
|
||||
currentUuid = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1181,6 +1265,12 @@ export function validateEvent(event: IValidatedEvent, params: ValidateEventLogPa
|
|||
expect(event?.kibana?.alerting?.instance_id).to.be(instanceId);
|
||||
}
|
||||
|
||||
if (InstanceActions.has(event?.event?.action)) {
|
||||
expect(typeof event?.kibana?.alert?.uuid).to.be('string');
|
||||
} else {
|
||||
expect(event?.kibana?.alert?.uuid).to.be(undefined);
|
||||
}
|
||||
|
||||
if (reason) {
|
||||
expect(event?.event?.reason).to.be(reason);
|
||||
}
|
||||
|
|
|
@ -233,7 +233,12 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo
|
|||
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}/_alert_summary`
|
||||
);
|
||||
|
||||
const actualAlerts = response.body.alerts;
|
||||
const actualAlerts = checkAndCleanActualAlerts(response.body.alerts, [
|
||||
'alertA',
|
||||
'alertB',
|
||||
'alertC',
|
||||
]);
|
||||
|
||||
const expectedAlerts = {
|
||||
alertA: {
|
||||
status: 'Active',
|
||||
|
@ -292,7 +297,11 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo
|
|||
`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdRule.id}/_instance_summary`
|
||||
);
|
||||
|
||||
const actualAlerts = response.body.instances;
|
||||
const actualAlerts = checkAndCleanActualAlerts(response.body.instances, [
|
||||
'alertA',
|
||||
'alertB',
|
||||
'alertC',
|
||||
]);
|
||||
const expectedAlerts = {
|
||||
alertA: {
|
||||
status: 'Active',
|
||||
|
@ -337,3 +346,25 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkAndCleanActualAlerts(actualAlerts: any, idsWithUuids: string[]) {
|
||||
const uuids = new Set<string>();
|
||||
const idsWithUuidsSet = new Set(idsWithUuids);
|
||||
|
||||
for (const alertId of Object.keys(actualAlerts)) {
|
||||
const alert = actualAlerts[alertId];
|
||||
|
||||
if (idsWithUuidsSet.has(alertId)) {
|
||||
const uuid = alert?.uuid;
|
||||
expect(typeof uuid).to.be('string');
|
||||
|
||||
if (uuid) {
|
||||
expect(uuids.has(uuid)).to.be(false);
|
||||
uuids.add(uuid);
|
||||
delete actualAlerts[alertId].uuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return actualAlerts;
|
||||
}
|
||||
|
|
|
@ -88,29 +88,41 @@ export default function eventLogAlertTests({ getService }: FtrProviderContext) {
|
|||
// Ensure every execution actually had a unique id from the others
|
||||
expect(totalUniqueExecutionIds.size).to.equal(totalExecutionEventCount);
|
||||
|
||||
const allAlertUuids = new Set<string>();
|
||||
const currentAlertSpan: {
|
||||
alertId?: string;
|
||||
start?: string;
|
||||
durationToDate?: string;
|
||||
uuid?: string;
|
||||
} = {};
|
||||
const flapping = [];
|
||||
for (let i = 0; i < instanceEvents.length; ++i) {
|
||||
expect(typeof instanceEvents[i]?.kibana?.alert?.uuid).to.be('string');
|
||||
const uuid = instanceEvents[i]?.kibana?.alert?.uuid!;
|
||||
|
||||
flapping.push(instanceEvents[i]?.kibana?.alert?.flapping);
|
||||
switch (instanceEvents[i]?.event?.action) {
|
||||
case 'new-instance':
|
||||
expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance');
|
||||
// a new alert should generate a unique UUID for the duration of its activeness
|
||||
expect(instanceEvents[i]?.kibana?.alert?.flapping).to.equal(false);
|
||||
expect(instanceEvents[i]?.event?.end).to.be(undefined);
|
||||
|
||||
// uuid should be unique for new instances, reused for active/recovered
|
||||
expect(currentAlertSpan.uuid).to.be(undefined);
|
||||
expect(allAlertUuids.has(uuid)).to.be(false);
|
||||
allAlertUuids.add(uuid);
|
||||
|
||||
currentAlertSpan.alertId = instanceEvents[i]?.kibana?.alerting?.instance_id;
|
||||
currentAlertSpan.start = instanceEvents[i]?.event?.start;
|
||||
currentAlertSpan.durationToDate = `${instanceEvents[i]?.event?.duration}`;
|
||||
currentAlertSpan.uuid = uuid;
|
||||
break;
|
||||
|
||||
case 'active-instance':
|
||||
expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance');
|
||||
expect(instanceEvents[i]?.event?.start).to.equal(currentAlertSpan.start);
|
||||
expect(instanceEvents[i]?.event?.end).to.be(undefined);
|
||||
expect(instanceEvents[i]?.kibana?.alert?.uuid).to.be(currentAlertSpan.uuid);
|
||||
|
||||
if (instanceEvents[i]?.event?.duration! !== '0') {
|
||||
expect(
|
||||
|
@ -125,10 +137,12 @@ export default function eventLogAlertTests({ getService }: FtrProviderContext) {
|
|||
expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance');
|
||||
expect(instanceEvents[i]?.event?.start).to.equal(currentAlertSpan.start);
|
||||
expect(instanceEvents[i]?.event?.end).not.to.be(undefined);
|
||||
expect(instanceEvents[i]?.kibana?.alert?.uuid).to.be(currentAlertSpan.uuid);
|
||||
expect(
|
||||
new Date(instanceEvents[i]?.event?.end!).valueOf() -
|
||||
new Date(instanceEvents[i]?.event?.start!).valueOf()
|
||||
).to.equal(nanosToMillis(instanceEvents[i]?.event?.duration!));
|
||||
currentAlertSpan.uuid = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -214,3 +214,75 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "task:d0487a50-c7f1-11ed-aefe-691acd8d4e25",
|
||||
"index": ".kibana_task_manager_1",
|
||||
"source": {
|
||||
"migrationVersion": {
|
||||
"task": "8.5.0"
|
||||
},
|
||||
"task": {
|
||||
"retryAt": null,
|
||||
"runAt": "2023-03-21T14:08:29.777Z",
|
||||
"startedAt": null,
|
||||
"params": "{\"alertId\":\"d0487a50-c7f1-11ed-aefe-691acd8d4e25\",\"spaceId\":\"default\",\"consumer\":\"alerts\"}",
|
||||
"ownerId": null,
|
||||
"enabled": true,
|
||||
"schedule": {
|
||||
"interval": "3s"
|
||||
},
|
||||
"taskType": "alerting:.index-threshold",
|
||||
"scope": [
|
||||
"alerting"
|
||||
],
|
||||
"traceparent": "",
|
||||
"state": "{\"alertTypeState\":{},\"alertInstances\":{\"host-C\":{\"state\":{\"start\":\"2023-03-21T14:08:20.886Z\",\"duration\":\"6010000000\"},\"meta\":{\"flappingHistory\":[true,false],\"flapping\":false,\"pendingRecoveredCount\":0,\"lastScheduledActions\":{\"group\":\"threshold met\",\"date\":\"2023-03-21T14:08:26.903Z\"}}}},\"alertRecoveredInstances\":{\"host-A\":{\"meta\":{\"flappingHistory\":[true,false,true],\"flapping\":false}},\"host-B\":{\"meta\":{\"flappingHistory\":[true,false,true],\"flapping\":false}}},\"summaryActions\":{},\"previousStartedAt\":\"2023-03-21T14:08:26.777Z\"}",
|
||||
"scheduledAt": "2023-03-21T14:08:23.770Z",
|
||||
"attempts": 0,
|
||||
"status": "idle"
|
||||
},
|
||||
"references": [],
|
||||
"updated_at": "2023-03-21T14:08:27.022Z",
|
||||
"type": "task"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "task:cf0ced10-c7f1-11ed-aefe-691acd8d4e25",
|
||||
"index": ".kibana_task_manager_1",
|
||||
"source": {
|
||||
"migrationVersion": {
|
||||
"task": "8.5.0"
|
||||
},
|
||||
"task": {
|
||||
"retryAt": null,
|
||||
"runAt": "2023-03-21T14:08:29.777Z",
|
||||
"startedAt": null,
|
||||
"params": "{\"alertId\":\"cf0ced10-c7f1-11ed-aefe-691acd8d4e25\",\"spaceId\":\"default\",\"consumer\":\"infrastructure\"}",
|
||||
"ownerId": null,
|
||||
"enabled": true,
|
||||
"schedule": {
|
||||
"interval": "3s"
|
||||
},
|
||||
"taskType": "alerting:metrics.alert.threshold",
|
||||
"scope": [
|
||||
"alerting"
|
||||
],
|
||||
"traceparent": "",
|
||||
"state": "{\"alertTypeState\":{\"wrapped\":{\"lastRunTimestamp\":1679407706777,\"missingGroups\":[],\"groupBy\":[\"network.name\"]},\"trackedAlerts\":{\"host-C\":{\"alertId\":\"host-C\",\"alertUuid\":\"34f86489-cf44-4760-a9df-d1a2166230d9\",\"started\":\"2023-03-21T14:08:20.770Z\",\"flappingHistory\":[true,false],\"flapping\":false,\"pendingRecoveredCount\":0}},\"trackedAlertsRecovered\":{\"host-A\":{\"alertId\":\"host-A\",\"alertUuid\":\"d3a6a7d0-71d3-475d-a8db-5ea7a03462d5\",\"started\":\"2023-03-21T14:08:11.767Z\",\"flappingHistory\":[true,false,false,true],\"flapping\":false,\"pendingRecoveredCount\":0},\"host-B\":{\"alertId\":\"host-B\",\"alertUuid\":\"41d17c71-12b6-4bbc-8fd7-1eee8498c17f\",\"started\":\"2023-03-21T14:08:11.767Z\",\"flappingHistory\":[true,false,false,true],\"flapping\":false,\"pendingRecoveredCount\":0}}},\"alertInstances\":{\"host-C\":{\"state\":{\"start\":\"2023-03-21T14:08:21.701Z\",\"duration\":\"6049000000\"},\"meta\":{\"flappingHistory\":[true,false],\"flapping\":false,\"pendingRecoveredCount\":0,\"lastScheduledActions\":{\"group\":\"metrics.threshold.fired\",\"date\":\"2023-03-21T14:08:27.755Z\"}}}},\"alertRecoveredInstances\":{\"host-A\":{\"meta\":{\"flappingHistory\":[true,false,false,true],\"flapping\":false}},\"host-B\":{\"meta\":{\"flappingHistory\":[true,false,false,true],\"flapping\":false}}},\"summaryActions\":{},\"previousStartedAt\":\"2023-03-21T14:08:26.777Z\"}",
|
||||
"scheduledAt": "2023-03-21T14:08:23.770Z",
|
||||
"attempts": 0,
|
||||
"status": "idle"
|
||||
},
|
||||
"references": [],
|
||||
"updated_at": "2023-03-21T14:08:27.877Z",
|
||||
"type": "task"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -161,21 +161,24 @@
|
|||
},
|
||||
"task": {
|
||||
"properties": {
|
||||
"attempts": {
|
||||
"type": "integer"
|
||||
},
|
||||
"ownerId": {
|
||||
"taskType": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"params": {
|
||||
"type": "text"
|
||||
},
|
||||
"retryAt": {
|
||||
"scheduledAt": {
|
||||
"type": "date"
|
||||
},
|
||||
"runAt": {
|
||||
"type": "date"
|
||||
},
|
||||
"startedAt": {
|
||||
"type": "date"
|
||||
},
|
||||
"retryAt": {
|
||||
"type": "date"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"schedule": {
|
||||
"properties": {
|
||||
"interval": {
|
||||
|
@ -183,26 +186,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"scheduledAt": {
|
||||
"type": "date"
|
||||
},
|
||||
"scope": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"startedAt": {
|
||||
"type": "date"
|
||||
},
|
||||
"state": {
|
||||
"type": "text"
|
||||
"attempts": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"taskType": {
|
||||
"type": "keyword"
|
||||
"traceparent": {
|
||||
"type": "text"
|
||||
},
|
||||
"params": {
|
||||
"type": "text"
|
||||
},
|
||||
"state": {
|
||||
"type": "text"
|
||||
},
|
||||
"user": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"scope": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -10,10 +10,12 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
|||
import type { TransportResult } from '@elastic/elasticsearch';
|
||||
import {
|
||||
ConcreteTaskInstance,
|
||||
SerializedConcreteTaskInstance,
|
||||
TaskInstanceWithDeprecatedFields,
|
||||
TaskStatus,
|
||||
} from '@kbn/task-manager-plugin/server/task';
|
||||
import { SavedObjectsUtils } from '@kbn/core/server';
|
||||
import type { RuleTaskState, WrappedLifecycleRuleState } from '@kbn/alerting-state-types';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
export default function createGetTests({ getService }: FtrProviderContext) {
|
||||
|
@ -190,5 +192,85 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
expect(task._source?.task.enabled).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('8.8.0', async () => {
|
||||
it('adds UUIDs to all alerts', async () => {
|
||||
const response = await es.search<{ task: SerializedConcreteTaskInstance }>(
|
||||
{
|
||||
index: '.kibana_task_manager',
|
||||
size: 100,
|
||||
body: { query: { match_all: {} } },
|
||||
},
|
||||
{ meta: true }
|
||||
);
|
||||
expect(response.statusCode).to.eql(200);
|
||||
const tasks = response.body.hits.hits;
|
||||
tasks.forEach((task) => {
|
||||
const stateString = task._source?.task.state;
|
||||
expect(stateString).to.be.ok();
|
||||
const state: RuleTaskState = JSON.parse(stateString!);
|
||||
const uuids = new Set<string>();
|
||||
|
||||
for (const alert of Object.values(state.alertInstances || {})) {
|
||||
const uuid = alert?.meta?.uuid || 'uuid-is-missing';
|
||||
expect(uuid).to.match(/^.{8}-.{4}-.{4}-.{4}-.{12}$/);
|
||||
expect(uuids.has(uuid)).to.be(false);
|
||||
uuids.add(uuid);
|
||||
}
|
||||
|
||||
for (const alert of Object.values(state.alertRecoveredInstances || {})) {
|
||||
const uuid = alert?.meta?.uuid || 'uuid-is-missing';
|
||||
expect(uuid).to.match(/^.{8}-.{4}-.{4}-.{4}-.{12}$/);
|
||||
expect(uuids.has(uuid)).to.be(false);
|
||||
uuids.add(uuid);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('copies UUIDs from rule registry wrapper to alerting framework', async () => {
|
||||
const response = await es.search<{ task: SerializedConcreteTaskInstance }>(
|
||||
{
|
||||
index: '.kibana_task_manager',
|
||||
size: 100,
|
||||
body: { query: { match_all: {} } },
|
||||
},
|
||||
{ meta: true }
|
||||
);
|
||||
expect(response.statusCode).to.eql(200);
|
||||
const tasks = response.body.hits.hits;
|
||||
tasks.forEach((task) => {
|
||||
const stateString = task._source?.task.state;
|
||||
expect(stateString).to.be.ok();
|
||||
|
||||
const state: RuleTaskState = JSON.parse(stateString!);
|
||||
if (!state?.alertTypeState?.wrapped) return;
|
||||
|
||||
const wrappedUUIDs = new Map<string, string>();
|
||||
const wrappedState = state.alertTypeState as WrappedLifecycleRuleState<any>;
|
||||
|
||||
for (const alert of Object.values(wrappedState.trackedAlerts || {})) {
|
||||
const id = alert.alertId;
|
||||
const uuid = alert.alertUuid;
|
||||
wrappedUUIDs.set(id, uuid);
|
||||
}
|
||||
|
||||
for (const alert of Object.values(wrappedState.trackedAlertsRecovered || {})) {
|
||||
const id = alert.alertId;
|
||||
const uuid = alert.alertUuid;
|
||||
wrappedUUIDs.set(id, uuid);
|
||||
}
|
||||
|
||||
for (const [id, alert] of Object.entries(state.alertInstances || {})) {
|
||||
const uuid = alert?.meta?.uuid || 'uuid-is-missing';
|
||||
expect(uuid).to.be(wrappedUUIDs.get(id));
|
||||
}
|
||||
|
||||
for (const [id, alert] of Object.entries(state.alertRecoveredInstances || {})) {
|
||||
const uuid = alert?.meta?.uuid || 'uuid-is-missing';
|
||||
expect(uuid).to.be(wrappedUUIDs.get(id));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export const cleanupTargetIndices = async (getService: GetService, user: User, s
|
|||
const aliasMap = await es.indices.getAlias({
|
||||
name: targetIndices,
|
||||
allow_no_indices: true,
|
||||
expand_wildcards: 'open',
|
||||
expand_wildcards: 'all',
|
||||
});
|
||||
const indices = Object.keys(aliasMap);
|
||||
expect(indices.length > 0).to.be(true);
|
||||
|
|
|
@ -11,4 +11,6 @@ export * from './create_transaction_metric';
|
|||
export * from './get_alerts_target_indices';
|
||||
export * from './wait_until_next_execution';
|
||||
export * from './cleanup_target_indices';
|
||||
export * from './cleanup_registry_indices';
|
||||
export * from './delete_alert';
|
||||
export * from './mock_alert_factory';
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
// implements a minimal mock alert factory used by a few tests
|
||||
|
||||
let UUIDCounter = 1;
|
||||
|
||||
class MockAlert {
|
||||
id: string;
|
||||
|
||||
constructor(alertId: string) {
|
||||
this.id = alertId;
|
||||
}
|
||||
getUuid() {
|
||||
return `uuid-${UUIDCounter++}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getMockAlertFactory() {
|
||||
return {
|
||||
create(alertId: string) {
|
||||
return new MockAlert(alertId);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -17,6 +17,9 @@ Object {
|
|||
"kibana.alert.evaluation.value": Array [
|
||||
50,
|
||||
],
|
||||
"kibana.alert.flapping": Array [
|
||||
false,
|
||||
],
|
||||
"kibana.alert.instance.id": Array [
|
||||
"apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED",
|
||||
],
|
||||
|
@ -32,9 +35,22 @@ Object {
|
|||
"kibana.alert.rule.name": Array [
|
||||
"Failed transaction rate threshold | opbeans-go",
|
||||
],
|
||||
"kibana.alert.rule.parameters": Array [
|
||||
Object {
|
||||
"environment": "ENVIRONMENT_ALL",
|
||||
"serviceName": "opbeans-go",
|
||||
"threshold": 30,
|
||||
"transactionType": "request",
|
||||
"windowSize": 5,
|
||||
"windowUnit": "m",
|
||||
},
|
||||
],
|
||||
"kibana.alert.rule.producer": Array [
|
||||
"apm",
|
||||
],
|
||||
"kibana.alert.rule.revision": Array [
|
||||
0,
|
||||
],
|
||||
"kibana.alert.rule.rule_type_id": Array [
|
||||
"apm.transaction_error_rate",
|
||||
],
|
||||
|
@ -81,6 +97,9 @@ Object {
|
|||
"kibana.alert.evaluation.value": Array [
|
||||
50,
|
||||
],
|
||||
"kibana.alert.flapping": Array [
|
||||
false,
|
||||
],
|
||||
"kibana.alert.instance.id": Array [
|
||||
"apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED",
|
||||
],
|
||||
|
@ -96,9 +115,22 @@ Object {
|
|||
"kibana.alert.rule.name": Array [
|
||||
"Failed transaction rate threshold | opbeans-go",
|
||||
],
|
||||
"kibana.alert.rule.parameters": Array [
|
||||
Object {
|
||||
"environment": "ENVIRONMENT_ALL",
|
||||
"serviceName": "opbeans-go",
|
||||
"threshold": 30,
|
||||
"transactionType": "request",
|
||||
"windowSize": 5,
|
||||
"windowUnit": "m",
|
||||
},
|
||||
],
|
||||
"kibana.alert.rule.producer": Array [
|
||||
"apm",
|
||||
],
|
||||
"kibana.alert.rule.revision": Array [
|
||||
0,
|
||||
],
|
||||
"kibana.alert.rule.rule_type_id": Array [
|
||||
"apm.transaction_error_rate",
|
||||
],
|
||||
|
|
|
@ -9,16 +9,20 @@ import expect from '@kbn/expect';
|
|||
import {
|
||||
ALERT_DURATION,
|
||||
ALERT_END,
|
||||
ALERT_INSTANCE_ID,
|
||||
ALERT_RULE_EXECUTION_UUID,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_START,
|
||||
ALERT_STATUS,
|
||||
ALERT_TIME_RANGE,
|
||||
ALERT_UUID,
|
||||
EVENT_KIND,
|
||||
VERSION,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { omit } from 'lodash';
|
||||
import { Rule } from '@kbn/alerting-plugin/common';
|
||||
import { SerializedConcreteTaskInstance } from '@kbn/task-manager-plugin/server/task';
|
||||
import type { RuleTaskState } from '@kbn/alerting-state-types';
|
||||
import type { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import {
|
||||
getAlertsTargetIndices,
|
||||
|
@ -32,6 +36,7 @@ import {
|
|||
import { AlertDef, AlertParams } from '../../../common/types';
|
||||
import { APM_METRIC_INDEX_NAME } from '../../../common/constants';
|
||||
import { obsOnly } from '../../../common/lib/authentication/users';
|
||||
import { getEventLog } from '../../../../alerting_api_integration/common/lib/get_event_log';
|
||||
|
||||
const SPACE_ID = 'space1';
|
||||
|
||||
|
@ -39,8 +44,7 @@ const SPACE_ID = 'space1';
|
|||
export default function registryRulesApiTest({ getService }: FtrProviderContext) {
|
||||
const es = getService('es');
|
||||
|
||||
// FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/125851
|
||||
describe.skip('Rule Registry API', () => {
|
||||
describe('Rule Registry API', async () => {
|
||||
describe('with write permissions', () => {
|
||||
it('does not bootstrap indices on plugin startup', async () => {
|
||||
const { body: targetIndices } = await getAlertsTargetIndices(getService, obsOnly, SPACE_ID);
|
||||
|
@ -119,7 +123,7 @@ export default function registryRulesApiTest({ getService }: FtrProviderContext)
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(res).to.be.empty();
|
||||
expect(res.hits.hits).to.be.empty();
|
||||
} catch (exc) {
|
||||
expect(exc.message).contain('index_not_found_exception');
|
||||
}
|
||||
|
@ -151,7 +155,7 @@ export default function registryRulesApiTest({ getService }: FtrProviderContext)
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(res).to.be.empty();
|
||||
expect(res.hits.hits).to.be.empty();
|
||||
} catch (exc) {
|
||||
expect(exc.message).contain('index_not_found_exception');
|
||||
}
|
||||
|
@ -195,9 +199,25 @@ export default function registryRulesApiTest({ getService }: FtrProviderContext)
|
|||
ALERT_UUID,
|
||||
ALERT_RULE_EXECUTION_UUID,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_TIME_RANGE,
|
||||
VERSION,
|
||||
];
|
||||
|
||||
const alertInstanceId = alertEvent[ALERT_INSTANCE_ID]?.[0];
|
||||
const alertUuid = alertEvent[ALERT_UUID]?.[0];
|
||||
const executionUuid = alertEvent[ALERT_RULE_EXECUTION_UUID]?.[0];
|
||||
expect(typeof alertUuid).to.be('string');
|
||||
expect(typeof executionUuid).to.be('string');
|
||||
|
||||
await checkEventLogAlertUuids(
|
||||
getService,
|
||||
SPACE_ID,
|
||||
createResponse.alert.id,
|
||||
alertInstanceId,
|
||||
alertUuid,
|
||||
executionUuid
|
||||
);
|
||||
|
||||
const toCompare = omit(alertEvent, exclude);
|
||||
|
||||
expectSnapshot(toCompare).toMatch();
|
||||
|
@ -259,3 +279,54 @@ export default function registryRulesApiTest({ getService }: FtrProviderContext)
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function checkEventLogAlertUuids(
|
||||
getService: FtrProviderContext['getService'],
|
||||
spaceId: string,
|
||||
ruleId: string,
|
||||
alertInstanceId: string,
|
||||
alertUuid: string,
|
||||
executionUuid: string
|
||||
) {
|
||||
const es = getService('es');
|
||||
const retry = getService('retry');
|
||||
|
||||
const docs: Awaited<ReturnType<typeof getEventLog>> = [];
|
||||
await retry.waitFor('getting event log docs', async () => {
|
||||
docs.push(...(await getEventLogDocs()));
|
||||
return docs.length > 0;
|
||||
});
|
||||
|
||||
expect(docs.length).to.be.greaterThan(0);
|
||||
for (const doc of docs) {
|
||||
expect(doc?.kibana?.alert?.uuid).to.be(alertUuid);
|
||||
}
|
||||
|
||||
// check that the task doc has the same UUID
|
||||
const taskDoc = await es.get<{ task: SerializedConcreteTaskInstance }>({
|
||||
index: '.kibana_task_manager',
|
||||
id: `task:${ruleId}`,
|
||||
});
|
||||
|
||||
const ruleStateString = taskDoc._source?.task.state || 'task-state-is-missing';
|
||||
const ruleState: RuleTaskState = JSON.parse(ruleStateString);
|
||||
if (ruleState.alertInstances?.[alertInstanceId]) {
|
||||
expect(ruleState.alertInstances[alertInstanceId].meta?.uuid).to.be(alertUuid);
|
||||
} else if (ruleState.alertRecoveredInstances?.[alertInstanceId]) {
|
||||
expect(ruleState.alertRecoveredInstances[alertInstanceId].meta?.uuid).to.be(alertUuid);
|
||||
} else {
|
||||
expect(false).to.be('alert instance not found in task doc');
|
||||
}
|
||||
|
||||
function getEventLogDocs() {
|
||||
return getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
type: 'alert',
|
||||
id: ruleId,
|
||||
provider: 'alerting',
|
||||
actions: new Map([['active-instance', { equal: 1 }]]),
|
||||
filter: `kibana.alert.rule.execution.uuid: ${executionUuid}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ import {
|
|||
MockAlertState,
|
||||
MockAllowedActionGroups,
|
||||
} from '../../../common/types';
|
||||
import { cleanupRegistryIndices } from '../../../common/lib/helpers/cleanup_registry_indices';
|
||||
import { cleanupRegistryIndices, getMockAlertFactory } from '../../../common/lib/helpers';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createGetSummarizedAlertsTest({ getService }: FtrProviderContext) {
|
||||
|
@ -173,7 +173,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide
|
|||
producer: 'observability.test',
|
||||
},
|
||||
services: {
|
||||
alertFactory: { create: sinon.stub() },
|
||||
alertFactory: getMockAlertFactory(),
|
||||
shouldWriteAlerts: sinon.stub().returns(true),
|
||||
},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
|
@ -332,7 +332,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide
|
|||
producer: 'observability.test',
|
||||
},
|
||||
services: {
|
||||
alertFactory: { create: sinon.stub() },
|
||||
alertFactory: getMockAlertFactory(),
|
||||
shouldWriteAlerts: sinon.stub().returns(true),
|
||||
},
|
||||
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// WARNING: This test running in Function Test Runner is building a live
|
||||
// LifecycleRuleExecutor, feeding it some mock data, but letting it write
|
||||
// it's various alerts to indices. I suspect it's quite fragile, and I
|
||||
// added this comment to fix some fragility in the way the alert factory
|
||||
// was built. I suspect it will suffer more such things in the future.
|
||||
// I fixed this as a drive-by, but opened an issue to do something later,
|
||||
// if needed: https://github.com/elastic/kibana/issues/144557
|
||||
|
||||
import { type Subject, ReplaySubject } from 'rxjs';
|
||||
import type { ElasticsearchClient, Logger, LogMeta } from '@kbn/core/server';
|
||||
import sinon from 'sinon';
|
||||
|
@ -29,7 +37,7 @@ import {
|
|||
MockAlertState,
|
||||
MockAllowedActionGroups,
|
||||
} from '../../../common/types';
|
||||
import { cleanupRegistryIndices } from '../../../common/lib/helpers/cleanup_registry_indices';
|
||||
import { cleanupRegistryIndices, getMockAlertFactory } from '../../../common/lib/helpers';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createLifecycleExecutorApiTest({ getService }: FtrProviderContext) {
|
||||
|
@ -57,8 +65,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid
|
|||
return Promise.resolve(client);
|
||||
};
|
||||
|
||||
// FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/125851
|
||||
describe.skip('createLifecycleExecutor', () => {
|
||||
describe('createLifecycleExecutor', () => {
|
||||
let ruleDataClient: IRuleDataClient;
|
||||
let pluginStop$: Subject<void>;
|
||||
|
||||
|
@ -170,13 +177,15 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid
|
|||
return Promise.resolve({ state });
|
||||
});
|
||||
|
||||
const ruleId = 'rule-id';
|
||||
// Create the options with the minimal amount of values to test the lifecycle executor
|
||||
const options = {
|
||||
alertId: id,
|
||||
alertId: ruleId,
|
||||
spaceId: 'default',
|
||||
tags: ['test'],
|
||||
startedAt: new Date(),
|
||||
rule: {
|
||||
id: ruleId,
|
||||
name: 'test rule',
|
||||
ruleTypeId: 'observability.test.fake',
|
||||
ruleTypeName: 'test',
|
||||
|
@ -184,9 +193,14 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid
|
|||
producer: 'observability.test',
|
||||
},
|
||||
services: {
|
||||
alertFactory: { create: sinon.stub() },
|
||||
alertFactory: getMockAlertFactory(),
|
||||
shouldWriteAlerts: sinon.stub().returns(true),
|
||||
},
|
||||
flappingSettings: {
|
||||
enabled: false,
|
||||
lookBackWindow: 20,
|
||||
statusChangeThreshold: 4,
|
||||
},
|
||||
} as unknown as RuleExecutorOptions<
|
||||
MockRuleParams,
|
||||
WrappedLifecycleRuleState<MockRuleState>,
|
||||
|
@ -205,6 +219,9 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid
|
|||
},
|
||||
});
|
||||
|
||||
const alertUuid = executorResult.state.trackedAlerts['host-01'].alertUuid;
|
||||
expect(alertUuid).to.be('uuid-1');
|
||||
|
||||
// We need to refresh the index so the data is available for the next call
|
||||
await es.indices.refresh({ index: `${ruleDataClient.indexName}*` });
|
||||
|
||||
|
|
|
@ -120,6 +120,7 @@
|
|||
"@kbn/files-plugin",
|
||||
"@kbn/shared-ux-file-types",
|
||||
"@kbn/securitysolution-io-ts-alerting-types",
|
||||
"@kbn/alerting-state-types",
|
||||
"@kbn/assetManager-plugin",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -2797,6 +2797,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/alerting-state-types@link:x-pack/packages/kbn-alerting-state-types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/alerts-as-data-utils@link:packages/kbn-alerts-as-data-utils":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue