Skip to content

Commit 7b1e564

Browse files
authored
[ResponseOps] Cases analytics index synchronization (#222820)
Closes [#221232](#221232) **Merging into a feature branch** ## Summary This PR adds: - Synchronization task registration. - Synchronization task scheduling. - Testing ## How to test A few things are needed. 1. Create a role that has index creation permission. Below is what works for me; it might be overkill. <img width="1030" alt="Screenshot 2025-04-25 at 11 30 45" src="https://github.com/user-attachments/assets/cbb7b384-e438-43ec-bbca-a18e62243523" /> 2. Create a user with the role created in step 1. Let's call the user `index_creator`. **The password should be** `changeme`. <img width="842" alt="Screenshot 2025-04-25 at 11 31 27" src="https://github.com/user-attachments/assets/a9547c96-086e-43b4-a442-a18f558dcb96" /> 6. Start Kibana with this user as the Elasticsearch user - `yarn start` with the option `--elasticsearch.username=index_creator`. 7. You can check the content of these indexes in the Dev Tools. ``` GET /.internal.cases/_search GET /.internal.cases-comments/_search GET /.internal.cases-attachments/_search ``` 8. Create Cases, Comments, and Attachments. 9. After about 5 minutes, check again the content of the indexes using the instructions in step 7. 10. Confirm the Cases, Comments, and Attachments created in step 8 can be seen in the analytics indexes.
1 parent fa8ef65 commit 7b1e564

File tree

19 files changed

+1122
-54
lines changed

19 files changed

+1122
-54
lines changed

x-pack/platform/plugins/shared/cases/server/cases_analytics/analytics_index.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ describe('AnalyticsIndex', () => {
187187
expect(scheduleCAIBackfillTaskMock).toBeCalledTimes(0);
188188

189189
expect(logger.debug).toBeCalledWith(
190-
`[${indexName}] Mapping version is up to date. Skipping update.`
190+
`[${indexName}] Mapping version is up to date. Skipping update.`,
191+
{ tags: ['cai-index-creation', `${indexName}`] }
191192
);
192193
});
193194

@@ -204,7 +205,8 @@ describe('AnalyticsIndex', () => {
204205
expect(scheduleCAIBackfillTaskMock).toBeCalledTimes(0);
205206

206207
expect(logger.debug).toBeCalledWith(
207-
`[${indexName}] Mapping version is up to date. Skipping update.`
208+
`[${indexName}] Mapping version is up to date. Skipping update.`,
209+
{ tags: ['cai-index-creation', `${indexName}`] }
208210
);
209211
});
210212

x-pack/platform/plugins/shared/cases/server/cases_analytics/analytics_index.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,10 @@ export class AnalyticsIndex {
122122
const indexExists = await this.indexExists();
123123

124124
if (!indexExists) {
125-
this.logger.debug(`[${this.indexName}] Index does not exist. Creating.`);
125+
this.logDebug(`Index does not exist. Creating.`);
126126
await this.createIndexMapping();
127127
} else {
128-
this.logger.debug(`[${this.indexName}] Index exists. Updating mapping.`);
128+
this.logDebug(`Index exists. Updating mapping.`);
129129
await this.updateIndexMapping();
130130
}
131131
} catch (error) {
@@ -140,7 +140,7 @@ export class AnalyticsIndex {
140140
if (shouldUpdateMapping) {
141141
await this.updateMapping();
142142
} else {
143-
this.logger.debug(`[${this.indexName}] Mapping version is up to date. Skipping update.`);
143+
this.logDebug(`Mapping version is up to date. Skipping update.`);
144144
}
145145
} catch (error) {
146146
this.handleError('Failed to update the index mapping.', error);
@@ -154,24 +154,24 @@ export class AnalyticsIndex {
154154
}
155155

156156
private async updateMapping() {
157-
this.logger.debug(`[${this.indexName}] Updating the painless script.`);
157+
this.logDebug(`Updating the painless script.`);
158158
await this.putScript();
159159

160-
this.logger.debug(`[${this.indexName}] Updating index mapping.`);
160+
this.logDebug(`Updating index mapping.`);
161161
await this.esClient.indices.putMapping({
162162
index: this.indexName,
163163
...this.mappings,
164164
});
165165

166-
this.logger.debug(`[${this.indexName}] Scheduling the backfill task.`);
166+
this.logDebug(`Scheduling the backfill task.`);
167167
await this.scheduleBackfillTask();
168168
}
169169

170170
private async createIndexMapping() {
171-
this.logger.debug(`[${this.indexName}] Creating painless script.`);
171+
this.logDebug(`Creating painless script.`);
172172
await this.putScript();
173173

174-
this.logger.debug(`[${this.indexName}] Creating index.`);
174+
this.logDebug(`Creating index.`);
175175
await this.esClient.indices.create({
176176
index: this.indexName,
177177
timeout: CAI_DEFAULT_TIMEOUT,
@@ -181,12 +181,12 @@ export class AnalyticsIndex {
181181
},
182182
});
183183

184-
this.logger.debug(`[${this.indexName}] Scheduling the backfill task.`);
184+
this.logDebug(`Scheduling the backfill task.`);
185185
await this.scheduleBackfillTask();
186186
}
187187

188188
private async indexExists(): Promise<boolean> {
189-
this.logger.debug(`[${this.indexName}] Checking if index exists.`);
189+
this.logDebug(`Checking if index exists.`);
190190
return this.esClient.indices.exists({
191191
index: this.indexName,
192192
});
@@ -197,12 +197,6 @@ export class AnalyticsIndex {
197197
return currentMapping[this.indexName].mappings._meta?.mapping_version < this.indexVersion;
198198
}
199199

200-
private handleError(message: string, error: EsErrors.ElasticsearchClientError) {
201-
this.logger.error(`[${this.indexName}] ${message} Error message: ${error.message}`);
202-
203-
throw error;
204-
}
205-
206200
private async putScript() {
207201
await this.esClient.putScript({
208202
id: this.painlessScriptId,
@@ -217,8 +211,8 @@ export class AnalyticsIndex {
217211
indexVersion: number;
218212
painlessScriptId: string;
219213
}): MappingMeta {
220-
this.logger.debug(
221-
`[${this.indexName}] Construction mapping._meta. Index version: ${indexVersion}. Painless script: ${painlessScriptId}.`
214+
this.logDebug(
215+
`Construction mapping._meta. Index version: ${indexVersion}. Painless script: ${painlessScriptId}.`
222216
);
223217

224218
return {
@@ -227,6 +221,17 @@ export class AnalyticsIndex {
227221
};
228222
}
229223

224+
public logDebug(message: string) {
225+
this.logger.debug(`[${this.indexName}] ${message}`, {
226+
tags: ['cai-index-creation', this.indexName],
227+
});
228+
}
229+
230+
private handleError(message: string, error: EsErrors.ElasticsearchClientError) {
231+
this.logger.error(`[${this.indexName}] ${message} Error message: ${error.message}`);
232+
233+
throw error;
234+
}
230235
private async scheduleBackfillTask() {
231236
await scheduleCAIBackfillTask({
232237
taskId: this.taskId,

x-pack/platform/plugins/shared/cases/server/cases_analytics/attachments_index/constants.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,48 @@ export const CAI_ATTACHMENTS_SOURCE_QUERY: QueryDslQueryContainer = {
3535
export const CAI_ATTACHMENTS_SOURCE_INDEX = ALERTING_CASES_SAVED_OBJECT_INDEX;
3636

3737
export const CAI_ATTACHMENTS_BACKFILL_TASK_ID = 'cai_attachments_backfill_task';
38+
39+
export const CAI_ATTACHMENTS_SYNCHRONIZATION_TASK_ID = 'cai_cases_attachments_synchronization_task';
40+
41+
export const getAttachmentsSynchronizationSourceQuery = (
42+
lastSyncAt: Date
43+
): QueryDslQueryContainer => ({
44+
bool: {
45+
must: [
46+
{
47+
term: {
48+
type: 'cases-comments',
49+
},
50+
},
51+
{
52+
bool: {
53+
must_not: {
54+
term: {
55+
'cases-comments.type': 'user',
56+
},
57+
},
58+
},
59+
},
60+
{
61+
bool: {
62+
should: [
63+
{
64+
range: {
65+
'cases-comments.created_at': {
66+
gte: lastSyncAt.toISOString(),
67+
},
68+
},
69+
},
70+
{
71+
range: {
72+
'cases-comments.updated_at': {
73+
gte: lastSyncAt.toISOString(),
74+
},
75+
},
76+
},
77+
],
78+
},
79+
},
80+
],
81+
},
82+
});

x-pack/platform/plugins/shared/cases/server/cases_analytics/attachments_index/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
CAI_ATTACHMENTS_SOURCE_INDEX,
1515
CAI_ATTACHMENTS_SOURCE_QUERY,
1616
CAI_ATTACHMENTS_BACKFILL_TASK_ID,
17+
CAI_ATTACHMENTS_SYNCHRONIZATION_TASK_ID,
1718
} from './constants';
1819
import { CAI_ATTACHMENTS_INDEX_MAPPINGS } from './mappings';
1920
import { CAI_ATTACHMENTS_INDEX_SCRIPT, CAI_ATTACHMENTS_INDEX_SCRIPT_ID } from './painless_scripts';
21+
import { scheduleCAISynchronizationTask } from '../tasks/synchronization_task';
2022

2123
export const createAttachmentsAnalyticsIndex = ({
2224
esClient,
@@ -43,3 +45,23 @@ export const createAttachmentsAnalyticsIndex = ({
4345
sourceIndex: CAI_ATTACHMENTS_SOURCE_INDEX,
4446
sourceQuery: CAI_ATTACHMENTS_SOURCE_QUERY,
4547
});
48+
49+
export const scheduleAttachmentsAnalyticsSyncTask = ({
50+
taskManager,
51+
logger,
52+
}: {
53+
taskManager: TaskManagerStartContract;
54+
logger: Logger;
55+
}) => {
56+
scheduleCAISynchronizationTask({
57+
taskId: CAI_ATTACHMENTS_SYNCHRONIZATION_TASK_ID,
58+
sourceIndex: CAI_ATTACHMENTS_SOURCE_INDEX,
59+
destIndex: CAI_ATTACHMENTS_INDEX_NAME,
60+
taskManager,
61+
logger,
62+
}).catch((e) => {
63+
logger.error(
64+
`Error scheduling ${CAI_ATTACHMENTS_SYNCHRONIZATION_TASK_ID} task, received ${e.message}`
65+
);
66+
});
67+
};

x-pack/platform/plugins/shared/cases/server/cases_analytics/cases_index/constants.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,37 @@ export const CAI_CASES_SOURCE_QUERY: QueryDslQueryContainer = {
2222
export const CAI_CASES_SOURCE_INDEX = ALERTING_CASES_SAVED_OBJECT_INDEX;
2323

2424
export const CAI_CASES_BACKFILL_TASK_ID = 'cai_cases_backfill_task';
25+
26+
export const CAI_CASES_SYNCHRONIZATION_TASK_ID = 'cai_cases_synchronization_task';
27+
28+
export const getCasesSynchronizationSourceQuery = (lastSyncAt: Date): QueryDslQueryContainer => ({
29+
bool: {
30+
must: [
31+
{
32+
term: {
33+
type: 'cases',
34+
},
35+
},
36+
{
37+
bool: {
38+
should: [
39+
{
40+
range: {
41+
'cases.created_at': {
42+
gte: lastSyncAt.toISOString(),
43+
},
44+
},
45+
},
46+
{
47+
range: {
48+
'cases.updated_at': {
49+
gte: lastSyncAt.toISOString(),
50+
},
51+
},
52+
},
53+
],
54+
},
55+
},
56+
],
57+
},
58+
});

x-pack/platform/plugins/shared/cases/server/cases_analytics/cases_index/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
CAI_CASES_SOURCE_INDEX,
1515
CAI_CASES_SOURCE_QUERY,
1616
CAI_CASES_BACKFILL_TASK_ID,
17+
CAI_CASES_SYNCHRONIZATION_TASK_ID,
1718
} from './constants';
1819
import { CAI_CASES_INDEX_MAPPINGS } from './mappings';
1920
import { CAI_CASES_INDEX_SCRIPT_ID, CAI_CASES_INDEX_SCRIPT } from './painless_scripts';
21+
import { scheduleCAISynchronizationTask } from '../tasks/synchronization_task';
2022

2123
export const createCasesAnalyticsIndex = ({
2224
esClient,
@@ -43,3 +45,23 @@ export const createCasesAnalyticsIndex = ({
4345
sourceIndex: CAI_CASES_SOURCE_INDEX,
4446
sourceQuery: CAI_CASES_SOURCE_QUERY,
4547
});
48+
49+
export const scheduleCasesAnalyticsSyncTask = ({
50+
taskManager,
51+
logger,
52+
}: {
53+
taskManager: TaskManagerStartContract;
54+
logger: Logger;
55+
}) => {
56+
scheduleCAISynchronizationTask({
57+
taskId: CAI_CASES_SYNCHRONIZATION_TASK_ID,
58+
sourceIndex: CAI_CASES_SOURCE_INDEX,
59+
destIndex: CAI_CASES_INDEX_NAME,
60+
taskManager,
61+
logger,
62+
}).catch((e) => {
63+
logger.error(
64+
`Error scheduling ${CAI_CASES_SYNCHRONIZATION_TASK_ID} task, received ${e.message}`
65+
);
66+
});
67+
};

x-pack/platform/plugins/shared/cases/server/cases_analytics/comments_index/constants.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,44 @@ export const CAI_COMMENTS_SOURCE_QUERY: QueryDslQueryContainer = {
3333
export const CAI_COMMENTS_SOURCE_INDEX = ALERTING_CASES_SAVED_OBJECT_INDEX;
3434

3535
export const CAI_COMMENTS_BACKFILL_TASK_ID = 'cai_comments_backfill_task';
36+
37+
export const CAI_COMMENTS_SYNCHRONIZATION_TASK_ID = 'cai_cases_comments_synchronization_task';
38+
39+
export const getCommentsSynchronizationSourceQuery = (
40+
lastSyncAt: Date
41+
): QueryDslQueryContainer => ({
42+
bool: {
43+
must: [
44+
{
45+
term: {
46+
'cases-comments.type': 'user',
47+
},
48+
},
49+
{
50+
term: {
51+
type: 'cases-comments',
52+
},
53+
},
54+
{
55+
bool: {
56+
should: [
57+
{
58+
range: {
59+
'cases-comments.created_at': {
60+
gte: lastSyncAt.toISOString(),
61+
},
62+
},
63+
},
64+
{
65+
range: {
66+
'cases-comments.updated_at': {
67+
gte: lastSyncAt.toISOString(),
68+
},
69+
},
70+
},
71+
],
72+
},
73+
},
74+
],
75+
},
76+
});

x-pack/platform/plugins/shared/cases/server/cases_analytics/comments_index/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
CAI_COMMENTS_SOURCE_INDEX,
1515
CAI_COMMENTS_SOURCE_QUERY,
1616
CAI_COMMENTS_BACKFILL_TASK_ID,
17+
CAI_COMMENTS_SYNCHRONIZATION_TASK_ID,
1718
} from './constants';
1819
import { CAI_COMMENTS_INDEX_MAPPINGS } from './mappings';
1920
import { CAI_COMMENTS_INDEX_SCRIPT, CAI_COMMENTS_INDEX_SCRIPT_ID } from './painless_scripts';
21+
import { scheduleCAISynchronizationTask } from '../tasks/synchronization_task';
2022

2123
export const createCommentsAnalyticsIndex = ({
2224
esClient,
@@ -43,3 +45,23 @@ export const createCommentsAnalyticsIndex = ({
4345
sourceIndex: CAI_COMMENTS_SOURCE_INDEX,
4446
sourceQuery: CAI_COMMENTS_SOURCE_QUERY,
4547
});
48+
49+
export const scheduleCommentsAnalyticsSyncTask = ({
50+
taskManager,
51+
logger,
52+
}: {
53+
taskManager: TaskManagerStartContract;
54+
logger: Logger;
55+
}) => {
56+
scheduleCAISynchronizationTask({
57+
taskId: CAI_COMMENTS_SYNCHRONIZATION_TASK_ID,
58+
sourceIndex: CAI_COMMENTS_SOURCE_INDEX,
59+
destIndex: CAI_COMMENTS_INDEX_NAME,
60+
taskManager,
61+
logger,
62+
}).catch((e) => {
63+
logger.error(
64+
`Error scheduling ${CAI_COMMENTS_SYNCHRONIZATION_TASK_ID} task, received ${e.message}`
65+
);
66+
});
67+
};

0 commit comments

Comments
 (0)