-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Alerting] formalize alert status and add status fields to alert save…
…d object (#75553) resolves #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. The data is added to the alert as the `executionStatus` field, with the following shape: ```ts interface AlertExecutionStatus { status: 'ok' | 'active' | 'error' | 'pending' | 'unknown'; lastExecutionDate: Date; error?: { reason: 'read' | 'decrypt' | 'execute' | 'unknown'; message: string; }; } ```
- Loading branch information
Showing
50 changed files
with
1,176 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
185 changes: 185 additions & 0 deletions
185
x-pack/plugins/alerts/server/lib/alert_execution_status.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { loggingSystemMock } from '../../../../../src/core/server/mocks'; | ||
import { AlertExecutionStatusErrorReasons } from '../types'; | ||
import { | ||
executionStatusFromState, | ||
executionStatusFromError, | ||
alertExecutionStatusToRaw, | ||
alertExecutionStatusFromRaw, | ||
} from './alert_execution_status'; | ||
import { ErrorWithReason } from './error_with_reason'; | ||
|
||
const MockLogger = loggingSystemMock.create().get(); | ||
|
||
describe('AlertExecutionStatus', () => { | ||
beforeEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
describe('executionStatusFromState()', () => { | ||
test('empty task state', () => { | ||
const status = executionStatusFromState({}); | ||
checkDateIsNearNow(status.lastExecutionDate); | ||
expect(status.status).toBe('ok'); | ||
expect(status.error).toBe(undefined); | ||
}); | ||
|
||
test('task state with no instances', () => { | ||
const status = executionStatusFromState({ alertInstances: {} }); | ||
checkDateIsNearNow(status.lastExecutionDate); | ||
expect(status.status).toBe('ok'); | ||
expect(status.error).toBe(undefined); | ||
}); | ||
|
||
test('task state with one instance', () => { | ||
const status = executionStatusFromState({ alertInstances: { a: {} } }); | ||
checkDateIsNearNow(status.lastExecutionDate); | ||
expect(status.status).toBe('active'); | ||
expect(status.error).toBe(undefined); | ||
}); | ||
}); | ||
|
||
describe('executionStatusFromError()', () => { | ||
test('error with no reason', () => { | ||
const status = executionStatusFromError(new Error('boo!')); | ||
expect(status.status).toBe('error'); | ||
expect(status.error).toMatchInlineSnapshot(` | ||
Object { | ||
"message": "boo!", | ||
"reason": "unknown", | ||
} | ||
`); | ||
}); | ||
|
||
test('error with a reason', () => { | ||
const status = executionStatusFromError(new ErrorWithReason('execute', new Error('hoo!'))); | ||
expect(status.status).toBe('error'); | ||
expect(status.error).toMatchInlineSnapshot(` | ||
Object { | ||
"message": "hoo!", | ||
"reason": "execute", | ||
} | ||
`); | ||
}); | ||
}); | ||
|
||
describe('alertExecutionStatusToRaw()', () => { | ||
const date = new Date('2020-09-03T16:26:58Z'); | ||
const status = 'ok'; | ||
const reason: AlertExecutionStatusErrorReasons = 'decrypt'; | ||
const error = { reason, message: 'wops' }; | ||
|
||
test('status without an error', () => { | ||
expect(alertExecutionStatusToRaw({ lastExecutionDate: date, status })).toMatchInlineSnapshot(` | ||
Object { | ||
"error": null, | ||
"lastExecutionDate": "2020-09-03T16:26:58.000Z", | ||
"status": "ok", | ||
} | ||
`); | ||
}); | ||
|
||
test('status with an error', () => { | ||
expect(alertExecutionStatusToRaw({ lastExecutionDate: date, status, error })) | ||
.toMatchInlineSnapshot(` | ||
Object { | ||
"error": Object { | ||
"message": "wops", | ||
"reason": "decrypt", | ||
}, | ||
"lastExecutionDate": "2020-09-03T16:26:58.000Z", | ||
"status": "ok", | ||
} | ||
`); | ||
}); | ||
}); | ||
|
||
describe('alertExecutionStatusFromRaw()', () => { | ||
const date = new Date('2020-09-03T16:26:58Z').toISOString(); | ||
const status = 'active'; | ||
const reason: AlertExecutionStatusErrorReasons = 'execute'; | ||
const error = { reason, message: 'wops' }; | ||
|
||
test('no input', () => { | ||
const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id'); | ||
expect(result).toBe(undefined); | ||
}); | ||
|
||
test('undefined input', () => { | ||
const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id', undefined); | ||
expect(result).toBe(undefined); | ||
}); | ||
|
||
test('null input', () => { | ||
const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id', null); | ||
expect(result).toBe(undefined); | ||
}); | ||
|
||
test('invalid date', () => { | ||
const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id', { | ||
lastExecutionDate: 'an invalid date', | ||
})!; | ||
checkDateIsNearNow(result.lastExecutionDate); | ||
expect(result.status).toBe('unknown'); | ||
expect(result.error).toBe(undefined); | ||
expect(MockLogger.debug).toBeCalledWith( | ||
'invalid alertExecutionStatus lastExecutionDate "an invalid date" in raw alert alert-id' | ||
); | ||
}); | ||
|
||
test('valid date', () => { | ||
const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id', { | ||
lastExecutionDate: date, | ||
}); | ||
expect(result).toMatchInlineSnapshot(` | ||
Object { | ||
"lastExecutionDate": 2020-09-03T16:26:58.000Z, | ||
"status": "unknown", | ||
} | ||
`); | ||
}); | ||
|
||
test('valid status and date', () => { | ||
const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id', { | ||
status, | ||
lastExecutionDate: date, | ||
}); | ||
expect(result).toMatchInlineSnapshot(` | ||
Object { | ||
"lastExecutionDate": 2020-09-03T16:26:58.000Z, | ||
"status": "active", | ||
} | ||
`); | ||
}); | ||
|
||
test('valid status, date and error', () => { | ||
const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id', { | ||
status, | ||
lastExecutionDate: date, | ||
error, | ||
}); | ||
expect(result).toMatchInlineSnapshot(` | ||
Object { | ||
"error": Object { | ||
"message": "wops", | ||
"reason": "execute", | ||
}, | ||
"lastExecutionDate": 2020-09-03T16:26:58.000Z, | ||
"status": "active", | ||
} | ||
`); | ||
}); | ||
}); | ||
}); | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
function checkDateIsNearNow(date: any) { | ||
expect(date instanceof Date).toBe(true); | ||
// allow for lots of slop in the time difference | ||
expect(Date.now() - date.valueOf()).toBeLessThanOrEqual(10000); | ||
} |
66 changes: 66 additions & 0 deletions
66
x-pack/plugins/alerts/server/lib/alert_execution_status.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { Logger } from 'src/core/server'; | ||
import { AlertTaskState, AlertExecutionStatus, RawAlertExecutionStatus } from '../types'; | ||
import { getReasonFromError } from './error_with_reason'; | ||
|
||
export function executionStatusFromState(state: AlertTaskState): AlertExecutionStatus { | ||
const instanceIds = Object.keys(state.alertInstances ?? {}); | ||
return { | ||
lastExecutionDate: new Date(), | ||
status: instanceIds.length === 0 ? 'ok' : 'active', | ||
}; | ||
} | ||
|
||
export function executionStatusFromError(error: Error): AlertExecutionStatus { | ||
return { | ||
lastExecutionDate: new Date(), | ||
status: 'error', | ||
error: { | ||
reason: getReasonFromError(error), | ||
message: error.message, | ||
}, | ||
}; | ||
} | ||
|
||
export function alertExecutionStatusToRaw({ | ||
lastExecutionDate, | ||
status, | ||
error, | ||
}: AlertExecutionStatus): RawAlertExecutionStatus { | ||
return { | ||
lastExecutionDate: lastExecutionDate.toISOString(), | ||
status, | ||
// explicitly setting to null (in case undefined) due to partial update concerns | ||
error: error ?? null, | ||
}; | ||
} | ||
|
||
export function alertExecutionStatusFromRaw( | ||
logger: Logger, | ||
alertId: string, | ||
rawAlertExecutionStatus?: Partial<RawAlertExecutionStatus> | null | undefined | ||
): AlertExecutionStatus | undefined { | ||
if (!rawAlertExecutionStatus) return undefined; | ||
|
||
const { lastExecutionDate, status = 'unknown', error } = rawAlertExecutionStatus; | ||
|
||
let parsedDateMillis = lastExecutionDate ? Date.parse(lastExecutionDate) : Date.now(); | ||
if (isNaN(parsedDateMillis)) { | ||
logger.debug( | ||
`invalid alertExecutionStatus lastExecutionDate "${lastExecutionDate}" in raw alert ${alertId}` | ||
); | ||
parsedDateMillis = Date.now(); | ||
} | ||
|
||
const parsedDate = new Date(parsedDateMillis); | ||
if (error) { | ||
return { lastExecutionDate: parsedDate, status, error }; | ||
} else { | ||
return { lastExecutionDate: parsedDate, status }; | ||
} | ||
} |
Oops, something went wrong.