Skip to content

Commit f0df49f

Browse files
committed
[Alerting] Enforces typing of Alert's ActionGroups (elastic#86761)
This PR tightens the typing on the Alerting framework's `AlertType` and its deeper typing around `AlertServices ` and `AlertExecutorOptions`. This ensures the following: 1. It's now impossible<sup>✴</sup> to schedule actions on any ActionGroup other than the groups specified on the AlertType (including the Recovery group) 2. It's now impossible<sup>✴</sup> to schedule actions with incorrect `InstanceState` or `InstanceContext` ✴ Unless they bypass the Typescript typing, which is an explicit choice to bypass type safety
1 parent cf65d47 commit f0df49f

File tree

69 files changed

+1006
-328
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+1006
-328
lines changed

x-pack/examples/alerting_example/common/constants.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample';
1010

1111
// always firing
1212
export const DEFAULT_INSTANCES_TO_GENERATE = 5;
13+
export interface AlwaysFiringThresholds {
14+
small?: number;
15+
medium?: number;
16+
large?: number;
17+
}
1318
export interface AlwaysFiringParams extends AlertTypeParams {
1419
instances?: number;
15-
thresholds?: {
16-
small?: number;
17-
medium?: number;
18-
large?: number;
19-
};
20+
thresholds?: AlwaysFiringThresholds;
2021
}
21-
export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds'];
22+
export type AlwaysFiringActionGroupIds = keyof AlwaysFiringThresholds;
2223

2324
// Astros
2425
export enum Craft {

x-pack/examples/alerting_example/public/alert_types/always_firing.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,10 @@ export const AlwaysFiringExpression: React.FunctionComponent<
133133
};
134134

135135
interface TShirtSelectorProps {
136-
actionGroup?: ActionGroupWithCondition<number>;
137-
setTShirtThreshold: (actionGroup: ActionGroupWithCondition<number>) => void;
136+
actionGroup?: ActionGroupWithCondition<number, AlwaysFiringActionGroupIds>;
137+
setTShirtThreshold: (
138+
actionGroup: ActionGroupWithCondition<number, AlwaysFiringActionGroupIds>
139+
) => void;
138140
}
139141
const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => {
140142
const [isOpen, setIsOpen] = useState(false);

x-pack/examples/alerting_example/server/alert_types/always_firing.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
DEFAULT_INSTANCES_TO_GENERATE,
1212
ALERTING_EXAMPLE_APP_ID,
1313
AlwaysFiringParams,
14+
AlwaysFiringActionGroupIds,
1415
} from '../../common/constants';
1516

