Onboard alerting tasks to use stateSchemaByVersion for task state validation of the framework fields (#162425)

Resolves https://github.com/elastic/kibana/issues/159343

In this PR, I'm preparing the alerting rule task types for serverless by
defining an explicit task state schema for the framework level fields.
This schema is used to validate the task's state before saving but also
when reading. In the scenario an older Kibana node runs a task after a
newer Kibana node has stored additional task state, the unknown state
properties will be dropped. Additionally, this will prompt developers to
be aware that adding required fields to the task state is a breaking
change that must be handled with care. (see
https://github.com/elastic/kibana/issues/155764).

The PR also includes the following changes:
- Modifying the `@kbn/alerting-state-types` package so the types are
generated from `config-schema`, instead of `io-ts`. The schema is
re-used for exporting a new `stateSchemaByVersion` property.
- Removing `DateFromString` in favour of strings everywhere
(config-schema doesn't support this conversion)
- Add a v1 `up` migration that will ensure the types match the
TypeScript interface on any existing alerting task. The migration
assumes any type of data could exist and dropping parts that don't match
the type expectation. The TypeScript interface uses `schema.maybe()` in
a lot of places (as `io-ts` did), so safe if ever data gets dropped.
- Cleanup the `alerting/common/**` exports to reduce bundle size.
Because the `@kbn/alerting-state-types` package grew.
- Since the new TypeScript interfaces / types are `ReadOnly<...>`, I
created some `Mutable...` types for places that needed it (in order to
avoid code refactoring).

## To verify

Stack Monitoring:
- To make TypeScript happy with the new ReadOnly `RawAlertInstance`
type, I removed some redundant code and solved the issue.

Security Solution:
- Changes to the `alertInstanceFactoryStub` set the alert's date to a
`string` instead of a `Date` value. Note: The HTTP API response
converted `Date` objects to `string`, so the HTTP API response will look
the same with this change.

Response Ops:
- In a fresh Kibana install, create alerting rules and ensure they run.
- In a 8.9 version, create some rules, upgrade to this branch and ensure
they still run.
- Compare the `io-ts` definition with the new `config-schema`. They
should match 1:1.
- Look for ways the migration code could fail.

Note: The main changes are within the following areas:
- `x-pack/plugins/alerting/server/rule_type_registry.ts`
- `x-pack/packages/kbn-alerting-state-types/*`

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Mike Côté 2023-08-29 06:40:28 -04:00 committed by GitHub
parent 55fb29716e
commit 82f551c3d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 619 additions and 458 deletions

View file

@ -5,20 +5,22 @@
* 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 { AlertInstanceContext } from './src/alert_instance';
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';
export type { RuleTaskParams } from './src/rule_task_instance';
export { ActionsCompletion, ruleParamsSchema } from './src/rule_task_instance';
export type {
LatestTaskStateSchema as RuleTaskState,
MutableLatestTaskStateSchema as MutableRuleTaskState,
LatestRawAlertInstanceSchema as RawAlertInstance,
LatestAlertInstanceMetaSchema as AlertInstanceMeta,
MutableLatestAlertInstanceMetaSchema as MutableAlertInstanceMeta,
LatestAlertInstanceStateSchema as AlertInstanceState,
LatestThrottledActionSchema as ThrottledActions,
LatestLastScheduledActionsSchema as LastScheduledActions,
} from './src/task_state';
export { stateSchemaByVersion, emptyState as emptyTaskState } from './src/task_state';

View file

@ -6,50 +6,6 @@
*/
import * as t from 'io-ts';
import { DateFromString } from './date_from_string';
const actionSchema = t.type({
date: DateFromString,
});
export const throttledActionSchema = t.record(t.string, actionSchema);
export type ThrottledActions = t.TypeOf<typeof throttledActionSchema>;
const lastScheduledActionsSchema = t.intersection([
t.partial({
subgroup: t.string,
}),
t.type({
group: t.string,
date: DateFromString,
}),
t.partial({ actions: throttledActionSchema }),
]);
export type LastScheduledActions = t.TypeOf<typeof lastScheduledActionsSchema>;
const metaSchema = t.partial({
lastScheduledActions: lastScheduledActionsSchema,
// an array used to track changes in alert state, the order is based on the rule executions (oldest to most recent)
// true - alert has changed from active/recovered
// false - 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,
maintenanceWindowIds: t.array(t.string),
pendingRecoveredCount: t.number,
uuid: t.string,
});
export type AlertInstanceMeta = t.TypeOf<typeof metaSchema>;
const stateSchema = t.record(t.string, t.unknown);
export type AlertInstanceState = t.TypeOf<typeof stateSchema>;
const contextSchema = t.record(t.string, t.unknown);
export type AlertInstanceContext = t.TypeOf<typeof contextSchema>;
export const rawAlertInstance = t.partial({
state: stateSchema,
meta: metaSchema,
});
export type RawAlertInstance = t.TypeOf<typeof rawAlertInstance>;

View file

@ -1,29 +0,0 @@
/*
* 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 { DateFromString } from './date_from_string';
import { right, isLeft } from 'fp-ts/lib/Either';
describe('DateFromString', () => {
test('validated and parses a string into a Date', () => {
const date = new Date(1973, 10, 30);
expect(DateFromString.decode(date.toISOString())).toEqual(right(date));
});
test('validated and returns a failure for an actual Date', () => {
const date = new Date(1973, 10, 30);
expect(isLeft(DateFromString.decode(date))).toEqual(true);
});
test('validated and returns a failure for an invalid Date string', () => {
expect(isLeft(DateFromString.decode('1234-23-45'))).toEqual(true);
});
test('validated and returns a failure for a null value', () => {
expect(isLeft(DateFromString.decode(null))).toEqual(true);
});
});

View file

@ -1,27 +0,0 @@
/*
* 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';
import { either } from 'fp-ts/lib/Either';
// represents a Date from an ISO string
export const DateFromString = new t.Type<Date, string, unknown>(
'DateFromString',
// detect the type
(value): value is Date => value instanceof Date,
(valueToDecode, context) =>
either.chain(
// validate this is a string
t.string.validate(valueToDecode, context),
// decode
(value) => {
const decoded = new Date(value);
return isNaN(decoded.getTime()) ? t.failure(valueToDecode, context) : t.success(decoded);
}
),
(valueToEncode) => valueToEncode.toISOString()
);

View file

@ -6,27 +6,12 @@
*/
import * as t from 'io-ts';
import { throttledActionSchema, rawAlertInstance } from './alert_instance';
import { DateFromString } from './date_from_string';
export enum ActionsCompletion {
COMPLETE = 'complete',
PARTIAL = 'partial',
}
export const ruleStateSchema = t.partial({
alertTypeState: t.record(t.string, t.unknown),
// tracks the active alerts
alertInstances: t.record(t.string, rawAlertInstance),
// tracks the recovered alerts for flapping purposes
alertRecoveredInstances: t.record(t.string, rawAlertInstance),
previousStartedAt: t.union([t.null, DateFromString]),
summaryActions: throttledActionSchema,
});
// This is serialized in the rule task document
export type RuleTaskState = t.TypeOf<typeof ruleStateSchema>;
export const ruleParamsSchema = t.intersection([
t.type({
alertId: t.string,

View file

@ -0,0 +1,40 @@
/*
* 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 { type TypeOf } from '@kbn/config-schema';
import * as v1 from './v1';
export const stateSchemaByVersion = {
1: v1.versionDefinition,
};
const latest = v1;
/**
* WARNING: Do not modify the code below when doing a new version.
* Update the "latest" variable instead.
*/
const latestTaskStateSchema = latest.versionDefinition.schema;
export type LatestTaskStateSchema = TypeOf<typeof latestTaskStateSchema>;
export type LatestRawAlertInstanceSchema = TypeOf<typeof latest.rawAlertInstanceSchema>;
export type LatestAlertInstanceMetaSchema = TypeOf<typeof latest.metaSchema>;
export type LatestAlertInstanceStateSchema = TypeOf<typeof latest.alertStateSchema>;
export type LatestThrottledActionSchema = TypeOf<typeof latest.throttledActionSchema>;
export type LatestLastScheduledActionsSchema = TypeOf<typeof latest.lastScheduledActionsSchema>;
export const emptyState: LatestTaskStateSchema = {
alertTypeState: {},
alertInstances: {},
alertRecoveredInstances: {},
previousStartedAt: null,
summaryActions: {},
};
type Mutable<T> = {
-readonly [k in keyof T]: Mutable<T[k]>;
};
export type MutableLatestTaskStateSchema = Mutable<LatestTaskStateSchema>;
export type MutableLatestAlertInstanceMetaSchema = Mutable<LatestAlertInstanceMetaSchema>;

View file

@ -0,0 +1,32 @@
/*
* 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 { isPlainObject } from 'lodash';
export function isJSONObject(obj: unknown): obj is Record<string, unknown> {
return isPlainObject(obj);
}
export function isString(value: unknown): value is string {
return typeof value === 'string';
}
export function isBoolean(value: unknown): value is boolean {
return typeof value === 'boolean';
}
export function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
export function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
export function isBooleanArray(value: unknown): value is boolean[] {
return Array.isArray(value) && value.every((item) => typeof item === 'boolean');
}

View file

@ -0,0 +1,23 @@
/*
* 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 { upMigration } from './migration';
import { versionSchema } from './schema';
export {
versionSchema,
throttledActionSchema,
rawAlertInstanceSchema,
metaSchema,
alertStateSchema,
lastScheduledActionsSchema,
} from './schema';
export const versionDefinition = {
up: upMigration,
schema: versionSchema,
};

View file

@ -0,0 +1,225 @@
/*
* 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 {
migrateThrottledActions,
migrateLastScheduledActions,
migrateMeta,
migrateAlertInstances,
upMigration,
} from './migration';
describe('migrateThrottledActions', () => {
it('should return undefined if input is not an object', () => {
const result = migrateThrottledActions(null);
expect(result).toBeUndefined();
});
it('should return the migrated throttledActions object', () => {
const input = {
key1: { date: '2023-07-31T12:00:00Z' },
key2: { date: '2023-07-30T12:00:00Z' },
key3: 'notAnObject',
};
const expectedOutput = {
key1: { date: '2023-07-31T12:00:00Z' },
key2: { date: '2023-07-30T12:00:00Z' },
};
const result = migrateThrottledActions(input);
expect(result).toEqual(expectedOutput);
});
});
describe('migrateLastScheduledActions', () => {
it('should return undefined if input is not a valid lastScheduledActions object', () => {
const result = migrateLastScheduledActions({ group: 'group1' }); // Missing 'date' property
expect(result).toBeUndefined();
});
it('should return the migrated lastScheduledActions object', () => {
const input = {
group: 'group1',
subgroup: 'subgroup1',
date: '2023-07-31T12:00:00Z',
actions: {
key1: { date: '2023-07-31T12:00:00Z' },
key2: { date: '2023-07-30T12:00:00Z' },
},
};
const expectedOutput = {
group: 'group1',
subgroup: 'subgroup1',
date: '2023-07-31T12:00:00Z',
actions: {
key1: { date: '2023-07-31T12:00:00Z' },
key2: { date: '2023-07-30T12:00:00Z' },
},
};
const result = migrateLastScheduledActions(input);
expect(result).toEqual(expectedOutput);
});
});
describe('migrateMeta', () => {
it('should return undefined if input is not an object', () => {
const result = migrateMeta(null);
expect(result).toBeUndefined();
});
it('should return the migrated meta object', () => {
const input = {
lastScheduledActions: {
group: 'group1',
date: '2023-07-31T12:00:00Z',
},
flappingHistory: [true, false, true],
flapping: true,
maintenanceWindowIds: ['id1', 'id2'],
pendingRecoveredCount: 3,
uuid: 'abc123',
};
const expectedOutput = {
lastScheduledActions: {
group: 'group1',
date: '2023-07-31T12:00:00Z',
},
flappingHistory: [true, false, true],
flapping: true,
maintenanceWindowIds: ['id1', 'id2'],
pendingRecoveredCount: 3,
uuid: 'abc123',
};
const result = migrateMeta(input);
expect(result).toEqual(expectedOutput);
});
});
describe('migrateAlertInstances', () => {
it('should return undefined if input is not an object', () => {
const result = migrateAlertInstances(null);
expect(result).toBeUndefined();
});
it('should return the migrated alertInstances object', () => {
const input = {
instance1: {
meta: {
lastScheduledActions: {
group: 'group1',
date: '2023-07-31T12:00:00Z',
},
flappingHistory: [true, false, true],
flapping: true,
maintenanceWindowIds: ['id1', 'id2'],
pendingRecoveredCount: 3,
uuid: 'abc123',
},
state: { key: 'value' },
},
instance2: {
meta: {
lastScheduledActions: {
group: 'group2',
date: '2023-07-30T12:00:00Z',
},
},
},
instance3: 'notAnObject',
};
const expectedOutput = {
instance1: {
meta: {
lastScheduledActions: {
group: 'group1',
date: '2023-07-31T12:00:00Z',
},
flappingHistory: [true, false, true],
flapping: true,
maintenanceWindowIds: ['id1', 'id2'],
pendingRecoveredCount: 3,
uuid: 'abc123',
},
state: { key: 'value' },
},
instance2: {
meta: {
lastScheduledActions: {
group: 'group2',
date: '2023-07-30T12:00:00Z',
},
},
},
};
const result = migrateAlertInstances(input);
expect(result).toEqual(expectedOutput);
});
});
describe('upMigration', () => {
it('should return the migrated state object', () => {
const inputState = {
alertTypeState: {},
alertInstances: {
instance1: {
meta: {
lastScheduledActions: {
group: 'group1',
date: '2023-07-31T12:00:00Z',
},
flappingHistory: [true, false, true],
flapping: true,
maintenanceWindowIds: ['id1', 'id2'],
pendingRecoveredCount: 3,
uuid: 'abc123',
},
state: { key: 'value' },
},
},
alertRecoveredInstances: {},
previousStartedAt: '2023-07-30T12:00:00Z',
summaryActions: {
action1: { date: '2023-07-31T12:00:00Z' },
},
};
const expectedOutput = {
alertTypeState: {},
alertInstances: {
instance1: {
meta: {
lastScheduledActions: {
group: 'group1',
date: '2023-07-31T12:00:00Z',
},
flappingHistory: [true, false, true],
flapping: true,
maintenanceWindowIds: ['id1', 'id2'],
pendingRecoveredCount: 3,
uuid: 'abc123',
},
state: { key: 'value' },
},
},
alertRecoveredInstances: {},
previousStartedAt: '2023-07-30T12:00:00Z',
summaryActions: {
action1: { date: '2023-07-31T12:00:00Z' },
},
};
const result = upMigration(inputState);
expect(result).toEqual(expectedOutput);
});
});

View file

@ -0,0 +1,102 @@
/*
* 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 { type TypeOf } from '@kbn/config-schema';
import { isJSONObject, isString, isBoolean, isNumber, isStringArray, isBooleanArray } from '../lib';
import {
versionSchema,
throttledActionSchema,
rawAlertInstanceSchema,
metaSchema,
lastScheduledActionsSchema,
} from './schema';
type VersionSchema = TypeOf<typeof versionSchema>;
type ThrottledActionsSchema = TypeOf<typeof throttledActionSchema>;
type LastScheduledActionsSchema = TypeOf<typeof lastScheduledActionsSchema>;
type RawAlertInstanceSchema = TypeOf<typeof rawAlertInstanceSchema>;
export function migrateThrottledActions(
throttledActions: unknown
): ThrottledActionsSchema | undefined {
if (!isJSONObject(throttledActions)) {
return;
}
return Object.keys(throttledActions).reduce((acc, key) => {
const val = throttledActions[key];
if (isJSONObject(val) && isString(val.date)) {
acc[key] = {
date: val.date,
};
}
return acc;
}, {} as TypeOf<typeof throttledActionSchema>);
}
export function migrateLastScheduledActions(
lastScheduledActions: unknown
): LastScheduledActionsSchema | undefined {
if (
!isJSONObject(lastScheduledActions) ||
!isString(lastScheduledActions.group) ||
!isString(lastScheduledActions.date)
) {
return;
}
return {
subgroup: isString(lastScheduledActions.subgroup) ? lastScheduledActions.subgroup : undefined,
group: lastScheduledActions.group,
date: lastScheduledActions.date,
actions: migrateThrottledActions(lastScheduledActions.actions),
};
}
export function migrateMeta(meta: unknown): TypeOf<typeof metaSchema> | undefined {
if (!isJSONObject(meta)) {
return;
}
return {
lastScheduledActions: migrateLastScheduledActions(meta.lastScheduledActions),
flappingHistory: isBooleanArray(meta.flappingHistory) ? meta.flappingHistory : undefined,
flapping: isBoolean(meta.flapping) ? meta.flapping : undefined,
maintenanceWindowIds: isStringArray(meta.maintenanceWindowIds)
? meta.maintenanceWindowIds
: undefined,
pendingRecoveredCount: isNumber(meta.pendingRecoveredCount)
? meta.pendingRecoveredCount
: undefined,
uuid: isString(meta.uuid) ? meta.uuid : undefined,
};
}
export function migrateAlertInstances(
alertInstances: unknown
): Record<string, RawAlertInstanceSchema> | undefined {
if (!isJSONObject(alertInstances)) {
return;
}
return Object.keys(alertInstances).reduce((acc, key) => {
const val = alertInstances[key];
if (isJSONObject(val)) {
acc[key] = {
meta: migrateMeta(val.meta),
state: isJSONObject(val.state) ? val.state : undefined,
};
}
return acc;
}, {} as Record<string, RawAlertInstanceSchema>);
}
export const upMigration = (state: Record<string, unknown>): VersionSchema => {
return {
alertTypeState: isJSONObject(state.alertTypeState) ? state.alertTypeState : undefined,
alertInstances: migrateAlertInstances(state.alertInstances),
alertRecoveredInstances: migrateAlertInstances(state.alertRecoveredInstances),
previousStartedAt: isString(state.previousStartedAt) ? state.previousStartedAt : undefined,
summaryActions: migrateThrottledActions(state.summaryActions),
};
};

View file

@ -0,0 +1,52 @@
/*
* 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 { schema } from '@kbn/config-schema';
const actionSchema = schema.object({ date: schema.string() });
export const throttledActionSchema = schema.recordOf(schema.string(), actionSchema);
// TODO: Add schema by rule type for alert state
// https://github.com/elastic/kibana/issues/159344
export const alertStateSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
// TODO: Add schema by rule type for rule state
// https://github.com/elastic/kibana/issues/159344
const ruleStateSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
export const lastScheduledActionsSchema = schema.object({
subgroup: schema.maybe(schema.string()),
group: schema.string(),
date: schema.string(),
actions: schema.maybe(throttledActionSchema),
});
export const metaSchema = schema.object({
lastScheduledActions: schema.maybe(lastScheduledActionsSchema),
// an array used to track changes in alert state, the order is based on the rule executions (oldest to most recent)
// true - alert has changed from active/recovered
// false - the status has remained either active or recovered
flappingHistory: schema.maybe(schema.arrayOf(schema.boolean())),
// flapping flag that indicates whether the alert is flapping
flapping: schema.maybe(schema.boolean()),
maintenanceWindowIds: schema.maybe(schema.arrayOf(schema.string())),
pendingRecoveredCount: schema.maybe(schema.number()),
uuid: schema.maybe(schema.string()),
});
export const rawAlertInstanceSchema = schema.object({
meta: schema.maybe(metaSchema),
state: schema.maybe(alertStateSchema),
});
export const versionSchema = schema.object({
alertTypeState: schema.maybe(ruleStateSchema),
// tracks the active alerts
alertInstances: schema.maybe(schema.recordOf(schema.string(), rawAlertInstanceSchema)),
// tracks the recovered alerts for flapping purposes
alertRecoveredInstances: schema.maybe(schema.recordOf(schema.string(), rawAlertInstanceSchema)),
previousStartedAt: schema.maybe(schema.nullable(schema.string())),
summaryActions: schema.maybe(throttledActionSchema),
});

View file

@ -13,5 +13,5 @@
"exclude": [
"target/**/*"
],
"kbn_references": []
"kbn_references": ["@kbn/config-schema"]
}

View file

@ -25,14 +25,6 @@ export type {
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';

View file

@ -44,7 +44,7 @@ describe('isThrottled', () => {
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1', {
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -58,10 +58,10 @@ describe('isThrottled', () => {
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1', {
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
actions: {
'slack:alert:1h': { date: new Date() },
'slack:alert:1h': { date: new Date().toISOString() },
},
},
},
@ -77,7 +77,7 @@ describe('isThrottled', () => {
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1', {
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -91,7 +91,7 @@ describe('isThrottled', () => {
const alert = new Alert<never, never, 'default' | 'other-group'>('1', {
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -105,10 +105,10 @@ describe('isThrottled', () => {
const alert = new Alert<never, never, 'default' | 'other-group'>('1', {
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
actions: {
'111-111': { date: new Date() },
'111-111': { date: new Date().toISOString() },
},
},
},
@ -122,10 +122,10 @@ describe('isThrottled', () => {
const alert = new Alert<never, never, 'default' | 'other-group'>('1', {
meta: {
lastScheduledActions: {
date: new Date('2020-01-01'),
date: new Date('2020-01-01').toISOString(),
group: 'default',
actions: {
'111-111': { date: new Date() },
'111-111': { date: new Date().toISOString() },
},
},
},
@ -141,10 +141,10 @@ describe('isThrottled', () => {
const alert = new Alert<never, never, 'default' | 'other-group'>('1', {
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
actions: {
'111-111': { date: new Date() },
'111-111': { date: new Date().toISOString() },
},
},
},
@ -165,7 +165,7 @@ describe('scheduledActionGroupHasChanged()', () => {
const alert = new Alert<AlertInstanceState, AlertInstanceContext, DefaultActionGroupId>('1', {
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -184,7 +184,7 @@ describe('scheduledActionGroupHasChanged()', () => {
const alert = new Alert<never, never, 'default' | 'penguin'>('1', {
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -274,7 +274,7 @@ describe('scheduleActions()', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -288,7 +288,7 @@ describe('scheduleActions()', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -302,7 +302,7 @@ describe('scheduleActions()', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -396,10 +396,10 @@ describe('updateLastScheduledActions()', () => {
flappingHistory: [],
maintenanceWindowIds: [],
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
actions: {
'slack:alert:1h': { date: new Date() },
'slack:alert:1h': { date: new Date().toISOString() },
},
},
},
@ -429,7 +429,7 @@ describe('getContext()', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -442,7 +442,7 @@ describe('getContext()', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -458,7 +458,7 @@ describe('hasContext()', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -472,7 +472,7 @@ describe('hasContext()', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -486,7 +486,7 @@ describe('hasContext()', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
},
@ -503,7 +503,7 @@ describe('toJSON', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
flappingHistory: [false, true],
@ -520,7 +520,7 @@ describe('toJSON', () => {
},
meta: {
lastScheduledActions: {
date: expect.any(Date),
date: expect.any(String),
group: 'default',
},
uuid: expect.any(String),
@ -538,7 +538,7 @@ describe('toRaw', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
flappingHistory: [false, true, true],
@ -557,7 +557,7 @@ describe('toRaw', () => {
state: { foo: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'default',
},
flappingHistory: [false, true, true],

View file

@ -7,12 +7,12 @@
import { v4 as uuidV4 } from 'uuid';
import { isEmpty } from 'lodash';
import { MutableAlertInstanceMeta } from '@kbn/alerting-state-types';
import { AlertHit, CombinedSummarizedAlerts } from '../types';
import {
AlertInstanceMeta,
AlertInstanceState,
RawAlertInstance,
rawAlertInstance,
AlertInstanceContext,
DefaultActionGroupId,
LastScheduledActions,
@ -52,7 +52,7 @@ export class Alert<
ActionGroupIds extends string = never
> {
private scheduledExecutionOptions?: ScheduledExecutionOptions<State, Context, ActionGroupIds>;
private meta: AlertInstanceMeta;
private meta: MutableAlertInstanceMeta;
private state: State;
private context: Context;
private readonly id: string;
@ -111,11 +111,13 @@ export class Alert<
this.meta.lastScheduledActions.actions[uuid] ||
this.meta.lastScheduledActions.actions[actionHash]; // actionHash must be removed once all the hash identifiers removed from the task state
const lastTriggerDate = actionInState?.date;
return !!(lastTriggerDate && lastTriggerDate.getTime() + throttleMills > Date.now());
return !!(
lastTriggerDate && new Date(lastTriggerDate).getTime() + throttleMills > Date.now()
);
}
return false;
} else {
return this.meta.lastScheduledActions.date.getTime() + throttleMills > Date.now();
return new Date(this.meta.lastScheduledActions.date).getTime() + throttleMills > Date.now();
}
}
return false;
@ -202,7 +204,7 @@ export class Alert<
if (!this.meta.lastScheduledActions) {
this.meta.lastScheduledActions = {} as LastScheduledActions;
}
const date = new Date();
const date = new Date().toISOString();
this.meta.lastScheduledActions.group = group;
this.meta.lastScheduledActions.date = date;
@ -224,7 +226,7 @@ export class Alert<
* Used to serialize alert instance state
*/
toJSON() {
return rawAlertInstance.encode(this.toRaw());
return this.toRaw();
}
toRaw(recovered: boolean = false): RawAlertInstance {

View file

@ -49,7 +49,10 @@ describe('createAlertFactory()', () => {
test('reuses existing alerts', () => {
const alert = new Alert('1', {
state: { foo: true },
meta: { lastScheduledActions: { group: 'default', date: new Date() }, uuid: 'uuid-previous' },
meta: {
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'uuid-previous',
},
});
const alertFactory = createAlertFactory({
alerts: {
@ -65,7 +68,7 @@ describe('createAlertFactory()', () => {
uuid: 'uuid-previous',
flappingHistory: [],
lastScheduledActions: {
date: expect.any(Date),
date: expect.any(String),
group: 'default',
},
},
@ -100,7 +103,10 @@ describe('createAlertFactory()', () => {
test('gets alert if it exists, returns null if it does not', () => {
const alert = new Alert('1', {
state: { foo: true },
meta: { lastScheduledActions: { group: 'default', date: new Date() }, uuid: 'uuid-previous' },
meta: {
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'uuid-previous',
},
});
const alertFactory = createAlertFactory({
alerts: {

View file

@ -177,7 +177,7 @@ describe('Alerts Client', () => {
meta: {
flapping: false,
flappingHistory: [true, false],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'abc',
},
}),
@ -186,7 +186,7 @@ describe('Alerts Client', () => {
meta: {
flapping: false,
flappingHistory: [true, false, false],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'def',
},
}),
@ -245,7 +245,7 @@ describe('Alerts Client', () => {
meta: {
flapping: false,
flappingHistory: [true, false],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: id,
},
});
@ -285,7 +285,7 @@ describe('Alerts Client', () => {
meta: {
flapping: false,
flappingHistory: [true, false],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'abc',
},
}),
@ -540,7 +540,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'abc',
},
},
@ -800,7 +800,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'abc',
},
},
@ -810,7 +810,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true, false],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'def',
},
},
@ -1779,7 +1779,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'abc',
},
},
@ -1789,7 +1789,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true, false],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'def',
},
},
@ -1827,7 +1827,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'abc',
},
},
@ -1837,7 +1837,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true, false],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'def',
},
},
@ -2050,7 +2050,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'abc',
},
},
@ -2227,7 +2227,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'abc',
},
},
@ -2415,7 +2415,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'abc',
},
},
@ -2505,7 +2505,7 @@ describe('Alerts Client', () => {
flapping: false,
flappingHistory: [true],
maintenanceWindowIds: [],
lastScheduledActions: { group: 'default', date: new Date() },
lastScheduledActions: { group: 'default', date: new Date().toISOString() },
uuid: 'abc',
},
},

View file

@ -110,7 +110,7 @@ const testAlert2 = {
meta: {
lastScheduledActions: {
group: 'default',
date: new Date(),
date: new Date().toISOString(),
},
uuid: 'def',
},

View file

@ -7,6 +7,7 @@
import { eventLoggerMock } from '@kbn/event-log-plugin/server/event_logger.mock';
import { IEvent, SAVED_OBJECT_REL_PRIMARY } from '@kbn/event-log-plugin/server';
import { ActionsCompletion } from '@kbn/alerting-state-types';
import {
AlertingEventLogger,
RuleContextOpts,
@ -19,7 +20,6 @@ import {
} from './alerting_event_logger';
import { UntypedNormalizedRuleType } from '../../rule_type_registry';
import {
ActionsCompletion,
RecoveredActionGroup,
RuleExecutionStatusErrorReasons,
RuleExecutionStatusWarningReasons,

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { ActionsCompletion } from '@kbn/alerting-state-types';
import { lastRunFromState } from './last_run_status';
import { ActionsCompletion } from '../../common';
import { RuleRunMetrics } from './rule_run_metrics_store';
import { RuleResultServiceResults, RuleResultService } from '../monitoring/rule_result_service';

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import { ActionsCompletion } from '@kbn/alerting-state-types';
import { RuleTaskStateAndMetrics } from '../task_runner/types';
import { getReasonFromError } from './error_with_reason';
import { getEsErrorMessage } from './errors';
import { ActionsCompletion, RuleLastRunOutcomeOrderMap, RuleLastRunOutcomes } from '../../common';
import { RuleLastRunOutcomeOrderMap, RuleLastRunOutcomes } from '../../common';
import {
RuleLastRunOutcomeValues,
RuleExecutionStatusWarningReasons,

View file

@ -6,11 +6,8 @@
*/
import { loggingSystemMock } from '@kbn/core/server/mocks';
import {
ActionsCompletion,
RuleExecutionStatusErrorReasons,
RuleExecutionStatusWarningReasons,
} from '../types';
import { ActionsCompletion } from '@kbn/alerting-state-types';
import { RuleExecutionStatusErrorReasons, RuleExecutionStatusWarningReasons } from '../types';
import {
executionStatusFromState,
executionStatusFromError,

View file

@ -6,6 +6,7 @@
*/
import { Logger } from '@kbn/core/server';
import { ActionsCompletion } from '@kbn/alerting-state-types';
import {
RuleExecutionStatus,
RuleExecutionStatusValues,
@ -16,7 +17,7 @@ import {
} from '../types';
import { getReasonFromError } from './error_with_reason';
import { getEsErrorMessage } from './errors';
import { ActionsCompletion, RuleExecutionStatuses } from '../../common';
import { RuleExecutionStatuses } from '../../common';
import { translations } from '../constants/translations';
import { RuleTaskStateAndMetrics } from '../task_runner/types';
import { RuleRunMetrics } from './rule_run_metrics_store';

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { ActionsCompletion } from '@kbn/alerting-state-types';
import { RuleRunMetricsStore } from './rule_run_metrics_store';
import { ActionsCompletion } from '../types';
describe('RuleRunMetricsStore', () => {
const ruleRunMetricsStore = new RuleRunMetricsStore();

View file

@ -6,7 +6,7 @@
*/
import { set } from '@kbn/safer-lodash-set';
import { ActionsCompletion } from '../types';
import { ActionsCompletion } from '@kbn/alerting-state-types';
import { ActionsConfigMap } from './get_actions_config_map';
import { SearchMetrics } from './types';

View file

@ -1,29 +0,0 @@
/*
* 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 { DateFromString } from './types';
import { right, isLeft } from 'fp-ts/lib/Either';
describe('DateFromString', () => {
test('validated and parses a string into a Date', () => {
const date = new Date(1973, 10, 30);
expect(DateFromString.decode(date.toISOString())).toEqual(right(date));
});
test('validated and returns a failure for an actual Date', () => {
const date = new Date(1973, 10, 30);
expect(isLeft(DateFromString.decode(date))).toEqual(true);
});
test('validated and returns a failure for an invalid Date string', () => {
expect(isLeft(DateFromString.decode('1234-23-45'))).toEqual(true);
});
test('validated and returns a failure for a null value', () => {
expect(isLeft(DateFromString.decode(null))).toEqual(true);
});
});

View file

@ -5,29 +5,9 @@
* 2.0.
*/
import * as t from 'io-ts';
import { either } from 'fp-ts/lib/Either';
import { Rule } from '../types';
import { RuleRunMetrics } from './rule_run_metrics_store';
// represents a Date from an ISO string
export const DateFromString = new t.Type<Date, string, unknown>(
'DateFromString',
// detect the type
(value): value is Date => value instanceof Date,
(valueToDecode, context) =>
either.chain(
// validate this is a string
t.string.validate(valueToDecode, context),
// decode
(value) => {
const decoded = new Date(value);
return isNaN(decoded.getTime()) ? t.failure(valueToDecode, context) : t.success(decoded);
}
),
(valueToEncode) => valueToEncode.toISOString()
);
export type RuleInfo = Pick<Rule, 'name' | 'alertTypeId' | 'id'> & { spaceId: string };
export interface LogSearchMetricsOpts {

View file

@ -32,7 +32,7 @@ describe('getRuleStateRoute', () => {
meta: {
lastScheduledActions: {
group: 'first_group',
date: new Date(),
date: new Date().toISOString(),
},
},
},

View file

@ -37,7 +37,7 @@ describe('getAlertStateRoute', () => {
meta: {
lastScheduledActions: {
group: 'first_group',
date: new Date(),
date: new Date().toISOString(),
},
},
},

View file

@ -17,7 +17,6 @@ import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock';
import { alertsServiceMock } from './alerts_service/alerts_service.mock';
import { schema } from '@kbn/config-schema';
import { RecoveredActionGroupId } from '../common';
import { rawRuleSchema } from './raw_rule_schema';
const logger = loggingSystemMock.create().get();
let mockedLicenseState: jest.Mocked<ILicenseState>;
@ -437,17 +436,12 @@ describe('Create Lifecycle', () => {
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
registry.register(ruleType);
expect(taskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1);
expect(taskManager.registerTaskDefinitions.mock.calls[0]).toEqual([
{
'alerting:test': {
createTaskRunner: expect.any(Function),
paramsSchema: expect.any(Object),
indirectParamsSchema: rawRuleSchema,
timeout: '20m',
title: 'Test',
},
expect(taskManager.registerTaskDefinitions.mock.calls[0][0]).toMatchObject({
'alerting:test': {
timeout: '20m',
title: 'Test',
},
]);
});
});
test('shallow clones the given rule type', () => {

View file

@ -13,6 +13,7 @@ import { intersection } from 'lodash';
import { Logger } from '@kbn/core/server';
import { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
import { RunContext, TaskManagerSetupContract } from '@kbn/task-manager-plugin/server';
import { stateSchemaByVersion } from '@kbn/alerting-state-types';
import { rawRuleSchema } from './raw_rule_schema';
import { TaskRunnerFactory } from './task_runner';
import {
@ -279,10 +280,12 @@ export class RuleTypeRegistry {
/** stripping the typing is required in order to store the RuleTypes in a Map */
normalizedRuleType as unknown as UntypedNormalizedRuleType
);
this.taskManager.registerTaskDefinitions({
[`alerting:${ruleType.id}`]: {
title: ruleType.name,
timeout: ruleType.ruleTaskTimeout,
stateSchemaByVersion,
createTaskRunner: (context: RunContext) =>
this.taskRunnerFactory.create<
Params,
@ -302,6 +305,7 @@ export class RuleTypeRegistry {
indirectParamsSchema: rawRuleSchema,
},
});
if (this.alertsService && ruleType.alerts) {
this.alertsService.register(ruleType.alerts as IRuleTypeAlerts);
}

View file

@ -41,8 +41,7 @@ const alert: SanitizedRule<{
};
describe('Alert Task Instance', () => {
test(`validates that a TaskInstance has valid Alert Task State`, () => {
const lastScheduledActionsDate = new Date();
test(`passes-through the state object`, () => {
const taskInstance: ConcreteTaskInstance = {
id: uuidv4(),
attempts: 0,
@ -52,129 +51,7 @@ describe('Alert Task Instance', () => {
scheduledAt: new Date(),
startedAt: new Date(),
retryAt: new Date(Date.now() + 5 * 60 * 1000),
state: {
alertTypeState: {
some: 'value',
},
alertInstances: {
first_instance: {
state: {},
meta: {
lastScheduledActions: {
group: 'first_group',
date: lastScheduledActionsDate.toISOString(),
},
},
},
second_instance: {},
},
},
taskType: 'alerting:test',
params: {
alertId: '1',
},
ownerId: null,
};
const alertTaskInsatnce: AlertTaskInstance = taskInstanceToAlertTaskInstance(taskInstance);
expect(alertTaskInsatnce).toEqual({
...taskInstance,
state: {
alertTypeState: {
some: 'value',
},
alertInstances: {
first_instance: {
state: {},
meta: {
lastScheduledActions: {
group: 'first_group',
date: lastScheduledActionsDate,
},
},
},
second_instance: {},
},
},
});
});
test(`throws if state is invalid`, () => {
const taskInstance: ConcreteTaskInstance = {
id: '215ee69b-1df9-428e-ab1a-ccf274f8fa5b',
attempts: 0,
status: TaskStatus.Running,
version: '123',
runAt: new Date(),
scheduledAt: new Date(),
startedAt: new Date(),
retryAt: new Date(Date.now() + 5 * 60 * 1000),
state: {
alertTypeState: {
some: 'value',
},
alertInstances: {
first_instance: 'invalid',
second_instance: {},
},
},
taskType: 'alerting:test',
params: {
alertId: '1',
},
ownerId: null,
};
expect(() => taskInstanceToAlertTaskInstance(taskInstance)).toThrowErrorMatchingInlineSnapshot(
`"Task \\"215ee69b-1df9-428e-ab1a-ccf274f8fa5b\\" has invalid state at .alertInstances.first_instance"`
);
});
test(`throws with Alert id when alert is present and state is invalid`, () => {
const taskInstance: ConcreteTaskInstance = {
id: '215ee69b-1df9-428e-ab1a-ccf274f8fa5b',
attempts: 0,
status: TaskStatus.Running,
version: '123',
runAt: new Date(),
scheduledAt: new Date(),
startedAt: new Date(),
retryAt: new Date(Date.now() + 5 * 60 * 1000),
state: {
alertTypeState: {
some: 'value',
},
alertInstances: {
first_instance: 'invalid',
second_instance: {},
},
},
taskType: 'alerting:test',
params: {
alertId: '1',
},
ownerId: null,
};
expect(() =>
taskInstanceToAlertTaskInstance(taskInstance, alert)
).toThrowErrorMatchingInlineSnapshot(
`"Task \\"215ee69b-1df9-428e-ab1a-ccf274f8fa5b\\" (underlying Alert \\"alert-123\\") has invalid state at .alertInstances.first_instance"`
);
});
test(`allows an initial empty state`, () => {
const taskInstance: ConcreteTaskInstance = {
id: uuidv4(),
attempts: 0,
status: TaskStatus.Running,
version: '123',
runAt: new Date(),
scheduledAt: new Date(),
startedAt: new Date(),
retryAt: new Date(Date.now() + 5 * 60 * 1000),
state: {},
state: { foo: true },
taskType: 'alerting:test',
params: {
alertId: '1',

View file

@ -9,14 +9,8 @@ import * as t from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server';
import {
SanitizedRule,
RuleTaskState,
ruleParamsSchema,
ruleStateSchema,
RuleTaskParams,
RuleTypeParams,
} from '../../common';
import { ruleParamsSchema } from '@kbn/alerting-state-types';
import { SanitizedRule, RuleTaskState, RuleTaskParams, RuleTypeParams } from '../../common';
export interface AlertTaskInstance extends ConcreteTaskInstance {
state: RuleTaskState;
@ -42,15 +36,6 @@ export function taskInstanceToAlertTaskInstance<Params extends RuleTypeParams>(
);
}, t.identity)
),
state: pipe(
ruleStateSchema.decode(taskInstance.state),
fold((e: t.Errors) => {
throw new Error(
`Task "${taskInstance.id}" ${
alert ? `(underlying Alert "${alert.id}") ` : ''
}has invalid state at ${enumerateErrorFields(e)}`
);
}, t.identity)
),
state: taskInstance.state as RuleTaskState,
};
}

View file

@ -13,10 +13,10 @@ import {
renderActionParameterTemplatesDefault,
} from '@kbn/actions-plugin/server/mocks';
import { KibanaRequest } from '@kbn/core/server';
import { ActionsCompletion } from '@kbn/alerting-state-types';
import { InjectActionParamsOpts, injectActionParams } from './inject_action_params';
import { NormalizedRuleType } from '../rule_type_registry';
import {
ActionsCompletion,
ThrottledActions,
RuleTypeParams,
RuleTypeState,
@ -166,7 +166,7 @@ const generateAlert = ({
meta: {
maintenanceWindowIds,
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: lastScheduledActionsGroup,
actions: throttledActions,
},
@ -188,7 +188,7 @@ const generateRecoveredAlert = ({ id, state }: { id: number; state?: AlertInstan
state: state || { test: true },
meta: {
lastScheduledActions: {
date: new Date(),
date: new Date().toISOString(),
group: 'recovered',
actions: {},
},
@ -792,7 +792,7 @@ describe('Execution Handler', () => {
await executionHandler.run(
generateAlert({
id: 1,
throttledActions: { '111-111': { date: new Date(DATE_1970) } },
throttledActions: { '111-111': { date: new Date(DATE_1970).toISOString() } },
})
);
@ -1016,7 +1016,7 @@ describe('Execution Handler', () => {
expect(result).toEqual({
throttledSummaryActions: {
'111-111': {
date: new Date(),
date: new Date().toISOString(),
},
},
});

View file

@ -11,6 +11,7 @@ import { getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils'
import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server';
import { isEphemeralTaskRejectedDueToCapacityError } from '@kbn/task-manager-plugin/server';
import { ExecuteOptions as EnqueueExecutionOptions } from '@kbn/actions-plugin/server/create_execute_function';
import { ActionsCompletion } from '@kbn/alerting-state-types';
import { ActionsClient } from '@kbn/actions-plugin/server/actions_client';
import { chunk } from 'lodash';
import { GetSummarizedAlertsParams, IAlertsClient } from '../alerts_client/types';
@ -24,7 +25,6 @@ import { transformActionParams, transformSummaryActionParams } from './transform
import { Alert } from '../alert';
import { NormalizedRuleType } from '../rule_type_registry';
import {
ActionsCompletion,
AlertInstanceContext,
AlertInstanceState,
RuleAction,
@ -259,7 +259,7 @@ export class ExecutionHandler<
});
if (isActionOnInterval(action)) {
throttledSummaryActions[action.uuid!] = { date: new Date() };
throttledSummaryActions[action.uuid!] = { date: new Date().toISOString() };
}
logActions.push({

View file

@ -383,7 +383,7 @@ export const generateRunnerResult = ({
...(state && { alertInstances }),
...(state && { alertRecoveredInstances }),
...(state && { alertTypeState: {} }),
...(state && { previousStartedAt: new Date('1970-01-01T00:00:00.000Z') }),
...(state && { previousStartedAt: new Date('1970-01-01T00:00:00.000Z').toISOString() }),
...(state && { summaryActions }),
},
hasError,
@ -440,7 +440,7 @@ export const generateAlertInstance = (
meta: {
uuid: expect.any(String),
lastScheduledActions: {
date: new Date(DATE_1970),
date: new Date(DATE_1970).toISOString(),
group: 'default',
...(actions && { actions }),
},

View file

@ -146,21 +146,21 @@ describe('rule_action_helper', () => {
const result = getSummaryActionsFromTaskState({
actions: [mockSummaryAction],
summaryActions: {
'111-111': { date: new Date('01.01.2020') },
'222-222': { date: new Date('01.01.2020') },
'111-111': { date: new Date('01.01.2020').toISOString() },
'222-222': { date: new Date('01.01.2020').toISOString() },
},
});
expect(result).toEqual({ '111-111': { date: new Date('01.01.2020') } });
expect(result).toEqual({ '111-111': { date: new Date('01.01.2020').toISOString() } });
});
test('should replace hash with uuid', () => {
const result = getSummaryActionsFromTaskState({
actions: [mockSummaryAction],
summaryActions: {
'slack:summary:1d': { date: new Date('01.01.2020') },
'slack:summary:1d': { date: new Date('01.01.2020').toISOString() },
},
});
expect(result).toEqual({ '111-111': { date: new Date('01.01.2020') } });
expect(result).toEqual({ '111-111': { date: new Date('01.01.2020').toISOString() } });
});
});
@ -180,7 +180,7 @@ describe('rule_action_helper', () => {
jest.useRealTimers();
});
const logger = { debug: jest.fn() } as unknown as Logger;
const throttledSummaryActions = { '111-111': { date: new Date('2020-01-01T00:00:00.000Z') } };
const throttledSummaryActions = { '111-111': { date: '2020-01-01T00:00:00.000Z' } };
test('should return false if the action does not have throttle filed', () => {
const result = isSummaryActionThrottled({
@ -227,7 +227,7 @@ describe('rule_action_helper', () => {
test('should return false if the action is not in the task instance', () => {
const result = isSummaryActionThrottled({
action: mockSummaryAction,
throttledSummaryActions: { '123-456': { date: new Date('2020-01-01T00:00:00.000Z') } },
throttledSummaryActions: { '123-456': { date: '2020-01-01T00:00:00.000Z' } },
logger,
});
expect(result).toBe(false);
@ -237,7 +237,7 @@ describe('rule_action_helper', () => {
jest.advanceTimersByTime(3600000 * 2);
const result = isSummaryActionThrottled({
action: mockSummaryAction,
throttledSummaryActions: { '123-456': { date: new Date('2020-01-01T00:00:00.000Z') } },
throttledSummaryActions: { '123-456': { date: '2020-01-01T00:00:00.000Z' } },
logger,
});
expect(result).toBe(false);

View file

@ -58,7 +58,7 @@ export const isSummaryActionThrottled = ({
logger.debug(`Action'${action?.actionTypeId}:${action?.id}', has an invalid throttle interval`);
}
const throttled = throttledAction.date.getTime() + throttleMills > Date.now();
const throttled = new Date(throttledAction.date).getTime() + throttleMills > Date.now();
if (throttled) {
logger.debug(

View file

@ -1565,7 +1565,7 @@ describe('Task Runner', () => {
generateEnqueueFunctionInput({ isBulk, id: '1', foo: true })
);
expect(result.state.summaryActions).toEqual({
'111-111': { date: new Date(DATE_1970) },
'111-111': { date: new Date(DATE_1970).toISOString() },
});
}
);
@ -1835,9 +1835,7 @@ describe('Task Runner', () => {
const runnerResult = await taskRunner.run();
expect(runnerResult.state.previousStartedAt).toEqual(
new Date(originalAlertSate.previousStartedAt)
);
expect(runnerResult.state.previousStartedAt).toEqual(originalAlertSate.previousStartedAt);
expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled();
});
@ -2745,7 +2743,7 @@ describe('Task Runner', () => {
meta: {
uuid: expect.any(String),
lastScheduledActions: {
date: new Date(DATE_1970),
date: new Date(DATE_1970).toISOString(),
group: 'default',
},
flappingHistory: [true],
@ -2915,7 +2913,7 @@ describe('Task Runner', () => {
meta: {
uuid: expect.any(String),
lastScheduledActions: {
date: new Date(DATE_1970),
date: new Date(DATE_1970).toISOString(),
group: 'default',
},
flappingHistory: [true],
@ -2932,7 +2930,7 @@ describe('Task Runner', () => {
meta: {
uuid: expect.any(String),
lastScheduledActions: {
date: new Date(DATE_1970),
date: new Date(DATE_1970).toISOString(),
group: 'default',
},
flappingHistory: [true],

View file

@ -856,7 +856,7 @@ export class TaskRunner<
): RuleTaskState => {
return {
...omit(runStateWithMetrics, ['metrics']),
previousStartedAt: startedAt,
previousStartedAt: startedAt?.toISOString(),
};
};

View file

@ -191,14 +191,16 @@ export class BaseRule {
return accum;
}
const alertInstance: RawAlertInstance = states.alertInstances[instanceId];
const filteredAlertInstance = this.filterAlertInstance(alertInstance, filters);
const { state, ...filteredAlertInstance } = this.filterAlertInstance(
alertInstance,
filters
);
if (filteredAlertInstance) {
accum[instanceId] = filteredAlertInstance as RawAlertInstance;
if (filteredAlertInstance.state) {
accum[instanceId].state = {
alertStates: (filteredAlertInstance.state as AlertInstanceState).alertStates,
};
}
accum[instanceId] = {
...filteredAlertInstance,
// Only keep "alertStates" within the state
...(state ? { state: { alertStates: state.alertStates } } : {}),
} as RawAlertInstance;
}
return accum;
},

View file

@ -29,19 +29,19 @@ export const alertInstanceFactoryStub = <
replaceState(state: TInstanceState) {
return new Alert<TInstanceState, TInstanceContext, TActionGroupIds>('', {
state: {} as TInstanceState,
meta: { lastScheduledActions: { group: 'default', date: new Date() } },
meta: { lastScheduledActions: { group: 'default', date: new Date().toISOString() } },
});
},
scheduleActions(actionGroup: TActionGroupIds, alertcontext: TInstanceContext) {
return new Alert<TInstanceState, TInstanceContext, TActionGroupIds>('', {
state: {} as TInstanceState,
meta: { lastScheduledActions: { group: 'default', date: new Date() } },
meta: { lastScheduledActions: { group: 'default', date: new Date().toISOString() } },
});
},
setContext(alertContext: TInstanceContext) {
return new Alert<TInstanceState, TInstanceContext, TActionGroupIds>('', {
state: {} as TInstanceState,
meta: { lastScheduledActions: { group: 'default', date: new Date() } },
meta: { lastScheduledActions: { group: 'default', date: new Date().toISOString() } },
});
},
getContext() {

View file

@ -16,6 +16,7 @@ import {
} from '@kbn/core/server';
import type {
RuleTaskState,
MutableRuleTaskState,
TrackedLifecycleAlertState,
WrappedLifecycleRuleState,
} from '@kbn/alerting-state-types';
@ -253,7 +254,7 @@ function addAlertUUID(doc: SavedObjectUnsanitizedDoc<SerializedConcreteTaskInsta
// mutates alerts passed in
function addAlertUUIDsToAlerts(
alerts: RuleTaskState['alertInstances'] | undefined,
alerts: MutableRuleTaskState['alertInstances'] | undefined,
alertToTrackedMap: Map<string, TrackedLifecycleAlertState>,
currentUUIDs: Map<string, string>
): void {

View file

@ -11,6 +11,7 @@ import { migrationMocks } from '@kbn/core/server/mocks';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import type {
RuleTaskState,
MutableRuleTaskState,
WrappedLifecycleRuleState,
RawAlertInstance,
} from '@kbn/alerting-state-types';
@ -73,7 +74,7 @@ describe('successful migrations for 8.8.0', () => {
});
function checkMetaInRuleTaskState(
actual: RuleTaskState,
actual: MutableRuleTaskState,
original: RuleTaskState,
wrappedUUIDs?: Map<string, string>
) {

View file

@ -82,7 +82,7 @@ describe('loadRuleState', () => {
meta: {
lastScheduledActions: {
group: 'first_group',
date: new Date('2020-02-09T23:15:41.941Z'),
date: '2020-02-09T23:15:41.941Z',
},
},
},

View file

@ -5,10 +5,6 @@
* 2.0.
*/
import { HttpSetup } from '@kbn/core/public';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { Errors, identity } from 'io-ts';
import { ruleStateSchema } from '@kbn/alerting-plugin/common';
import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
import { RuleTaskState } from '../../../types';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
@ -33,17 +29,9 @@ export async function loadRuleState({
http: HttpSetup;
ruleId: string;
}): Promise<RuleTaskState> {
return await http
return (await http
.get<AsApiContract<RuleTaskState> | EmptyHttpResponse>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${ruleId}/state`
)
.then((state) => (state ? rewriteBodyRes(state) : {}))
.then((state: RuleTaskState) => {
return pipe(
ruleStateSchema.decode(state),
fold((e: Errors) => {
throw new Error(`Rule "${ruleId}" has invalid state`);
}, identity)
);
});
.then((state) => (state ? rewriteBodyRes(state) : {}))) as RuleTaskState;
}