Skip to content

Commit

Permalink
[Alerting] formalize alert status and add status fields to alert save…
Browse files Browse the repository at this point in the history
…d object

resolves elastic#51099

This formalizes the concept of "alert status", in terms of it's execution, with
some new fields in the alert saved object and types used with the alert client
and http APIs.

These fields are read-only from the client point-of-view; they are provided in
the alert structures, but are only updated by the alerting framework itself.
The values will be updated after each run of the alert type executor.

interim commits:

calculate the execution status, some refactoring
write the execution status to the alert after execution
use real date in execution status on create
add an await to an async fn
comment out status update to see if SIEM FT succeeds
fix SIEM FT alert deletion issue
use partial updates and retries in alerts clients to avoid conflicts
fix jest tests
clean up conflict-fixin code
moar conflict-prevention fixing
fix type error with find result
add reasons to alert execution errors
add some jest tests
add some function tests
fix status update to use alert namespace
fix function test
finish function tests
more fixes after rebase
fix type checks and jest tests after rebase
  • Loading branch information
pmuellr committed Sep 28, 2020
1 parent 97fd0df commit 65a24bd
Show file tree
Hide file tree
Showing 49 changed files with 1,093 additions and 43 deletions.
13 changes: 13 additions & 0 deletions x-pack/plugins/alerts/common/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export interface IntervalSchedule extends SavedObjectAttributes {
interval: string;
}

export type AlertExecutionStatuses = 'ok' | 'active' | 'error' | 'unknown';
export type AlertExecutionStatusErrorReasons = 'read' | 'decrypt' | 'execute' | 'unknown';

export interface AlertExecutionStatus {
status: AlertExecutionStatuses;
date: Date;
error?: {
reason: AlertExecutionStatusErrorReasons;
message: string;
};
}

export type AlertActionParams = SavedObjectAttributes;

