-
Notifications
You must be signed in to change notification settings - Fork 8.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Alerting] formalize alert status and add status fields to alert saved object #75553
Changes from 3 commits
b33d556
5edacca
95fcc5a
c366d4b
860647f
599a72e
8775610
bf71148
3d68a1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -393,6 +393,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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Am I right that we set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yup, it's a partial update issue; we need to make sure we remove a previous error if we're doing an update and there is no error this time. So it's typed in the raw alert as "null-able". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add a comment in the raw alert definition for this ... |
||
"status": "unknown", | ||
}, | ||
"meta": Object { | ||
"versionApiKeyLastmodified": "v7.10.0", | ||
}, | ||
|
@@ -1029,6 +1034,11 @@ describe('create()', () => { | |
muteAll: false, | ||
mutedInstanceIds: [], | ||
tags: ['foo'], | ||
executionStatus: { | ||
date: '2019-02-12T21:01:22.479Z', | ||
status: 'unknown', | ||
error: null, | ||
}, | ||
}, | ||
{ | ||
references: [ | ||
|
@@ -1145,6 +1155,11 @@ describe('create()', () => { | |
muteAll: false, | ||
mutedInstanceIds: [], | ||
tags: ['foo'], | ||
executionStatus: { | ||
date: '2019-02-12T21:01:22.479Z', | ||
status: 'unknown', | ||
error: null, | ||
}, | ||
}, | ||
{ | ||
references: [ | ||
|
@@ -2496,6 +2511,11 @@ const BaseAlertInstanceSummarySavedObject: SavedObject<RawAlert> = { | |
throttle: null, | ||
muteAll: false, | ||
mutedInstanceIds: [], | ||
executionStatus: { | ||
status: 'unknown', | ||
date: '2020-08-20T19:23:38Z', | ||
error: null, | ||
}, | ||
}, | ||
references: [], | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,7 @@ import { | |
AlertTaskState, | ||
AlertInstanceSummary, | ||
} from './types'; | ||
import { validateAlertTypeParams } from './lib'; | ||
import { validateAlertTypeParams, alertExecutionStatusFromRaw } from './lib'; | ||
import { | ||
InvalidateAPIKeyParams, | ||
GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, | ||
|
@@ -122,6 +122,7 @@ export interface CreateOptions { | |
| 'muteAll' | ||
| 'mutedInstanceIds' | ||
| 'actions' | ||
| 'executionStatus' | ||
Comment on lines
122
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're not wrong, but I hate to make changes like this, in a PR like this. It's more of a general clean up thing, and this particular item seems hardly worth an issue by itself. Do we have some general "simple tech debt items" issue we could add this to? |
||
> & { actions: NormalizedAlertAction[] }; | ||
options?: { | ||
migrationVersion?: Record<string, string>; | ||
|
@@ -228,6 +229,11 @@ export class AlertsClient { | |
params: validatedAlertTypeParams as RawAlert['params'], | ||
muteAll: false, | ||
mutedInstanceIds: [], | ||
executionStatus: { | ||
status: 'unknown', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. It feels like we should be using enums for these rather than hard coded strings, no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kinda look at the "set of these strings" type as being the ceremony-free version of string enums. Is there an explicit value in using enums instead? The upside to not using them is not having to maintain the duplication of the enum keys / values, and not having to have access to that enum type in code that needs it. ie, less ceremony :-) Would the enum type be exposed in the Alert objects themselves, or just an internal detail? I think we could type it in the Alert and RawAlert as enum safely, but not quite sure. Another reason I generally avoid enums, because I always have to go read the chapter on them in the TS handbook :-) |
||
date: new Date().toISOString(), | ||
error: null, | ||
}, | ||
}; | ||
const createdAlert = await this.unsecuredSavedObjectsClient.create( | ||
'alert', | ||
|
@@ -961,9 +967,14 @@ export class AlertsClient { | |
updatedAt: SavedObject['updated_at'] = createdAt, | ||
references: SavedObjectReference[] | undefined | ||
): PartialAlert { | ||
const rawAlertWithoutExecutionStatus: Partial<Omit<RawAlert, 'executionStatus'>> = { | ||
...rawAlert, | ||
}; | ||
delete rawAlertWithoutExecutionStatus.executionStatus; | ||
const executionStatus = alertExecutionStatusFromRaw(rawAlert.executionStatus); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. It just feels more in line with the rest of the code there There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was sooooo hard - I tried multiple approaches to this, including doing what you suggested. Not happy with that The main problem is the types of the props in the RawAlert and Alert pretty much match up, so that It's worth a comment I think, will help the next person looking at this save a few minutes when they try to fix it. heh |
||
return { | ||
id, | ||
...rawAlert, | ||
...rawAlertWithoutExecutionStatus, | ||
// we currently only support the Interval Schedule type | ||
// Once we support additional types, this type signature will likely change | ||
schedule: rawAlert.schedule as IntervalSchedule, | ||
|
@@ -973,6 +984,7 @@ export class AlertsClient { | |
...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}), | ||
...(createdAt ? { createdAt: new Date(createdAt) } : {}), | ||
...(scheduledTaskId ? { scheduledTaskId } : {}), | ||
...(executionStatus ? { executionStatus } : {}), | ||
}; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
/* | ||
* 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 { AlertExecutionStatusErrorReasons } from '../types'; | ||
import { | ||
executionStatusFromState, | ||
executionStatusFromError, | ||
alertExecutionStatusToRaw, | ||
alertExecutionStatusFromRaw, | ||
} from './alert_execution_status'; | ||
import { ErrorWithReason } from './error_with_reason'; | ||
|
||
describe('AlertExecutionStatus', () => { | ||
describe('executionStatusFromState()', () => { | ||
test('empty task state', () => { | ||
const status = executionStatusFromState({}); | ||
checkDateIsNearNow(status.date); | ||
expect(status.status).toBe('ok'); | ||
expect(status.error).toBe(undefined); | ||
}); | ||
|
||
test('task state with no instances', () => { | ||
const status = executionStatusFromState({ alertInstances: {} }); | ||
checkDateIsNearNow(status.date); | ||
expect(status.status).toBe('ok'); | ||
expect(status.error).toBe(undefined); | ||
}); | ||
|
||
test('task state with one instance', () => { | ||
const status = executionStatusFromState({ alertInstances: { a: {} } }); | ||
checkDateIsNearNow(status.date); | ||
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({ date, status })).toMatchInlineSnapshot(` | ||
Object { | ||
"date": "2020-09-03T16:26:58.000Z", | ||
"error": null, | ||
"status": "ok", | ||
} | ||
`); | ||
}); | ||
|
||
test('status with an error', () => { | ||
expect(alertExecutionStatusToRaw({ date, status, error })).toMatchInlineSnapshot(` | ||
Object { | ||
"date": "2020-09-03T16:26:58.000Z", | ||
"error": Object { | ||
"message": "wops", | ||
"reason": "decrypt", | ||
}, | ||
"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(); | ||
expect(result).toBe(undefined); | ||
}); | ||
|
||
test('undefined input', () => { | ||
const result = alertExecutionStatusFromRaw(undefined); | ||
expect(result).toBe(undefined); | ||
}); | ||
|
||
test('null input', () => { | ||
const result = alertExecutionStatusFromRaw(null); | ||
expect(result).toBe(undefined); | ||
}); | ||
|
||
test('invalid date', () => { | ||
const result = alertExecutionStatusFromRaw({ date: 'an invalid date' })!; | ||
checkDateIsNearNow(result.date); | ||
expect(result.status).toBe('unknown'); | ||
expect(result.error).toBe(undefined); | ||
}); | ||
|
||
test('valid date', () => { | ||
const result = alertExecutionStatusFromRaw({ date }); | ||
expect(result).toMatchInlineSnapshot(` | ||
Object { | ||
"date": 2020-09-03T16:26:58.000Z, | ||
"status": "unknown", | ||
} | ||
`); | ||
}); | ||
|
||
test('valid status and date', () => { | ||
const result = alertExecutionStatusFromRaw({ status, date }); | ||
expect(result).toMatchInlineSnapshot(` | ||
Object { | ||
"date": 2020-09-03T16:26:58.000Z, | ||
"status": "active", | ||
} | ||
`); | ||
}); | ||
|
||
test('valid status, date and error', () => { | ||
const result = alertExecutionStatusFromRaw({ status, date, error }); | ||
expect(result).toMatchInlineSnapshot(` | ||
Object { | ||
"date": 2020-09-03T16:26:58.000Z, | ||
"error": Object { | ||
"message": "wops", | ||
"reason": "execute", | ||
}, | ||
"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); | ||
} | ||
Comment on lines
+181
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some day I'll have to learn fake timers bits; I'm always confused seeing we're using sinon when I think jest also has fake timers. I'll leave this as is for now, make a mental note to learn fake timers for the next one of these. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
/* | ||
* 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 { AlertTaskState, AlertExecutionStatus, RawAlertExecutionStatus } from '../types'; | ||
import { getReasonFromError } from './error_with_reason'; | ||
|
||
export function executionStatusFromState(state: AlertTaskState): AlertExecutionStatus { | ||
const instanceIds = Object.keys(state.alertInstances ?? {}); | ||
return { | ||
date: new Date(), | ||
status: instanceIds.length === 0 ? 'ok' : 'active', | ||
}; | ||
} | ||
|
||
export function executionStatusFromError(error: Error): AlertExecutionStatus { | ||
return { | ||
date: new Date(), | ||
status: 'error', | ||
error: { | ||
reason: getReasonFromError(error), | ||
message: error.message, | ||
}, | ||
}; | ||
} | ||
|
||
export function alertExecutionStatusToRaw({ | ||
date, | ||
status, | ||
error, | ||
}: AlertExecutionStatus): RawAlertExecutionStatus { | ||
return { | ||
date: date.toISOString(), | ||
status, | ||
error: error ?? null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming this is in order to remove existing |
||
}; | ||
} | ||
|
||
export function alertExecutionStatusFromRaw( | ||
rawAlertExecutionStatus?: Partial<RawAlertExecutionStatus> | null | undefined | ||
): AlertExecutionStatus | undefined { | ||
if (!rawAlertExecutionStatus) return undefined; | ||
|
||
const { date, status = 'unknown', error } = rawAlertExecutionStatus; | ||
|
||
let parsedDateMillis = date ? Date.parse(date) : Date.now(); | ||
if (isNaN(parsedDateMillis)) { | ||
// TODO: log a message? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, worth a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but now I have to thread a Logger through a few methods :-( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
parsedDateMillis = Date.now(); | ||
} | ||
|
||
const parsedDate = new Date(parsedDateMillis); | ||
if (error) { | ||
return { date: parsedDate, status, error }; | ||
} else { | ||
return { date: parsedDate, status }; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we rename
date
to something that is explicit about what date it is?lastExecution
?lastUpdate
? etc.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it does feel kind of "context free", viewed here, but it is a property of the
executionStatus
object, so I thought usingdate
by itself would be fine; ie, you're always be referencing this piece of data asexecutionStatus.date
. Adding additional context ondate
itself seemed like overkill and overly wordy. 🤔There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, it isn't obvious to me what this date actually is 🤷
I won't block on this, but my feeling is that we're adding to the cognitive load by not being clear what his date actually means.
I'm guessing it means "lastUpdate" of the status, but I'm still not 100% sure and to me that's a reason to clarify.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But you know what the
status
is? Should we change it tolastExecutionStatus
as well? :-) I think that was my thinking in trying to not add more context here to the prop names, when it feels like it's implied by it's containing property &| type.Contextually, how people would end up accessing this, would look like the following for the two variants:
alert.executionStatus.date
alert.executionStatus.lastExecutionDate
I don't have really strong feels, just trying to cut down verbosity / ceremony / overkill where not actually needed.
But I just thought of a decent reason to do this - if for some reason we add some other date to this structure later, THEN it will certainly be confusing what the un-prefixed date would be.
So, I think
lastExecutionDate
prolly works best for me, since that exactly describes it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current branch is using
lastExecutionDate
- thanks for prodding on this Gidi!