[8.x] [Response Ops][Alerting] Use ES client to update rule SO at end of rule run instead of SO client. (#193341) (#194444)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Response Ops][Alerting] Use ES client to update rule SO at end of
rule run instead of SO client.
(#193341)](https://github.com/elastic/kibana/pull/193341)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Ying
Mao","email":"ying.mao@elastic.co"},"sourceCommit":{"committedDate":"2024-09-30T14:40:02Z","message":"[Response
Ops][Alerting] Use ES client to update rule SO at end of rule run
instead of SO client. (#193341)\n\nResolves
https://github.com/elastic/kibana/issues/192397\r\n\r\n##
Summary\r\n\r\nUpdates alerting task runner end of run updates to use
the ES client\r\nupdate function for a true partial update instead of
the saved objects\r\nclient update function that performs a GET then an
update.\r\n\r\n## To verify\r\nCreate a rule in multiple spaces and
ensure they run correctly and their\r\nexecution status and monitoring
history are updated at the end of each\r\nrun. Because we're performing
a partial update on attributes that are\r\nnot in the AAD, the rule
should continue running without any encryption\r\nerrors.\r\n\r\n## Risk
Matrix\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Updating saved object directly using ES client will break BWC |
Medium\r\n| High | Response Ops follows an intermediate release strategy
for any\r\nchanges to the rule saved object where schema changes are
introduced in\r\nan intermediate release before any changes to the saved
object are\r\nactually made in a followup release. This ensures that any
rollbacks\r\nthat may be required in a release will roll back to a
version that is\r\nalready aware of the new schema. The team is
socialized to this strategy\r\nas we are requiring users of the alerting
framework to also follow this\r\nstrategy. This should address any
backward compatibility issues that\r\nmight arise by circumventing the
saved objects client update function. |\r\n| Updating saved object
directly using ES client will break AAD | Medium\r\n| High | An explicit
allowlist of non-AAD fields that are allowed to be\r\npartially updated
has been introduced and any fields not in this\r\nallowlist will not be
included in the partial update. Any updates to the\r\nrule saved object
that might break AAD would show up with > 1 execution\r\nof a rule and
we have a plethora of functional tests that rely on\r\nmultiple
executions of a rule that would flag if there were issues\r\nrunning due
to AAD issues. |\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"05926c20c57b7abc69c6c068d5733f29306f73ba","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Alerting","release_note:skip","Team:ResponseOps","v9.0.0","backport:prev-minor","v8.16.0"],"title":"[Response
Ops][Alerting] Use ES client to update rule SO at end of rule run
instead of SO
client.","number":193341,"url":"https://github.com/elastic/kibana/pull/193341","mergeCommit":{"message":"[Response
Ops][Alerting] Use ES client to update rule SO at end of rule run
instead of SO client. (#193341)\n\nResolves
https://github.com/elastic/kibana/issues/192397\r\n\r\n##
Summary\r\n\r\nUpdates alerting task runner end of run updates to use
the ES client\r\nupdate function for a true partial update instead of
the saved objects\r\nclient update function that performs a GET then an
update.\r\n\r\n## To verify\r\nCreate a rule in multiple spaces and
ensure they run correctly and their\r\nexecution status and monitoring
history are updated at the end of each\r\nrun. Because we're performing
a partial update on attributes that are\r\nnot in the AAD, the rule
should continue running without any encryption\r\nerrors.\r\n\r\n## Risk
Matrix\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Updating saved object directly using ES client will break BWC |
Medium\r\n| High | Response Ops follows an intermediate release strategy
for any\r\nchanges to the rule saved object where schema changes are
introduced in\r\nan intermediate release before any changes to the saved
object are\r\nactually made in a followup release. This ensures that any
rollbacks\r\nthat may be required in a release will roll back to a
version that is\r\nalready aware of the new schema. The team is
socialized to this strategy\r\nas we are requiring users of the alerting
framework to also follow this\r\nstrategy. This should address any
backward compatibility issues that\r\nmight arise by circumventing the
saved objects client update function. |\r\n| Updating saved object
directly using ES client will break AAD | Medium\r\n| High | An explicit
allowlist of non-AAD fields that are allowed to be\r\npartially updated
has been introduced and any fields not in this\r\nallowlist will not be
included in the partial update. Any updates to the\r\nrule saved object
that might break AAD would show up with > 1 execution\r\nof a rule and
we have a plethora of functional tests that rely on\r\nmultiple
executions of a rule that would flag if there were issues\r\nrunning due
to AAD issues. |\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"05926c20c57b7abc69c6c068d5733f29306f73ba"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/193341","number":193341,"mergeCommit":{"message":"[Response
Ops][Alerting] Use ES client to update rule SO at end of rule run
instead of SO client. (#193341)\n\nResolves
https://github.com/elastic/kibana/issues/192397\r\n\r\n##
Summary\r\n\r\nUpdates alerting task runner end of run updates to use
the ES client\r\nupdate function for a true partial update instead of
the saved objects\r\nclient update function that performs a GET then an
update.\r\n\r\n## To verify\r\nCreate a rule in multiple spaces and
ensure they run correctly and their\r\nexecution status and monitoring
history are updated at the end of each\r\nrun. Because we're performing
a partial update on attributes that are\r\nnot in the AAD, the rule
should continue running without any encryption\r\nerrors.\r\n\r\n## Risk
Matrix\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Updating saved object directly using ES client will break BWC |
Medium\r\n| High | Response Ops follows an intermediate release strategy
for any\r\nchanges to the rule saved object where schema changes are
introduced in\r\nan intermediate release before any changes to the saved
object are\r\nactually made in a followup release. This ensures that any
rollbacks\r\nthat may be required in a release will roll back to a
version that is\r\nalready aware of the new schema. The team is
socialized to this strategy\r\nas we are requiring users of the alerting
framework to also follow this\r\nstrategy. This should address any
backward compatibility issues that\r\nmight arise by circumventing the
saved objects client update function. |\r\n| Updating saved object
directly using ES client will break AAD | Medium\r\n| High | An explicit
allowlist of non-AAD fields that are allowed to be\r\npartially updated
has been introduced and any fields not in this\r\nallowlist will not be
included in the partial update. Any updates to the\r\nrule saved object
that might break AAD would show up with > 1 execution\r\nof a rule and
we have a plethora of functional tests that rely on\r\nmultiple
executions of a rule that would flag if there were issues\r\nrunning due
to AAD issues. |\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"05926c20c57b7abc69c6c068d5733f29306f73ba"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Ying Mao <ying.mao@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-01 02:33:28 +10:00 committed by GitHub
parent 464430dd87
commit ce42c68ccd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 342 additions and 143 deletions

View file

@ -23,7 +23,7 @@ import { RawRule } from '../types';
import { getImportWarnings } from './get_import_warnings';
import { isRuleExportable } from './is_rule_exportable';
import { RuleTypeRegistry } from '../rule_type_registry';
export { partiallyUpdateRule } from './partially_update_rule';
export { partiallyUpdateRule, partiallyUpdateRuleWithEs } from './partially_update_rule';
import {
RULES_SETTINGS_SAVED_OBJECT_TYPE,
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,

View file

@ -10,16 +10,23 @@ import {
ISavedObjectsRepository,
SavedObjectsErrorHelpers,
} from '@kbn/core/server';
import { PartiallyUpdateableRuleAttributes, partiallyUpdateRule } from './partially_update_rule';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import {
PartiallyUpdateableRuleAttributes,
partiallyUpdateRule,
partiallyUpdateRuleWithEs,
} from './partially_update_rule';
import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
import { RULE_SAVED_OBJECT_TYPE } from '.';
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import { estypes } from '@elastic/elasticsearch';
import { RuleExecutionStatuses } from '@kbn/alerting-types';
const MockSavedObjectsClientContract = savedObjectsClientMock.create();
const MockISavedObjectsRepository =
MockSavedObjectsClientContract as unknown as jest.Mocked<ISavedObjectsRepository>;
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
describe('partially_update_rule', () => {
describe('partiallyUpdateRule', () => {
beforeEach(() => {
jest.resetAllMocks();
});
@ -104,6 +111,101 @@ describe('partially_update_rule', () => {
});
});
describe('partiallyUpdateRuleWithEs', () => {
beforeEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
test('should work with no options', async () => {
esClient.update.mockResolvedValueOnce(MockEsUpdateResponse(MockRuleId));
await partiallyUpdateRuleWithEs(esClient, MockRuleId, DefaultAttributesForEsUpdate);
expect(esClient.update).toHaveBeenCalledTimes(1);
expect(esClient.update).toHaveBeenCalledWith({
id: `alert:${MockRuleId}`,
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
doc: {
alert: DefaultAttributesForEsUpdate,
},
});
});
test('should strip unallowed attributes ', async () => {
const attributes =
AttributesForEsUpdateWithUnallowedFields as unknown as PartiallyUpdateableRuleAttributes;
esClient.update.mockResolvedValueOnce(MockEsUpdateResponse(MockRuleId));
await partiallyUpdateRuleWithEs(esClient, MockRuleId, attributes);
expect(esClient.update).toHaveBeenCalledWith({
id: `alert:${MockRuleId}`,
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
doc: {
alert: DefaultAttributesForEsUpdate,
},
});
});
test('should handle ES errors', async () => {
esClient.update.mockRejectedValueOnce(new Error('wops'));
await expect(
partiallyUpdateRuleWithEs(esClient, MockRuleId, DefaultAttributes)
).rejects.toThrowError('wops');
});
test('should handle the version option', async () => {
esClient.update.mockResolvedValueOnce(MockEsUpdateResponse(MockRuleId));
await partiallyUpdateRuleWithEs(esClient, MockRuleId, DefaultAttributesForEsUpdate, {
version: 'WzQsMV0=',
});
expect(esClient.update).toHaveBeenCalledWith({
id: `alert:${MockRuleId}`,
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
if_primary_term: 1,
if_seq_no: 4,
doc: {
alert: DefaultAttributesForEsUpdate,
},
});
});
test('should handle the ignore404 option', async () => {
esClient.update.mockResolvedValueOnce(MockEsUpdateResponse(MockRuleId));
await partiallyUpdateRuleWithEs(esClient, MockRuleId, DefaultAttributesForEsUpdate, {
ignore404: true,
});
expect(esClient.update).toHaveBeenCalledWith(
{
id: `alert:${MockRuleId}`,
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
doc: {
alert: DefaultAttributesForEsUpdate,
},
},
{ ignore: [404] }
);
});
test('should handle the refresh option', async () => {
esClient.update.mockResolvedValueOnce(MockEsUpdateResponse(MockRuleId));
await partiallyUpdateRuleWithEs(esClient, MockRuleId, DefaultAttributesForEsUpdate, {
refresh: 'wait_for',
});
expect(esClient.update).toHaveBeenCalledWith({
id: `alert:${MockRuleId}`,
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
doc: {
alert: DefaultAttributesForEsUpdate,
},
refresh: 'wait_for',
});
});
});
function getMockSavedObjectClients(): Record<
string,
jest.Mocked<SavedObjectsClientContract | ISavedObjectsRepository>
@ -126,6 +228,50 @@ const DefaultAttributes = {
const ExtraneousAttributes = { ...DefaultAttributes, foo: 'bar' };
const DefaultAttributesForEsUpdate = {
running: false,
executionStatus: {
status: 'active' as RuleExecutionStatuses,
lastExecutionDate: '2023-01-01T08:44:40.000Z',
lastDuration: 12,
error: null,
warning: null,
},
monitoring: {
run: {
calculated_metrics: {
success_ratio: 20,
},
history: [
{
success: true,
timestamp: 1640991880000,
duration: 12,
outcome: 'success',
},
],
last_run: {
timestamp: '2023-01-01T08:44:40.000Z',
metrics: {
duration: 12,
gap_duration_s: null,
total_alerts_created: null,
total_alerts_detected: null,
total_indexing_duration_ms: null,
total_search_duration_ms: null,
},
},
},
},
};
const AttributesForEsUpdateWithUnallowedFields = {
...DefaultAttributesForEsUpdate,
alertTypeId: 'foo',
consumer: 'consumer',
randomField: 'bar',
};
const MockRuleId = 'rule-id';
const MockUpdateValue = {
@ -137,3 +283,13 @@ const MockUpdateValue = {
},
references: [],
};
const MockEsUpdateResponse = (id: string) => ({
_index: '.kibana_alerting_cases_9.0.0_001',
_id: `alert:${id}`,
_version: 3,
result: 'updated' as estypes.Result,
_shards: { total: 1, successful: 1, failed: 0 },
_seq_no: 5,
_primary_term: 1,
});

View file

@ -7,10 +7,13 @@
import { omit, pick } from 'lodash';
import {
ElasticsearchClient,
SavedObjectsClient,
SavedObjectsErrorHelpers,
SavedObjectsUpdateOptions,
} from '@kbn/core/server';
import { decodeRequestVersion } from '@kbn/core-saved-objects-base-server-internal';
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import { RawRule } from '../types';
import {
@ -67,3 +70,50 @@ export async function partiallyUpdateRule(
throw err;
}
}
// Explicit list of attributes that we allow to be partially updated
// There should be no overlap between this list and RuleAttributesIncludedInAAD or RuleAttributesToEncrypt
const RuleAttributesAllowedForPartialUpdate = [
'executionStatus',
'lastRun',
'monitoring',
'nextRun',
'running',
];
// direct, partial update to a rule saved object via ElasticsearchClient
// we do this direct partial update to avoid the overhead of the SavedObjectsClient for
// only these allow-listed fields which don't impact encryption. in addition, because these
// fields are only updated by the system user at the end of a rule run, they should not
// need to be included in any (user-centric) audit logs.
export async function partiallyUpdateRuleWithEs(
esClient: ElasticsearchClient,
id: string,
attributes: PartiallyUpdateableRuleAttributes,
options: PartiallyUpdateRuleSavedObjectOptions = {}
): Promise<void> {
// ensure we only have the valid attributes that are not encrypted and are excluded from AAD
const attributeUpdates = omit(attributes, [
...RuleAttributesToEncrypt,
...RuleAttributesIncludedInAAD,
]);
// ensure we only have attributes that we explicitly allow to be updated
const attributesAllowedForUpdate = pick(attributeUpdates, RuleAttributesAllowedForPartialUpdate);
const updateParams = {
id: `alert:${id}`,
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
...(options.version ? decodeRequestVersion(options.version) : {}),
doc: {
alert: attributesAllowedForUpdate,
},
...(options.refresh ? { refresh: options.refresh } : {}),
};
if (options.ignore404) {
await esClient.update(updateParams, { ignore: [404] });
} else {
await esClient.update(updateParams);
}
}

View file

@ -7,6 +7,7 @@
import { TaskStatus } from '@kbn/task-manager-plugin/server';
import { SavedObject } from '@kbn/core/server';
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import {
Rule,
RuleTypeParams,
@ -64,7 +65,7 @@ const defaultHistory = [
},
];
export const generateSavedObjectParams = ({
export const generateRuleUpdateParams = ({
error = null,
warning = null,
status = 'ok',
@ -83,53 +84,59 @@ export const generateSavedObjectParams = ({
history?: RuleMonitoring['run']['history'];
alertsCount?: Record<string, number>;
}) => [
RULE_SAVED_OBJECT_TYPE,
'1',
{
monitoring: {
run: {
calculated_metrics: {
success_ratio: successRatio,
},
history,
last_run: {
timestamp: '1970-01-01T00:00:00.000Z',
metrics: {
duration: 0,
gap_duration_s: null,
total_alerts_created: null,
total_alerts_detected: null,
total_indexing_duration_ms: null,
total_search_duration_ms: null,
id: `alert:1`,
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
doc: {
alert: {
monitoring: {
run: {
calculated_metrics: {
success_ratio: successRatio,
},
history,
last_run: {
timestamp: '1970-01-01T00:00:00.000Z',
metrics: {
duration: 0,
gap_duration_s: null,
total_alerts_created: null,
total_alerts_detected: null,
total_indexing_duration_ms: null,
total_search_duration_ms: null,
},
},
},
},
executionStatus: {
error,
lastDuration: 0,
lastExecutionDate: '1970-01-01T00:00:00.000Z',
status,
warning,
},
lastRun: {
outcome,
outcomeOrder: RuleLastRunOutcomeOrderMap[outcome],
outcomeMsg:
(error?.message && [error?.message]) ||
(warning?.message && [warning?.message]) ||
null,
warning: error?.reason || warning?.reason || null,
alertsCount: {
active: 0,
ignored: 0,
new: 0,
recovered: 0,
...(alertsCount || {}),
},
},
nextRun,
running: false,
},
},
executionStatus: {
error,
lastDuration: 0,
lastExecutionDate: '1970-01-01T00:00:00.000Z',
status,
warning,
},
lastRun: {
outcome,
outcomeOrder: RuleLastRunOutcomeOrderMap[outcome],
outcomeMsg:
(error?.message && [error?.message]) || (warning?.message && [warning?.message]) || null,
warning: error?.reason || warning?.reason || null,
alertsCount: {
active: 0,
ignored: 0,
new: 0,
recovered: 0,
...(alertsCount || {}),
},
},
nextRun,
running: false,
},
{ refresh: false, namespace: undefined },
{ ignore: [404] },
];
export const GENERIC_ERROR_MESSAGE = 'GENERIC ERROR MESSAGE';

View file

@ -59,7 +59,7 @@ import {
generateRunnerResult,
RULE_ACTIONS,
generateEnqueueFunctionInput,
generateSavedObjectParams,
generateRuleUpdateParams,
mockTaskInstance,
GENERIC_ERROR_MESSAGE,
generateAlertInstance,
@ -341,8 +341,8 @@ describe('Task Runner', () => {
testAlertingEventLogCalls({ status: 'ok' });
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
...generateSavedObjectParams({})
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
...generateRuleUpdateParams({})
);
expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1);
@ -2676,8 +2676,8 @@ describe('Task Runner', () => {
status: 'ok',
});
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
...generateSavedObjectParams({})
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
...generateRuleUpdateParams({})
);
expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled();
});
@ -2789,10 +2789,8 @@ describe('Task Runner', () => {
});
await taskRunner.run();
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
...generateSavedObjectParams({
nextRun: '1970-01-01T00:00:10.000Z',
})
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
...generateRuleUpdateParams({ nextRun: '1970-01-01T00:00:10.000Z' })
);
});
@ -2825,21 +2823,14 @@ describe('Task Runner', () => {
);
await taskRunner.run();
ruleType.executor.mockClear();
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
...generateSavedObjectParams({
error: {
message: GENERIC_ERROR_MESSAGE,
reason: 'execute',
},
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
...generateRuleUpdateParams({
error: { message: GENERIC_ERROR_MESSAGE, reason: 'execute' },
outcome: 'failed',
status: 'error',
successRatio: 0,
history: [
{
success: false,
timestamp: 0,
},
],
history: [{ success: false, timestamp: 0 }],
})
);
});
@ -2947,15 +2938,12 @@ describe('Task Runner', () => {
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
...generateSavedObjectParams({
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
...generateRuleUpdateParams({
status: 'warning',
outcome: 'warning',
warning,
alertsCount: {
active: 1,
new: 1,
},
alertsCount: { active: 1, new: 1 },
})
);
@ -3117,15 +3105,12 @@ describe('Task Runner', () => {
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
...generateSavedObjectParams({
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
...generateRuleUpdateParams({
status: 'warning',
outcome: 'warning',
warning,
alertsCount: {
active: 2,
new: 2,
},
alertsCount: { active: 2, new: 2 },
})
);

View file

@ -42,7 +42,7 @@ import {
import { asErr, asOk, isErr, isOk, map, resolveErr, Result } from '../lib/result_type';
import { taskInstanceToAlertTaskInstance } from './alert_task_instance';
import { isAlertSavedObjectNotFoundError, isEsUnavailableError } from '../lib/is_alerting_error';
import { partiallyUpdateRule, RULE_SAVED_OBJECT_TYPE } from '../saved_objects';
import { partiallyUpdateRuleWithEs, RULE_SAVED_OBJECT_TYPE } from '../saved_objects';
import {
AlertInstanceContext,
AlertInstanceState,
@ -204,7 +204,6 @@ export class TaskRunner<
private async updateRuleSavedObjectPostRun(
ruleId: string,
namespace: string | undefined,
attributes: {
executionStatus?: RawRuleExecutionStatus;
monitoring?: RawRuleMonitoring;
@ -212,7 +211,7 @@ export class TaskRunner<
lastRun?: RawRuleLastRun | null;
}
) {
const client = this.internalSavedObjectsRepository;
const client = this.context.elasticsearch.client.asInternalUser;
try {
// Future engineer -> Here we are just checking if we need to wait for
// the update of the attribute `running` in the rule's saved object
@ -223,13 +222,12 @@ export class TaskRunner<
// eslint-disable-next-line no-empty
} catch {}
try {
await partiallyUpdateRule(
await partiallyUpdateRuleWithEs(
client,
ruleId,
{ ...attributes, running: false },
{
ignore404: true,
namespace,
refresh: false,
}
);
@ -548,7 +546,7 @@ export class TaskRunner<
const { executionStatus: execStatus, executionMetrics: execMetrics } =
await this.timer.runWithTimer(TaskRunnerTimerSpan.ProcessRuleRun, async () => {
const {
params: { alertId: ruleId, spaceId },
params: { alertId: ruleId },
startedAt,
schedule: taskSchedule,
} = this.taskInstance;
@ -560,8 +558,6 @@ export class TaskRunner<
nextRun = getNextRun({ startDate: startedAt, interval: taskSchedule.interval });
}
const namespace = this.context.spaceIdToNamespace(spaceId);
const { executionStatus, executionMetrics, lastRun, outcome } = processRunResults({
logger: this.logger,
logPrefix: `${this.ruleType.id}:${ruleId}`,
@ -602,7 +598,7 @@ export class TaskRunner<
)} - ${JSON.stringify(lastRun)}`
);
}
await this.updateRuleSavedObjectPostRun(ruleId, namespace, {
await this.updateRuleSavedObjectPostRun(ruleId, {
executionStatus: ruleExecutionStatusToRaw(executionStatus),
nextRun,
lastRun: lastRunToRaw(lastRun),
@ -758,11 +754,10 @@ export class TaskRunner<
// Write event log entry
const {
params: { alertId: ruleId, spaceId, consumer },
params: { alertId: ruleId, consumer },
schedule: taskSchedule,
startedAt,
} = this.taskInstance;
const namespace = this.context.spaceIdToNamespace(spaceId);
if (consumer && !this.ruleConsumer) {
this.ruleConsumer = consumer;
@ -803,7 +798,7 @@ export class TaskRunner<
`Updating rule task for ${this.ruleType.id} rule with id ${ruleId} - execution error due to timeout`
);
const outcome = 'failed';
await this.updateRuleSavedObjectPostRun(ruleId, namespace, {
await this.updateRuleSavedObjectPostRun(ruleId, {
executionStatus: ruleExecutionStatusToRaw(executionStatus),
lastRun: {
outcome,

View file

@ -46,7 +46,7 @@ import {
RULE_NAME,
generateRunnerResult,
RULE_ACTIONS,
generateSavedObjectParams,
generateRuleUpdateParams,
mockTaskInstance,
DATE_1970,
DATE_1970_5_MIN,
@ -376,8 +376,8 @@ describe('Task Runner', () => {
{ tags: ['1', 'test'] }
);
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
...generateSavedObjectParams({})
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
...generateRuleUpdateParams({})
);
expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1);
@ -510,8 +510,8 @@ describe('Task Runner', () => {
'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"numberOfActiveAlerts":0,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":0,"numberOfDelayedAlerts":0,"hasReachedAlertLimit":false,"hasReachedQueuedActionsLimit":false,"triggeredActionsStatus":"complete"}',
{ tags: ['1', 'test'] }
);
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
...generateSavedObjectParams({})
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
...generateRuleUpdateParams({})
);
expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1);
expect(
@ -708,8 +708,8 @@ describe('Task Runner', () => {
tags: ['1', 'test'],
});
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
...generateSavedObjectParams({})
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
...generateRuleUpdateParams({})
);
expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1);
@ -799,8 +799,8 @@ describe('Task Runner', () => {
tags: ['1', 'test'],
});
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
...generateSavedObjectParams({})
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
...generateRuleUpdateParams({})
);
expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1);

View file

@ -63,6 +63,7 @@ import { TaskRunnerContext } from './types';
import { backfillClientMock } from '../backfill_client/backfill_client.mock';
import { UntypedNormalizedRuleType } from '../rule_type_registry';
import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock';
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock';
jest.mock('uuid', () => ({
@ -225,53 +226,57 @@ describe('Task Runner Cancel', () => {
testAlertingEventLogCalls({ status: 'ok' });
expect(internalSavedObjectsRepository.update).toHaveBeenCalledTimes(1);
expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
'1',
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledTimes(1);
expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith(
{
executionStatus: {
error: {
message: `test:1: execution cancelled due to timeout - exceeded rule type timeout of 5m`,
reason: 'timeout',
},
lastDuration: 0,
lastExecutionDate: '1970-01-01T00:00:00.000Z',
status: 'error',
warning: null,
},
lastRun: {
alertsCount: {},
outcome: 'failed',
outcomeMsg: [
'test:1: execution cancelled due to timeout - exceeded rule type timeout of 5m',
],
outcomeOrder: 20,
warning: 'timeout',
},
monitoring: {
run: {
calculated_metrics: {
success_ratio: 0,
},
history: [],
last_run: {
metrics: {
duration: 0,
gap_duration_s: null,
total_alerts_created: null,
total_alerts_detected: null,
total_indexing_duration_ms: null,
total_search_duration_ms: null,
id: `alert:1`,
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
doc: {
alert: {
executionStatus: {
error: {
message: `test:1: execution cancelled due to timeout - exceeded rule type timeout of 5m`,
reason: 'timeout',
},
timestamp: '1970-01-01T00:00:00.000Z',
lastDuration: 0,
lastExecutionDate: '1970-01-01T00:00:00.000Z',
status: 'error',
warning: null,
},
lastRun: {
alertsCount: {},
outcome: 'failed',
outcomeMsg: [
'test:1: execution cancelled due to timeout - exceeded rule type timeout of 5m',
],
outcomeOrder: 20,
warning: 'timeout',
},
monitoring: {
run: {
calculated_metrics: {
success_ratio: 0,
},
history: [],
last_run: {
metrics: {
duration: 0,
gap_duration_s: null,
total_alerts_created: null,
total_alerts_detected: null,
total_indexing_duration_ms: null,
total_search_duration_ms: null,
},
timestamp: '1970-01-01T00:00:00.000Z',
},
},
},
nextRun: '1970-01-01T00:00:10.000Z',
running: false,
},
},
nextRun: '1970-01-01T00:00:10.000Z',
running: false,
},
{ refresh: false, namespace: undefined }
{ ignore: [404] }
);
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1);
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledWith({

View file

@ -72,7 +72,8 @@
"@kbn/alerting-state-types",
"@kbn/core-security-server",
"@kbn/core-http-server",
"@kbn/zod"
"@kbn/zod",
"@kbn/core-saved-objects-base-server-internal"
],
"exclude": [
"target/**/*"