export interface AlertAction {
Expand Down Expand Up @@ -44,6 +56,7 @@ export interface Alert {
throttle: string | null;
muteAll: boolean;
mutedInstanceIds: string[];
executionStatus: AlertExecutionStatus;
}

export type SanitizedAlert = Omit<Alert, 'apiKey'>;
52 changes: 52 additions & 0 deletions x-pack/plugins/alerts/server/alerts_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ describe('create()', () => {
"createdAt": 2019-02-12T21:01:22.479Z,
"createdBy": "elastic",
"enabled": true,
"executionStatus": Object {
"date": 2019-02-12T21:01:22.479Z,
"status": "unknown",
},
"id": "1",
"muteAll": false,
"mutedInstanceIds": Array [],
Expand Down Expand Up @@ -393,6 +397,11 @@ describe('create()', () => {
"createdAt": "2019-02-12T21:01:22.479Z",
"createdBy": "elastic",
"enabled": true,
"executionStatus": Object {
"date": "2019-02-12T21:01:22.479Z",
"error": null,
"status": "unknown",
},
"meta": Object {
"versionApiKeyLastmodified": "v7.10.0",
},
Expand Down Expand Up @@ -588,6 +597,10 @@ describe('create()', () => {
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"executionStatus": Object {
"date": 2019-02-12T21:01:22.479Z,
"status": "unknown",
},
"id": "1",
"params": Object {
"bar": true,
Expand Down Expand Up @@ -662,6 +675,10 @@ describe('create()', () => {
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": false,
"executionStatus": Object {
"date": 2019-02-12T21:01:22.479Z,
"status": "unknown",
},
"id": "1",
"params": Object {
"bar": true,
Expand Down Expand Up @@ -1029,6 +1046,11 @@ describe('create()', () => {
muteAll: false,
mutedInstanceIds: [],
tags: ['foo'],
executionStatus: {
date: '2019-02-12T21:01:22.479Z',
status: 'unknown',
error: null,
},
},
{
references: [
Expand Down Expand Up @@ -1145,6 +1167,11 @@ describe('create()', () => {
muteAll: false,
mutedInstanceIds: [],
tags: ['foo'],
executionStatus: {
date: '2019-02-12T21:01:22.479Z',
status: 'unknown',
error: null,
},
},
{
references: [
Expand Down Expand Up @@ -2159,6 +2186,10 @@ describe('get()', () => {
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"executionStatus": Object {
"date": 2019-02-12T21:01:22.479Z,
"status": "unknown",
},
"id": "1",
"params": Object {
"bar": true,
Expand Down Expand Up @@ -2481,6 +2512,11 @@ const BaseAlertInstanceSummarySavedObject: SavedObject<RawAlert> = {
throttle: null,
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'unknown',
date: '2020-08-20T19:23:38Z',
error: null,
},
},
references: [],
};
Expand Down Expand Up @@ -2772,6 +2808,10 @@ describe('find()', () => {
],
"alertTypeId": "myType",
"createdAt": 2019-02-12T21:01:22.479Z,
"executionStatus": Object {
"date": 2019-02-12T21:01:22.479Z,
"status": "unknown",
},
"id": "1",
"params": Object {
"bar": true,
Expand Down Expand Up @@ -3219,6 +3259,10 @@ describe('update()', () => {
],
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": true,
"executionStatus": Object {
"date": 2019-02-12T21:01:22.479Z,
"status": "unknown",
},
"id": "1",
"params": Object {
"bar": true,
Expand Down Expand Up @@ -3400,6 +3444,10 @@ describe('update()', () => {
"apiKey": "MTIzOmFiYw==",
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": true,
"executionStatus": Object {
"date": 2019-02-12T21:01:22.479Z,
"status": "unknown",
},
"id": "1",
"params": Object {
"bar": true,
Expand Down Expand Up @@ -3555,6 +3603,10 @@ describe('update()', () => {
"apiKey": null,
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": false,
"executionStatus": Object {
"date": 2019-02-12T21:01:22.479Z,
"status": "unknown",
},
"id": "1",
"params": Object {
"bar": true,
Expand Down
74 changes: 72 additions & 2 deletions x-pack/plugins/alerts/server/alerts_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
SavedObjectReference,
SavedObject,
PluginInitializerContext,
} from 'src/core/server';
ISavedObjectsRepository,
SavedObjectsErrorHelpers,
} from '../../../../src/core/server';
import { esKuery } from '../../../../src/plugins/data/server';
import { ActionsClient, ActionsAuthorization } from '../../actions/server';
import {
Expand All @@ -27,6 +29,8 @@ import {
SanitizedAlert,
AlertTaskState,
AlertInstanceSummary,
AlertExecutionStatus,
RawAlertExecutionStatus,
} from './types';
import { validateAlertTypeParams } from './lib';
import {
Expand All @@ -45,6 +49,10 @@ import { parseIsoOrRelativeDate } from './lib/iso_or_relative_date';
import { alertInstanceSummaryFromEventLog } from './lib/alert_instance_summary_from_event_log';
import { IEvent } from '../../event_log/server';
import { parseDuration } from '../common/parse_duration';
import {
AlertAttributesExcludedFromAAD,
AlertAttributesExcludedFromAADType,
} from './saved_objects';

export interface RegistryAlertTypeWithAuth extends RegistryAlertType {
authorizedConsumers: string[];
Expand Down Expand Up @@ -120,6 +128,7 @@ export interface CreateOptions {
| 'muteAll'
| 'mutedInstanceIds'
| 'actions'
| 'executionStatus'
> & { actions: NormalizedAlertAction[] };
options?: {
migrationVersion?: Record<string, string>;
Expand Down Expand Up @@ -226,6 +235,11 @@ export class AlertsClient {
params: validatedAlertTypeParams as RawAlert['params'],
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'unknown',
date: new Date().toISOString(),
error: null,
},
};
const createdAlert = await this.unsecuredSavedObjectsClient.create(
'alert',
Expand Down Expand Up @@ -363,12 +377,20 @@ export class AlertsClient {
// eslint-disable-next-line @typescript-eslint/naming-convention
const authorizedData = data.map(({ id, attributes, updated_at, references }) => {
ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer);
return this.getAlertFromRaw(
const alert: Partial<Alert> = this.getAlertFromRaw(
id,
fields ? (pick(attributes, fields) as RawAlert) : attributes,
updated_at,
references
);
// executionStatus is a possibly added field by getAlertFromRaw(), so we
// need to remove it if it wasn't listed in the fields
if (fields && !fields.includes('executionStatus')) {
delete alert.executionStatus;
}
// this is bad and wrong and will be fixed some day, see:
// https:/elastic/kibana/issues/76527
return alert as Alert;
});

logSuccessfulAuthorization();
Expand Down Expand Up @@ -881,12 +903,14 @@ export class AlertsClient {
updatedAt: SavedObject['updated_at'] = createdAt,
references: SavedObjectReference[] | undefined
): PartialAlert {
const executionStatus = this.rawAlertToExecutionStatus(rawAlert);
return {
id,
...rawAlert,
// we currently only support the Interval Schedule type
// Once we support additional types, this type signature will likely change
schedule: rawAlert.schedule as IntervalSchedule,
executionStatus,
actions: rawAlert.actions
? this.injectReferencesIntoActions(id, rawAlert.actions, references || [])
: [],
Expand All @@ -896,6 +920,22 @@ export class AlertsClient {
};
}

private rawAlertToExecutionStatus(rawAlert: Partial<RawAlert> = {}): AlertExecutionStatus {
const rawExecutionStatus: Partial<RawAlertExecutionStatus> = rawAlert.executionStatus || {};
const { status: rawStatus, date: rawDate, error: rawError } = rawExecutionStatus;

const result: AlertExecutionStatus = {
status: rawStatus || 'unknown',
date: rawDate ? new Date(rawDate) : new Date(),
};

if (rawError) {
result.error = rawError;
}

return result;
}

private validateActions(alertType: AlertType, actions: NormalizedAlertAction[]): void {
const { actionGroups: alertTypeActionGroups } = alertType;
const usedAlertActionGroups = actions.map((action) => action.group);
Expand Down Expand Up @@ -970,6 +1010,36 @@ export class AlertsClient {
}
}

type SavedObjectClient = SavedObjectsClientContract | ISavedObjectsRepository;

type PartiallyUpdateableAlertAttributes = Partial<
Pick<RawAlert, AlertAttributesExcludedFromAADType>
>;

interface PartiallyUpdateAlertSavedObjectOptions {
ignore404?: boolean;
namespace?: string; // only should be used with ISavedObjectsRepository
}

// Specialized partial update for fields which do not contribute to AAD.
export async function partiallyUpdateAlertSavedObject(
savedObjectsClient: SavedObjectClient,
id: string,
attributes: PartiallyUpdateableAlertAttributes,
options?: PartiallyUpdateAlertSavedObjectOptions
): Promise<void> {
const attributeUpdates = pick(attributes, AlertAttributesExcludedFromAAD);
const updateOptions = options?.namespace ? { namespace: options.namespace } : {};
try {
await savedObjectsClient.update<RawAlert>('alert', id, attributeUpdates, updateOptions);
} catch (err) {
if (options?.ignore404 && SavedObjectsErrorHelpers.isNotFoundError(err)) {
return;
}
throw err;
}
}

function parseDate(dateString: string | undefined, propertyName: string, defaultValue: Date): Date {
if (dateString === undefined) {
return defaultValue;
Expand Down
Loading

0 comments on commit 65a24bd

Please sign in to comment.