Skip to content

Commit 1b5ece0

Browse files
[Composite SLO] Instrument background task with APM transaction spans and labels (elastic#269762)
## Summary <img width="1885" height="955" alt="image" src="https://github.com/user-attachments/assets/31b15756-6822-44bd-87a5-7e39734d333f" /> - Instrument `computeAndPersistCompositeSummaries` with `@kbn/apm-utils`: **`withSpan`** for decode/group, member SO fetch, member summary compute, and bulk write (children of Task Manager’s existing **`task-run`** transaction—no new top-level transaction). - **`addTransactionLabels`** on each run for outcome, counters, duration, pages fetched, and max-limit flag; same for early **`runTask`** exits (`task_not_started`, `outdated_task_version`). - Unit tests with mocked `@kbn/apm-utils`. Closes elastic#264015. ## Test plan - [x] `node scripts/jest` — `compute_and_persist_composite_summaries.test.ts`, `composite_slo_summary_task.test.ts` - [ ] Optional: Kibana with `elastic.apm.active: true` and confirm spans + labels on one task run kibana.dev.yml: ```Yaml xpack.slo.experimental.compositeSlo.enabled: true xpack.slo.compositeSloSummaryTaskEnabled: true # APM https://kibana-cloud-apm.elastic.dev/app/apm/services?comparisonEnabled=true&rangeFrom=now-15m&rangeTo=now&offset=1d elastic.apm.active: true elastic.apm.transactionSampleRate: 1 elastic.apm.environment: <YOUR_USERNAME> ```
1 parent fd9180a commit 1b5ece0

5 files changed

Lines changed: 446 additions & 25 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { addTransactionLabels } from '@kbn/apm-utils';
9+
import type { CoreSetup, LoggerFactory } from '@kbn/core/server';
10+
import {
11+
coreMock,
12+
elasticsearchServiceMock,
13+
loggingSystemMock,
14+
savedObjectsRepositoryMock,
15+
} from '@kbn/core/server/mocks';
16+
import type { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server';
17+
import { TaskStatus } from '@kbn/task-manager-plugin/server';
18+
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
19+
import type { SLOConfig, SLOPluginStartDependencies } from '../../../types';
20+
import {
21+
CompositeSloSummaryTask,
22+
getCompositeSloSummaryTaskId,
23+
TYPE,
24+
} from './composite_slo_summary_task';
25+
import { computeAndPersistCompositeSummaries } from './compute_and_persist_composite_summaries';
26+
import { COMPOSITE_SLO_SUMMARY_TASK_SKIP_REASON } from './constants';
27+
28+
jest.mock('@kbn/apm-utils', () => ({
29+
addTransactionLabels: jest.fn(),
30+
}));
31+
32+
jest.mock('./compute_and_persist_composite_summaries', () => ({
33+
computeAndPersistCompositeSummaries: jest.fn().mockResolvedValue(undefined),
34+
}));
35+
36+
const addTransactionLabelsMock = addTransactionLabels as jest.MockedFunction<
37+
typeof addTransactionLabels
38+
>;
39+
const mockPersist = computeAndPersistCompositeSummaries as jest.MockedFunction<
40+
typeof computeAndPersistCompositeSummaries
41+
>;
42+
43+
function createConcreteTaskInstanceStub(id: string): ConcreteTaskInstance {
44+
const now = new Date('2024-01-01T00:00:00.000Z');
45+
return {
46+
id,
47+
taskType: TYPE,
48+
params: {},
49+
state: {},
50+
scheduledAt: now,
51+
attempts: 0,
52+
status: TaskStatus.Running,
53+
runAt: now,
54+
startedAt: now,
55+
retryAt: null,
56+
ownerId: null,
57+
};
58+
}
59+
60+
function createStartPlugins(): SLOPluginStartDependencies {
61+
return {
62+
licensing: {
63+
getLicense: jest.fn().mockResolvedValue({ hasAtLeast: jest.fn().mockReturnValue(true) }),
64+
},
65+
taskManager: taskManagerMock.createStart(),
66+
} as unknown as SLOPluginStartDependencies;
67+
}
68+
69+
describe('CompositeSloSummaryTask', () => {
70+
let coreSetup: ReturnType<typeof coreMock.createSetup>;
71+
let task: CompositeSloSummaryTask;
72+
73+
const baseConfig = {
74+
sloOrphanSummaryCleanUpTaskEnabled: true,
75+
tempSummaryCleanupTaskEnabled: true,
76+
healthScanTaskEnabled: true,
77+
staleInstancesCleanupTaskEnabled: false,
78+
compositeSloSummaryTaskEnabled: true,
79+
enabled: true,
80+
experimental: {
81+
ruleFormV2: { enabled: false },
82+
compositeSlo: { enabled: true },
83+
},
84+
} satisfies SLOConfig;
85+
86+
function createTask(): CompositeSloSummaryTask {
87+
coreSetup = coreMock.createSetup();
88+
coreSetup.getStartServices.mockResolvedValue([
89+
{
90+
elasticsearch: {
91+
client: {
92+
asInternalUser: elasticsearchServiceMock.createClusterClient().asInternalUser,
93+
},
94+
},
95+
savedObjects: {
96+
createInternalRepository: jest.fn().mockReturnValue(savedObjectsRepositoryMock.create()),
97+
},
98+
} as never,
99+
{} as never,
100+
{} as never,
101+
]);
102+
103+
return new CompositeSloSummaryTask({
104+
core: coreSetup as CoreSetup,
105+
config: baseConfig,
106+
taskManager: taskManagerMock.createSetup(),
107+
logFactory: loggingSystemMock.create() as unknown as LoggerFactory,
108+
});
109+
}
110+
111+
beforeEach(() => {
112+
jest.clearAllMocks();
113+
mockPersist.mockResolvedValue(undefined);
114+
task = createTask();
115+
});
116+
117+
describe('runTask APM labels', () => {
118+
it('labels skipped when task was never started', async () => {
119+
await task.runTask(
120+
createConcreteTaskInstanceStub(getCompositeSloSummaryTaskId()),
121+
coreSetup as CoreSetup,
122+
new AbortController()
123+
);
124+
125+
expect(addTransactionLabelsMock).toHaveBeenCalledWith({
126+
plugin: 'slo',
127+
composite_slo_summary_run_outcome: 'skipped',
128+
composite_slo_summary_skip_reason: COMPOSITE_SLO_SUMMARY_TASK_SKIP_REASON.TASK_NOT_STARTED,
129+
});
130+
expect(mockPersist).not.toHaveBeenCalled();
131+
});
132+
133+
it('labels skipped outdated_task_version when task instance id does not match', async () => {
134+
await task.start(createStartPlugins());
135+
136+
await task.runTask(
137+
createConcreteTaskInstanceStub('stale-task-instance-id'),
138+
coreSetup as CoreSetup,
139+
new AbortController()
140+
);
141+
142+
expect(addTransactionLabelsMock).toHaveBeenCalledWith({
143+
plugin: 'slo',
144+
composite_slo_summary_run_outcome: 'skipped',
145+
composite_slo_summary_skip_reason:
146+
COMPOSITE_SLO_SUMMARY_TASK_SKIP_REASON.OUTDATED_TASK_VERSION,
147+
});
148+
expect(mockPersist).not.toHaveBeenCalled();
149+
});
150+
151+
it('runs computeAndPersistCompositeSummaries when started and id matches', async () => {
152+
await task.start(createStartPlugins());
153+
154+
await task.runTask(
155+
createConcreteTaskInstanceStub(getCompositeSloSummaryTaskId()),
156+
coreSetup as CoreSetup,
157+
new AbortController()
158+
);
159+
160+
expect(mockPersist).toHaveBeenCalledTimes(1);
161+
});
162+
});
163+
});

x-pack/solutions/observability/plugins/slo/server/services/tasks/composite_slo_summary_task/composite_slo_summary_task.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type Logger,
1212
type LoggerFactory,
1313
} from '@kbn/core/server';
14+
import { addTransactionLabels } from '@kbn/apm-utils';
1415
import type {
1516
ConcreteTaskInstance,
1617
TaskManagerSetupContract,
@@ -19,6 +20,7 @@ import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task';
1920
import type { SLOConfig, SLOPluginStartDependencies } from '../../../types';
2021
import { SO_SLO_COMPOSITE_TYPE } from '../../../saved_objects/slo_composite';
2122
import { computeAndPersistCompositeSummaries } from './compute_and_persist_composite_summaries';
23+
import { COMPOSITE_SLO_SUMMARY_TASK_SKIP_REASON } from './constants';
2224

2325
export const TYPE = 'slo:composite-slo-summary-task';
2426

@@ -118,13 +120,24 @@ export class CompositeSloSummaryTask {
118120
): Promise<{ state: Record<string, unknown> } | void> {
119121
if (!this.wasStarted) {
120122
this.logger.debug('runTask Aborted. Task not started yet');
123+
addTransactionLabels({
124+
plugin: 'slo',
125+
composite_slo_summary_run_outcome: 'skipped',
126+
composite_slo_summary_skip_reason: COMPOSITE_SLO_SUMMARY_TASK_SKIP_REASON.TASK_NOT_STARTED,
127+
});
121128
return;
122129
}
123130

124131
if (taskInstance.id !== this.taskId) {
125132
this.logger.debug(
126133
`Outdated task version: Got [${taskInstance.id}], current version is [${this.taskId}]`
127134
);
135+
addTransactionLabels({
136+
plugin: 'slo',
137+
composite_slo_summary_run_outcome: 'skipped',
138+
composite_slo_summary_skip_reason:
139+
COMPOSITE_SLO_SUMMARY_TASK_SKIP_REASON.OUTDATED_TASK_VERSION,
140+
});
128141
return getDeleteTaskRunResult();
129142
}
130143

0 commit comments

Comments
 (0)