[Response Ops][Alerting] AAD for alerting example rules (#174032)

Towards https://github.com/elastic/response-ops-team/issues/164

## Summary

Registering alerting example rules with framework AAD. This creates a
new alerts index `.alerts-default.alerts-default` that will eventually
hold alerts for all rules that have not customized their registration.
This index contains only the mappings for the basic alerts as data
documents, no custom context or payload fields.

## To Verify
Run kibana using `--run-examples` flag. Create one of the example rule
types and let it alert and then resolve and see an alert document get
created in the `.alerts-default.alerts-default` index.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ying Mao 2024-01-09 11:23:49 -05:00 committed by GitHub
parent 537614c36e
commit 2e1a611798
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 207 additions and 44 deletions

View file

@ -322,6 +322,7 @@ export const schemaGeoPoint = rt.union([
export const schemaGeoPointArray = rt.array(schemaGeoPoint);
// prettier-ignore
const %%schemaPrefix%%Required = %%REQUIRED_FIELDS%%;
// prettier-ignore
const %%schemaPrefix%%Optional = %%OPTIONAL_FIELDS%%;
// prettier-ignore

View file

@ -80,6 +80,7 @@ const AlertRequired = rt.type({
'kibana.alert.uuid': schemaString,
'kibana.space_ids': schemaStringArray,
});
// prettier-ignore
const AlertOptional = rt.partial({
'event.action': schemaString,
'event.kind': schemaString,

View file

@ -0,0 +1,78 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
// ---------------------------------- WARNING ----------------------------------
// this file was generated, and should not be edited by hand
// ---------------------------------- WARNING ----------------------------------
import * as rt from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { AlertSchema } from './alert_schema';
const ISO_DATE_PATTERN = /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z$/;
export const IsoDateString = new rt.Type<string, string, unknown>(
'IsoDateString',
rt.string.is,
(input, context): Either<rt.Errors, string> => {
if (typeof input === 'string' && ISO_DATE_PATTERN.test(input)) {
return rt.success(input);
} else {
return rt.failure(input, context);
}
},
rt.identity
);
export type IsoDateStringC = typeof IsoDateString;
export const schemaUnknown = rt.unknown;
export const schemaUnknownArray = rt.array(rt.unknown);
export const schemaString = rt.string;
export const schemaStringArray = rt.array(schemaString);
export const schemaNumber = rt.number;
export const schemaNumberArray = rt.array(schemaNumber);
export const schemaDate = rt.union([IsoDateString, schemaNumber]);
export const schemaDateArray = rt.array(schemaDate);
export const schemaDateRange = rt.partial({
gte: schemaDate,
lte: schemaDate,
});
export const schemaDateRangeArray = rt.array(schemaDateRange);
export const schemaStringOrNumber = rt.union([schemaString, schemaNumber]);
export const schemaStringOrNumberArray = rt.array(schemaStringOrNumber);
export const schemaBoolean = rt.boolean;
export const schemaBooleanArray = rt.array(schemaBoolean);
const schemaGeoPointCoords = rt.type({
type: schemaString,
coordinates: schemaNumberArray,
});
const schemaGeoPointString = schemaString;
const schemaGeoPointLatLon = rt.type({
lat: schemaNumber,
lon: schemaNumber,
});
const schemaGeoPointLocation = rt.type({
location: schemaNumberArray,
});
const schemaGeoPointLocationString = rt.type({
location: schemaString,
});
export const schemaGeoPoint = rt.union([
schemaGeoPointCoords,
schemaGeoPointString,
schemaGeoPointLatLon,
schemaGeoPointLocation,
schemaGeoPointLocationString,
]);
export const schemaGeoPointArray = rt.array(schemaGeoPoint);
// prettier-ignore
const DefaultAlertRequired = rt.type({
});
// prettier-ignore
const DefaultAlertOptional = rt.partial({
});
// prettier-ignore
export const DefaultAlertSchema = rt.intersection([DefaultAlertRequired, DefaultAlertOptional, AlertSchema]);
// prettier-ignore
export type DefaultAlert = rt.TypeOf<typeof DefaultAlertSchema>;

View file

@ -70,6 +70,7 @@ const EcsRequired = rt.type({
'@timestamp': schemaDate,
'ecs.version': schemaString,
});
// prettier-ignore
const EcsOptional = rt.partial({
'agent.build.original': schemaString,
'agent.ephemeral_id': schemaString,

View file

@ -68,6 +68,7 @@ export const schemaGeoPointArray = rt.array(schemaGeoPoint);
// prettier-ignore
const LegacyAlertRequired = rt.type({
});
// prettier-ignore
const LegacyAlertOptional = rt.partial({
'ecs.version': schemaString,
'kibana.alert.risk_score': schemaNumber,

View file

@ -69,6 +69,7 @@ export const schemaGeoPointArray = rt.array(schemaGeoPoint);
const MlAnomalyDetectionAlertRequired = rt.type({
'kibana.alert.job_id': schemaString,
});
// prettier-ignore
const MlAnomalyDetectionAlertOptional = rt.partial({
'kibana.alert.anomaly_score': schemaNumber,
'kibana.alert.anomaly_timestamp': schemaDate,

View file

@ -69,6 +69,7 @@ export const schemaGeoPointArray = rt.array(schemaGeoPoint);
// prettier-ignore
const ObservabilityApmAlertRequired = rt.type({
});
// prettier-ignore
const ObservabilityApmAlertOptional = rt.partial({
'agent.name': schemaString,
'error.grouping_key': schemaString,

View file

@ -70,6 +70,7 @@ export const schemaGeoPointArray = rt.array(schemaGeoPoint);
// prettier-ignore
const ObservabilityLogsAlertRequired = rt.type({
});
// prettier-ignore
const ObservabilityLogsAlertOptional = rt.partial({
'kibana.alert.context': schemaUnknown,
'kibana.alert.evaluation.threshold': schemaStringOrNumber,

View file

@ -70,6 +70,7 @@ export const schemaGeoPointArray = rt.array(schemaGeoPoint);
// prettier-ignore
const ObservabilityMetricsAlertRequired = rt.type({
});
// prettier-ignore
const ObservabilityMetricsAlertOptional = rt.partial({
'kibana.alert.context': schemaUnknown,
'kibana.alert.evaluation.threshold': schemaStringOrNumber,

View file

@ -69,6 +69,7 @@ export const schemaGeoPointArray = rt.array(schemaGeoPoint);
// prettier-ignore
const ObservabilitySloAlertRequired = rt.type({
});
// prettier-ignore
const ObservabilitySloAlertOptional = rt.partial({
'kibana.alert.context': schemaUnknown,
'kibana.alert.evaluation.threshold': schemaStringOrNumber,

View file

@ -69,6 +69,7 @@ export const schemaGeoPointArray = rt.array(schemaGeoPoint);
// prettier-ignore
const ObservabilityUptimeAlertRequired = rt.type({
});
// prettier-ignore
const ObservabilityUptimeAlertOptional = rt.partial({
'agent.name': schemaString,
'anomaly.bucket_span.minutes': schemaString,

View file

@ -117,6 +117,7 @@ const SecurityAlertRequired = rt.type({
'kibana.alert.uuid': schemaString,
'kibana.space_ids': schemaStringArray,
});
// prettier-ignore
const SecurityAlertOptional = rt.partial({
'ecs.version': schemaString,
'event.action': schemaString,

View file

@ -68,6 +68,7 @@ export const schemaGeoPointArray = rt.array(schemaGeoPoint);
// prettier-ignore
const StackAlertRequired = rt.type({
});
// prettier-ignore
const StackAlertOptional = rt.partial({
'kibana.alert.evaluation.conditions': schemaString,
'kibana.alert.evaluation.threshold': schemaStringOrNumber,

View file

@ -14,6 +14,7 @@ import type { ObservabilitySloAlert } from './generated/observability_slo_schema
import type { ObservabilityUptimeAlert } from './generated/observability_uptime_schema';
import type { SecurityAlert } from './generated/security_schema';
import type { MlAnomalyDetectionAlert } from './generated/ml_anomaly_detection_schema';
import type { DefaultAlert } from './generated/default_schema';
export * from './create_schema_from_field_map';
@ -26,6 +27,7 @@ export type { ObservabilityUptimeAlert } from './generated/observability_uptime_
export type { SecurityAlert } from './generated/security_schema';
export type { StackAlert } from './generated/stack_schema';
export type { MlAnomalyDetectionAlert } from './generated/ml_anomaly_detection_schema';
export type { DefaultAlert } from './generated/default_schema';
export type AADAlert =
| Alert
@ -35,4 +37,5 @@ export type AADAlert =
| ObservabilitySloAlert
| ObservabilityUptimeAlert
| SecurityAlert
| MlAnomalyDetectionAlert;
| MlAnomalyDetectionAlert
| DefaultAlert;

View file

@ -25,6 +25,7 @@ export const AlertConsumers = {
UPTIME: 'uptime',
ML: 'ml',
STACK_ALERTS: 'stackAlerts',
EXAMPLE: 'AlertingExample',
} as const;
export type AlertConsumers = typeof AlertConsumers[keyof typeof AlertConsumers];
export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed' | 'in-progress'; // TODO: remove 'in-progress' after migration to 'acknowledged'

View file

@ -12,9 +12,9 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
import { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/server';
import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
import { alertType as alwaysFiringAlert } from './alert_types/always_firing';
import { alertType as peopleInSpaceAlert } from './alert_types/astros';
import { alertType as patternAlert } from './alert_types/pattern';
import { ruleType as alwaysFiringRule } from './rule_types/always_firing';
import { ruleType as peopleInSpaceRule } from './rule_types/astros';
import { ruleType as patternRule } from './rule_types/pattern';
// can't import static code from another plugin to support examples functional test
const INDEX_THRESHOLD_ID = '.index-threshold';
import { ALERTING_EXAMPLE_APP_ID } from '../common/constants';
@ -27,9 +27,9 @@ export interface AlertingExampleDeps {
export class AlertingExamplePlugin implements Plugin<void, void, AlertingExampleDeps> {
public setup(core: CoreSetup, { alerting, features }: AlertingExampleDeps) {
alerting.registerType(alwaysFiringAlert);
alerting.registerType(peopleInSpaceAlert);
alerting.registerType(patternAlert);
alerting.registerType(alwaysFiringRule);
alerting.registerType(peopleInSpaceRule);
alerting.registerType(patternRule);
features.registerKibanaFeature({
id: ALERTING_EXAMPLE_APP_ID,
@ -41,15 +41,15 @@ export class AlertingExamplePlugin implements Plugin<void, void, AlertingExample
insightsAndAlerting: ['triggersActions'],
},
category: DEFAULT_APP_CATEGORIES.management,
alerting: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
alerting: [alwaysFiringRule.id, peopleInSpaceRule.id, INDEX_THRESHOLD_ID],
privileges: {
all: {
alerting: {
rule: {
all: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
all: [alwaysFiringRule.id, peopleInSpaceRule.id, INDEX_THRESHOLD_ID],
},
alert: {
all: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
all: [alwaysFiringRule.id, peopleInSpaceRule.id, INDEX_THRESHOLD_ID],
},
},
savedObject: {
@ -64,10 +64,10 @@ export class AlertingExamplePlugin implements Plugin<void, void, AlertingExample
read: {
alerting: {
rule: {
read: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
read: [alwaysFiringRule.id, peopleInSpaceRule.id, INDEX_THRESHOLD_ID],
},
alert: {
read: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
read: [alwaysFiringRule.id, peopleInSpaceRule.id, INDEX_THRESHOLD_ID],
},
},
savedObject: {

View file

@ -7,8 +7,14 @@
import { v4 as uuidv4 } from 'uuid';
import { range } from 'lodash';
import { RuleType } from '@kbn/alerting-plugin/server';
import {
DEFAULT_AAD_CONFIG,
RuleType,
RuleTypeState,
AlertsClientError,
} from '@kbn/alerting-plugin/server';
import { schema } from '@kbn/config-schema';
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
import {
DEFAULT_INSTANCES_TO_GENERATE,
ALERTING_EXAMPLE_APP_ID,
@ -17,6 +23,12 @@ import {
} from '../../common/constants';
type ActionGroups = 'small' | 'medium' | 'large';
interface State extends RuleTypeState {
count?: number;
}
interface AlertState {
triggerdOnCycle: number;
}
const DEFAULT_ACTION_GROUP: ActionGroups = 'small';
function getTShirtSizeByIdAndThreshold(
@ -38,13 +50,15 @@ function getTShirtSizeByIdAndThreshold(
return DEFAULT_ACTION_GROUP;
}
export const alertType: RuleType<
export const ruleType: RuleType<
AlwaysFiringParams,
never,
{ count?: number },
{ triggerdOnCycle: number },
State,
AlertState,
never,
AlwaysFiringActionGroupIds
AlwaysFiringActionGroupIds,
never,
DefaultAlert
> = {
id: 'example.always-firing',
name: 'Always firing',
@ -61,15 +75,20 @@ export const alertType: RuleType<
params: { instances = DEFAULT_INSTANCES_TO_GENERATE, thresholds },
state,
}) {
const { alertsClient } = services;
if (!alertsClient) {
throw new AlertsClientError();
}
const count = (state.count ?? 0) + 1;
range(instances)
.map(() => uuidv4())
.forEach((id: string) => {
services.alertFactory
.create(id)
.replaceState({ triggerdOnCycle: count })
.scheduleActions(getTShirtSizeByIdAndThreshold(id, thresholds));
alertsClient.report({
id,
actionGroup: getTShirtSizeByIdAndThreshold(id, thresholds),
state: { triggerdOnCycle: count },
});
});
return {
@ -92,4 +111,5 @@ export const alertType: RuleType<
),
}),
},
alerts: DEFAULT_AAD_CONFIG,
};

View file

@ -6,8 +6,15 @@
*/
import axios from 'axios';
import { RuleType } from '@kbn/alerting-plugin/server';
import {
DEFAULT_AAD_CONFIG,
RuleType,
RuleTypeParams,
RuleTypeState,
AlertsClientError,
} from '@kbn/alerting-plugin/server';
import { schema } from '@kbn/config-schema';
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
import { Operator, Craft, ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
interface PeopleInSpace {
@ -18,6 +25,18 @@ interface PeopleInSpace {
number: number;
}
interface Params extends RuleTypeParams {
outerSpaceCapacity: number;
craft: string;
op: string;
}
interface State extends RuleTypeState {
peopleInSpace: number;
}
interface AlertState {
craft: string;
}
function getOperator(op: string) {
switch (op) {
case Operator.AreAbove:
@ -40,14 +59,15 @@ function getCraftFilter(craft: string) {
craft === Craft.OuterSpace ? true : craft === person.craft;
}
export const alertType: RuleType<
{ outerSpaceCapacity: number; craft: string; op: string },
export const ruleType: RuleType<
Params,
never,
{ peopleInSpace: number },
{ craft: string },
State,
AlertState,
never,
'default',
'hasLandedBackOnEarth'
'hasLandedBackOnEarth',
DefaultAlert
> = {
id: 'example.people-in-space',
name: 'People In Space Right Now',
@ -60,6 +80,10 @@ export const alertType: RuleType<
name: 'Has landed back on Earth',
},
async executor({ services, params }) {
const { alertsClient } = services;
if (!alertsClient) {
throw new AlertsClientError();
}
const { outerSpaceCapacity, craft: craftToTriggerBy, op } = params;
const response = await axios.get<PeopleInSpace>('http://api.open-notify.org/astros.json');
@ -71,7 +95,7 @@ export const alertType: RuleType<
if (getOperator(op)(peopleInCraft.length, outerSpaceCapacity)) {
peopleInCraft.forEach(({ craft, name }) => {
services.alertFactory.create(name).replaceState({ craft }).scheduleActions('default');
alertsClient.report({ id: name, actionGroup: 'default', state: { craft } });
});
}
@ -86,6 +110,7 @@ export const alertType: RuleType<
getViewInAppRelativeUrl({ rule }) {
return `/app/${ALERTING_EXAMPLE_APP_ID}/astros/${rule.id}`;
},
alerts: DEFAULT_AAD_CONFIG,
validate: {
params: schema.object({
outerSpaceCapacity: schema.number(),

View file

@ -6,7 +6,7 @@
*/
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { alertType } from './pattern';
import { ruleType } from './pattern';
const logger = loggingSystemMock.create().get();
@ -22,9 +22,10 @@ describe('pattern rule type', () => {
const options = {
params,
state,
services: { alertsClient: {} },
};
try {
await alertType.executor(options as any);
await ruleType.executor(options as any);
} catch (err) {
expect(err.message).toMatchInlineSnapshot(
`"errors in patterns: pattern for instA contains invalid string: \\"nope\\", pattern for instB contains invalid string: \\"hallo!\\""`
@ -50,7 +51,7 @@ describe('pattern rule type', () => {
let result: any;
for (let i = 0; i < 6; i++) {
result = await alertType.executor(options as any);
result = await ruleType.executor(options as any);
options.state = result.state;
}
@ -182,20 +183,15 @@ describe('pattern rule type', () => {
});
});
// services.alertFactory.create(instance).scheduleActions('default', context);
type ScheduledAction = [string, string, any];
function getServices() {
const scheduledActions: ScheduledAction[] = [];
return {
scheduledActions,
alertFactory: {
create(instance: string) {
return {
scheduleActions(actionGroup: string, context: any) {
scheduledActions.push([instance, actionGroup, context]);
},
};
alertsClient: {
report(reported: { id: string; actionGroup: string; context: any }) {
scheduledActions.push([reported.id, reported.actionGroup, reported.context]);
},
},
};

View file

@ -10,7 +10,11 @@ import {
RuleType as BaseRuleType,
RuleTypeState,
RuleExecutorOptions as BaseRuleExecutorOptions,
DEFAULT_AAD_CONFIG,
AlertsClientError,
} from '@kbn/alerting-plugin/server';
import type { DefaultAlert } from '@kbn/alerts-as-data-utils';
import { RecoveredActionGroupId } from '@kbn/alerting-plugin/common';
type Params = TypeOf<typeof Params>;
const Params = schema.object(
@ -41,10 +45,19 @@ interface State extends RuleTypeState {
runs?: number;
}
type RuleExecutorOptions = BaseRuleExecutorOptions<Params, State, {}, {}, 'default'>;
type RuleExecutorOptions = BaseRuleExecutorOptions<Params, State, {}, {}, 'default', DefaultAlert>;
type RuleType = BaseRuleType<Params, never, State, {}, {}, 'default'>;
export const alertType: RuleType = getPatternRuleType();
type RuleType = BaseRuleType<
Params,
never,
State,
{},
{},
'default',
RecoveredActionGroupId,
DefaultAlert
>;
export const ruleType: RuleType = getPatternRuleType();
function getPatternRuleType(): RuleType {
return {
@ -57,6 +70,7 @@ function getPatternRuleType(): RuleType {
minimumLicenseRequired: 'basic',
isExportable: true,
executor,
alerts: DEFAULT_AAD_CONFIG,
validate: {
params: Params,
},
@ -65,6 +79,10 @@ function getPatternRuleType(): RuleType {
async function executor(options: RuleExecutorOptions): Promise<{ state: State }> {
const { services, state, params } = options;
const { alertsClient } = services;
if (!alertsClient) {
throw new AlertsClientError();
}
if (state.runs == null) {
state.runs = 0;
@ -96,7 +114,7 @@ async function executor(options: RuleExecutorOptions): Promise<{ state: State }>
switch (action) {
case 'a':
const context = { patternIndex, action, pattern, runs };
services.alertFactory.create(instance).scheduleActions('default', context);
alertsClient.report({ id: instance, actionGroup: 'default', context });
break;
case '-':
break;

View file

@ -27,5 +27,6 @@
"@kbn/core-application-common",
"@kbn/shared-ux-router",
"@kbn/config-schema",
"@kbn/alerts-as-data-utils",
]
}

View file

@ -34,6 +34,7 @@ export type {
GetViewInAppRelativeUrlFnOpts,
DataStreamAdapter,
} from './types';
export { DEFAULT_AAD_CONFIG } from './types';
export { RULE_SAVED_OBJECT_TYPE } from './saved_objects';
export { RuleNotifyWhen } from '../common';
export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config';

View file

@ -16,6 +16,7 @@ describe('rule_type_registry_deprecated_consumers', () => {
expect(Object.keys(ruleTypeIdWithValidLegacyConsumers)).toMatchInlineSnapshot(`
Array [
"example.always-firing",
"example.people-in-space",
"transform_health",
".index-threshold",
".geo-containment",

View file

@ -9,6 +9,7 @@ import { ALERTS_FEATURE_ID } from './types';
export const ruleTypeIdWithValidLegacyConsumers: Record<string, string[]> = {
'example.always-firing': [ALERTS_FEATURE_ID],
'example.people-in-space': [ALERTS_FEATURE_ID],
transform_health: [ALERTS_FEATURE_ID],
'.index-threshold': [ALERTS_FEATURE_ID],
'.geo-containment': [ALERTS_FEATURE_ID],

View file

@ -22,7 +22,7 @@ import {
} from '@kbn/core/server';
import type { PublicMethodsOf } from '@kbn/utility-types';
import { SharePluginStart } from '@kbn/share-plugin/server';
import type { FieldMap } from '@kbn/alerts-as-data-utils';
import type { DefaultAlert, FieldMap } from '@kbn/alerts-as-data-utils';
import { Alert } from '@kbn/alerts-as-data-utils';
import { Filter } from '@kbn/es-query';
import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry';
@ -209,6 +209,12 @@ export type FormatAlert<AlertData extends RuleAlertData> = (
alert: Partial<AlertData>
) => Partial<AlertData>;
export const DEFAULT_AAD_CONFIG: IRuleTypeAlerts<DefaultAlert> = {
context: `default`,
mappings: { fieldMap: {} },
shouldWrite: true,
};
export interface IRuleTypeAlerts<AlertData extends RuleAlertData = never> {
/**
* Specifies the target alerts-as-data resource