Skip to content

Commit

Permalink
[Alerting] Enforces typing of Alert's ActionGroups (elastic#86761)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gmmorris committed Jan 5, 2021
1 parent cf65d47 commit f0df49f
Show file tree
Hide file tree
Showing 69 changed files with 1,006 additions and 328 deletions.
13 changes: 7 additions & 6 deletions x-pack/examples/alerting_example/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample';

// always firing
export const DEFAULT_INSTANCES_TO_GENERATE = 5;
export interface AlwaysFiringThresholds {
small?: number;
medium?: number;
large?: number;
}
export interface AlwaysFiringParams extends AlertTypeParams {
instances?: number;
thresholds?: {
small?: number;
medium?: number;
large?: number;
};
thresholds?: AlwaysFiringThresholds;
}
export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds'];
export type AlwaysFiringActionGroupIds = keyof AlwaysFiringThresholds;

// Astros
export enum Craft {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,10 @@ export const AlwaysFiringExpression: React.FunctionComponent<
};

interface TShirtSelectorProps {
actionGroup?: ActionGroupWithCondition<number>;
setTShirtThreshold: (actionGroup: ActionGroupWithCondition<number>) => void;
actionGroup?: ActionGroupWithCondition<number, AlwaysFiringActionGroupIds>;
setTShirtThreshold: (
actionGroup: ActionGroupWithCondition<number, AlwaysFiringActionGroupIds>
) => void;
}
const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => {
const [isOpen, setIsOpen] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DEFAULT_INSTANCES_TO_GENERATE,
ALERTING_EXAMPLE_APP_ID,
AlwaysFiringParams,
AlwaysFiringActionGroupIds,
} from '../../common/constants';

type ActionGroups = 'small' | 'medium' | 'large';
Expand Down Expand Up @@ -39,7 +40,8 @@ export const alertType: AlertType<
AlwaysFiringParams,
{ count?: number },
{ triggerdOnCycle: number },
never
never,
AlwaysFiringActionGroupIds
> = {
id: 'example.always-firing',
name: 'Always firing',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ function getCraftFilter(craft: string) {
export const alertType: AlertType<
{ outerSpaceCapacity: number; craft: string; op: string },
{ peopleInSpace: number },
{ craft: string }
{ craft: string },
never,
'default',
'hasLandedBackOnEarth'
> = {
id: 'example.people-in-space',
name: 'People In Space Right Now',
Expand Down
41 changes: 38 additions & 3 deletions x-pack/plugins/alerts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,41 @@ This example receives server and threshold as parameters. It will read the CPU u

```typescript
import { schema } from '@kbn/config-schema';
import {
Alert,
AlertTypeParams,
AlertTypeState,
AlertInstanceState,
AlertInstanceContext
} from 'x-pack/plugins/alerts/common';
...
server.newPlatform.setup.plugins.alerts.registerType({
interface MyAlertTypeParams extends AlertTypeParams {
server: string;
threshold: number;
}

interface MyAlertTypeState extends AlertTypeState {
lastChecked: number;
}

interface MyAlertTypeInstanceState extends AlertInstanceState {
cpuUsage: number;
}

interface MyAlertTypeInstanceContext extends AlertInstanceContext {
server: string;
hasCpuUsageIncreased: boolean;
}

type MyAlertTypeActionGroups = 'default' | 'warning';

const myAlertType: AlertType<
MyAlertTypeParams,
MyAlertTypeState,
MyAlertTypeInstanceState,
MyAlertTypeInstanceContext,
MyAlertTypeActionGroups
> = {
id: 'my-alert-type',
name: 'My alert type',
validate: {
Expand Down Expand Up @@ -180,7 +213,7 @@ server.newPlatform.setup.plugins.alerts.registerType({
services,
params,
state,
}: AlertExecutorOptions) {
}: AlertExecutorOptions<MyAlertTypeParams, MyAlertTypeState, MyAlertTypeInstanceState, MyAlertTypeInstanceContext, MyAlertTypeActionGroups>) {
// Let's assume params is { server: 'server_1', threshold: 0.8 }
const { server, threshold } = params;

Expand Down Expand Up @@ -219,7 +252,9 @@ server.newPlatform.setup.plugins.alerts.registerType({
};
},
producer: 'alerting',
});
};

server.newPlatform.setup.plugins.alerts.registerType(myAlertType);
```

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.
Expand Down
22 changes: 16 additions & 6 deletions x-pack/plugins/alerts/common/alert_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,29 @@
*/

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

export interface AlertType {
export interface AlertType<
ActionGroupIds extends Exclude<string, RecoveredActionGroupId> = DefaultActionGroupId,
RecoveryActionGroupId extends string = RecoveredActionGroupId
> {
id: string;
name: string;
actionGroups: ActionGroup[];
recoveryActionGroup: ActionGroup;
actionGroups: Array<ActionGroup<ActionGroupIds>>;
recoveryActionGroup: ActionGroup<RecoveryActionGroupId>;
actionVariables: string[];
defaultActionGroupId: ActionGroup['id'];
defaultActionGroupId: ActionGroupIds;
producer: string;
minimumLicenseRequired: LicenseType;
}

export interface ActionGroup {
id: string;
export interface ActionGroup<ActionGroupIds extends string> {
id: ActionGroupIds;
name: string;
}

export type ActionGroupIdsOf<T> = T extends ActionGroup<infer groups>
? groups
: T extends Readonly<ActionGroup<infer groups>>
? groups
: never;
22 changes: 18 additions & 4 deletions x-pack/plugins/alerts/common/builtin_action_groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,27 @@
import { i18n } from '@kbn/i18n';
import { ActionGroup } from './alert_type';

export const RecoveredActionGroup: Readonly<ActionGroup> = {
export type DefaultActionGroupId = 'default';

export type RecoveredActionGroupId = typeof RecoveredActionGroup['id'];
export const RecoveredActionGroup: Readonly<ActionGroup<'recovered'>> = Object.freeze({
id: 'recovered',
name: i18n.translate('xpack.alerts.builtinActionGroups.recovered', {
defaultMessage: 'Recovered',
}),
};
});

export type ReservedActionGroups<RecoveryActionGroupId extends string> =
| RecoveryActionGroupId
| RecoveredActionGroupId;

export type WithoutReservedActionGroups<
ActionGroupIds extends string,
RecoveryActionGroupId extends string
> = ActionGroupIds extends ReservedActionGroups<RecoveryActionGroupId> ? never : ActionGroupIds;

export function getBuiltinActionGroups(customRecoveryGroup?: ActionGroup): ActionGroup[] {
return [customRecoveryGroup ?? Object.freeze(RecoveredActionGroup)];
export function getBuiltinActionGroups<RecoveryActionGroupId extends string>(
customRecoveryGroup?: ActionGroup<RecoveryActionGroupId>
): [ActionGroup<ReservedActionGroups<RecoveryActionGroupId>>] {
return [customRecoveryGroup ?? RecoveredActionGroup];
}
Loading

0 comments on commit f0df49f

Please sign in to comment.