1617
type ActionGroups = 'small' | 'medium' | 'large';
@@ -39,7 +40,8 @@ export const alertType: AlertType<
3940
AlwaysFiringParams,
4041
{ count?: number },
4142
{ triggerdOnCycle: number },
42-
never
43+
never,
44+
AlwaysFiringActionGroupIds
4345
> = {
4446
id: 'example.always-firing',
4547
name: 'Always firing',

x-pack/examples/alerting_example/server/alert_types/astros.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ function getCraftFilter(craft: string) {
4141
export const alertType: AlertType<
4242
{ outerSpaceCapacity: number; craft: string; op: string },
4343
{ peopleInSpace: number },
44-
{ craft: string }
44+
{ craft: string },
45+
never,
46+
'default',
47+
'hasLandedBackOnEarth'
4548
> = {
4649
id: 'example.people-in-space',
4750
name: 'People In Space Right Now',

x-pack/plugins/alerts/README.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,41 @@ This example receives server and threshold as parameters. It will read the CPU u
142142

143143
```typescript
144144
import { schema } from '@kbn/config-schema';
145+
import {
146+
Alert,
147+
AlertTypeParams,
148+
AlertTypeState,
149+
AlertInstanceState,
150+
AlertInstanceContext
151+
} from 'x-pack/plugins/alerts/common';
145152
...
146-
server.newPlatform.setup.plugins.alerts.registerType({
153+
interface MyAlertTypeParams extends AlertTypeParams {
154+
server: string;
155+
threshold: number;
156+
}
157+
158+
interface MyAlertTypeState extends AlertTypeState {
159+
lastChecked: number;
160+
}
161+
162+
interface MyAlertTypeInstanceState extends AlertInstanceState {
163+
cpuUsage: number;
164+
}
165+
166+
interface MyAlertTypeInstanceContext extends AlertInstanceContext {
167+
server: string;
168+
hasCpuUsageIncreased: boolean;
169+
}
170+
171+
type MyAlertTypeActionGroups = 'default' | 'warning';
172+
173+
const myAlertType: AlertType<
174+
MyAlertTypeParams,
175+
MyAlertTypeState,
176+
MyAlertTypeInstanceState,
177+
MyAlertTypeInstanceContext,
178+
MyAlertTypeActionGroups
179+
> = {
147180
id: 'my-alert-type',
148181
name: 'My alert type',
149182
validate: {
@@ -180,7 +213,7 @@ server.newPlatform.setup.plugins.alerts.registerType({
180213
services,
181214
params,
182215
state,
183-
}: AlertExecutorOptions) {
216+
}: AlertExecutorOptions<MyAlertTypeParams, MyAlertTypeState, MyAlertTypeInstanceState, MyAlertTypeInstanceContext, MyAlertTypeActionGroups>) {
184217
// Let's assume params is { server: 'server_1', threshold: 0.8 }
185218
const { server, threshold } = params;
186219

@@ -219,7 +252,9 @@ server.newPlatform.setup.plugins.alerts.registerType({
219252
};
220253
},
221254
producer: 'alerting',
222-
});
255+
};
256+
257+
server.newPlatform.setup.plugins.alerts.registerType(myAlertType);
223258
```
224259

225260
This example only receives threshold as a parameter. It will read the CPU usage of all the servers and schedule individual actions if the reading for a server is greater than the threshold. This is a better implementation than above as only one query is performed for all the servers instead of one query per server.

x-pack/plugins/alerts/common/alert_type.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,29 @@
55
*/
66

77
import { LicenseType } from '../../licensing/common/types';
8+
import { RecoveredActionGroupId, DefaultActionGroupId } from './builtin_action_groups';
89

9-
export interface AlertType {
10+
export interface AlertType<
11+
ActionGroupIds extends Exclude<string, RecoveredActionGroupId> = DefaultActionGroupId,
12+
RecoveryActionGroupId extends string = RecoveredActionGroupId
13+
> {
1014
id: string;
1115
name: string;
12-
actionGroups: ActionGroup[];
13-
recoveryActionGroup: ActionGroup;
16+
actionGroups: Array<ActionGroup<ActionGroupIds>>;
17+
recoveryActionGroup: ActionGroup<RecoveryActionGroupId>;
1418
actionVariables: string[];
15-
defaultActionGroupId: ActionGroup['id'];
19+
defaultActionGroupId: ActionGroupIds;
1620
producer: string;
1721
minimumLicenseRequired: LicenseType;
1822
}
1923

20-
export interface ActionGroup {
21-
id: string;
24+
export interface ActionGroup<ActionGroupIds extends string> {
25+
id: ActionGroupIds;
2226
name: string;
2327
}
28+
29+
export type ActionGroupIdsOf<T> = T extends ActionGroup<infer groups>
30+
? groups
31+
: T extends Readonly<ActionGroup<infer groups>>
32+
? groups
33+
: never;

x-pack/plugins/alerts/common/builtin_action_groups.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,27 @@
66
import { i18n } from '@kbn/i18n';
77
import { ActionGroup } from './alert_type';
88

9-
export const RecoveredActionGroup: Readonly<ActionGroup> = {
9+
export type DefaultActionGroupId = 'default';
10+
11+
export type RecoveredActionGroupId = typeof RecoveredActionGroup['id'];
12+
export const RecoveredActionGroup: Readonly<ActionGroup<'recovered'>> = Object.freeze({
1013
id: 'recovered',
1114
name: i18n.translate('xpack.alerts.builtinActionGroups.recovered', {
1215
defaultMessage: 'Recovered',
1316
}),
14-
};
17+
});
18+
19+
export type ReservedActionGroups<RecoveryActionGroupId extends string> =
20+
| RecoveryActionGroupId
21+
| RecoveredActionGroupId;
22+
23+
export type WithoutReservedActionGroups<
24+
ActionGroupIds extends string,
25+
RecoveryActionGroupId extends string
26+
> = ActionGroupIds extends ReservedActionGroups<RecoveryActionGroupId> ? never : ActionGroupIds;
1527

16-
export function getBuiltinActionGroups(customRecoveryGroup?: ActionGroup): ActionGroup[] {
17-
return [customRecoveryGroup ?? Object.freeze(RecoveredActionGroup)];
28+
export function getBuiltinActionGroups<RecoveryActionGroupId extends string>(
29+
customRecoveryGroup?: ActionGroup<RecoveryActionGroupId>
30+
): [ActionGroup<ReservedActionGroups<RecoveryActionGroupId>>] {
31+
return [customRecoveryGroup ?? RecoveredActionGroup];
1832
}

0 commit comments

Comments
 (0)