Skip to content
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][actions] add task scheduled date and delay to event log #102252

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion x-pack/plugins/actions/server/lib/action_executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const services = actionsMock.createServices();
const actionsClient = actionsClientMock.create();
const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
const actionTypeRegistry = actionTypeRegistryMock.create();
const eventLogger = eventLoggerMock.create();

const executeParams = {
actionId: '1',
Expand All @@ -42,7 +43,7 @@ actionExecutor.initialize({
getActionsClientWithRequest,
actionTypeRegistry,
encryptedSavedObjectsClient,
eventLogger: eventLoggerMock.create(),
eventLogger,
preconfiguredActions: [],
});

Expand Down Expand Up @@ -108,6 +109,77 @@ test('successfully executes', async () => {
});

expect(loggerMock.debug).toBeCalledWith('executing action test:1: 1');
expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"event": Object {
"action": "execute",
"outcome": "success",
},
"kibana": Object {
"saved_objects": Array [
Object {
"id": "1",
"namespace": "some-namespace",
"rel": "primary",
"type": "action",
"type_id": "test",
},
],
},
"message": "action executed: test:1: 1",
},
],
]
`);
});

test('successfully executes as a task', async () => {
const actionType: jest.Mocked<ActionType> = {
id: 'test',
name: 'Test',
minimumLicenseRequired: 'basic',
executor: jest.fn(),
};
const actionSavedObject = {
id: '1',
type: 'action',
attributes: {
actionTypeId: 'test',
config: {
bar: true,
},
secrets: {
baz: true,
},
},
references: [],
};
const actionResult = {
id: actionSavedObject.id,
name: actionSavedObject.id,
...pick(actionSavedObject.attributes, 'actionTypeId', 'config'),
isPreconfigured: false,
};
actionsClient.get.mockResolvedValueOnce(actionResult);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);

const scheduleDelay = 10000; // milliseconds
const scheduled = new Date(Date.now() - scheduleDelay);
await actionExecutor.execute({
...executeParams,
taskInfo: {
scheduled,
},
});

const eventTask = eventLogger.logEvent.mock.calls[0][0]?.kibana?.task;
expect(eventTask).toBeDefined();
expect(eventTask?.scheduled).toBe(scheduled.toISOString());
expect(eventTask?.schedule_delay).toBeGreaterThanOrEqual(scheduleDelay * 1000 * 1000);
expect(eventTask?.schedule_delay).toBeLessThanOrEqual(2 * scheduleDelay * 1000 * 1000);
});

test('provides empty config when config and / or secrets is empty', async () => {
Expand Down
19 changes: 19 additions & 0 deletions x-pack/plugins/actions/server/lib/action_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import { ActionsClient } from '../actions_client';
import { ActionExecutionSource } from './action_execution_source';
import { RelatedSavedObjects } from './related_saved_objects';

// 1,000,000 nanoseconds in 1 millisecond
const Millis2Nanos = 1000 * 1000;

export interface ActionExecutorContext {
logger: Logger;
spaces?: SpacesServiceStart;
Expand All @@ -38,11 +41,16 @@ export interface ActionExecutorContext {
preconfiguredActions: PreConfiguredAction[];
}

export interface TaskInfo {
scheduled: Date;
}

export interface ExecuteOptions<Source = unknown> {
actionId: string;
request: KibanaRequest;
params: Record<string, unknown>;
source?: ActionExecutionSource<Source>;
taskInfo?: TaskInfo;
relatedSavedObjects?: RelatedSavedObjects;
}

Expand Down Expand Up @@ -70,6 +78,7 @@ export class ActionExecutor {
params,
request,
source,
taskInfo,
relatedSavedObjects,
}: ExecuteOptions): Promise<ActionTypeExecutorResult<unknown>> {
if (!this.isInitialized) {
Expand Down Expand Up @@ -142,9 +151,19 @@ export class ActionExecutor {
const actionLabel = `${actionTypeId}:${actionId}: ${name}`;
logger.debug(`executing action ${actionLabel}`);

const task = taskInfo
? {
task: {
scheduled: taskInfo.scheduled.toISOString(),
schedule_delay: Millis2Nanos * (Date.now() - taskInfo.scheduled.getTime()),
},
Comment on lines +156 to +159
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on your comment here: #102252 (comment). Task id could be interesting as well, or it could also belong in the event log's kibana saved objects array to complete the dependency train.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heh, ya, didn't get around to it, but we do have the top-level task object we can provide more goodies for later.

One of the issues with adding the task manager SO reference, is that we'd also have to indicate the index, since it's not in .kibana - or I guess that's my understanding, that you'd need to know the index name to actually look them up. Or we could special case some of the "types" I suppose, if the existing SO client doesn't magically find them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the issues with adding the task manager SO reference, is that we'd also have to indicate the index

I think this would be ok as long as the task SO is accessed via the saved objects client/repository. The SO repository would automatically know it's a type of task and look up the index task belongs into. All good to defer this 👍

}
: {};

const event: IEvent = {
event: { action: EVENT_LOG_ACTIONS.execute },
kibana: {
...task,
saved_objects: [
{
rel: SAVED_OBJECT_REL_PRIMARY,
Expand Down
16 changes: 15 additions & 1 deletion x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ test('executes the task by calling the executor with proper parameters', async (
authorization: 'ApiKey MTIzOmFiYw==',
},
}),
taskInfo: {
scheduled: new Date(),
},
});

const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
Expand Down Expand Up @@ -255,6 +258,9 @@ test('uses API key when provided', async () => {
authorization: 'ApiKey MTIzOmFiYw==',
},
}),
taskInfo: {
scheduled: new Date(),
},
});

const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
Expand Down Expand Up @@ -300,6 +306,9 @@ test('uses relatedSavedObjects when provided', async () => {
authorization: 'ApiKey MTIzOmFiYw==',
},
}),
taskInfo: {
scheduled: new Date(),
},
});
});

Expand All @@ -323,7 +332,6 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => {
});

await taskRunner.run();

expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
Expand All @@ -334,6 +342,9 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => {
authorization: 'ApiKey MTIzOmFiYw==',
},
}),
taskInfo: {
scheduled: new Date(),
},
});
});

Expand Down Expand Up @@ -363,6 +374,9 @@ test(`doesn't use API key when not provided`, async () => {
request: expect.objectContaining({
headers: {},
}),
taskInfo: {
scheduled: new Date(),
},
});

const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/actions/server/lib/task_runner_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export class TaskRunnerFactory {
getUnsecuredSavedObjectsClient,
} = this.taskRunnerContext!;

const taskInfo = {
scheduled: taskInstance.runAt,
};

return {
async run() {
const { spaceId, actionTaskParamsId } = taskInstance.params as Record<string, string>;
Expand Down Expand Up @@ -118,6 +122,7 @@ export class TaskRunnerFactory {
actionId,
request: fakeRequest,
...getSourceFromReferences(references),
taskInfo,
relatedSavedObjects: validatedRelatedSavedObjects(logger, relatedSavedObjects),
});
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@ test('enqueues execution per selected action', async () => {
"id": "1",
"license": "basic",
"name": "name-of-alert",
"namespace": "test1",
"ruleset": "alerts",
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ export function createExecutionHandler<
license: alertType.minimumLicenseRequired,
category: alertType.id,
ruleset: alertType.producer,
...namespace,
name: alertName,
},
};
Expand Down
Loading