Skip to content

Commit fdd4978

Browse files
authored
[ResponseOps] Cases activity index (#224085)
**Merging into a feature branch** ## Summary This PR adds the `.internal.cases-activity` analytics index. ## How to test 1. Add `xpack.cases.analytics.index.enabled: true` to `kibana.dev.yml` 2. Check out [this branch](elastic/elasticsearch#129414) from the ES project. 3. Start Elastic Search with `yarn es source`. 4. Add a bunch of activity to your cases. Edit tags, category, etc. 5. Go to the dev tools and confirm this activity shows up in the index - `GET /.internal.cases-activity/_search`.
1 parent 698a3fe commit fdd4978

File tree

7 files changed

+264
-2
lines changed

7 files changed

+264
-2
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
9+
10+
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
11+
12+
export const CAI_ACTIVITY_INDEX_NAME = '.internal.cases-activity';
13+
14+
export const CAI_ACTIVITY_INDEX_VERSION = 1;
15+
16+
export const CAI_ACTIVITY_SOURCE_QUERY: QueryDslQueryContainer = {
17+
term: {
18+
type: 'cases-user-actions',
19+
},
20+
};
21+
22+
export const CAI_ACTIVITY_SOURCE_INDEX = ALERTING_CASES_SAVED_OBJECT_INDEX;
23+
24+
export const CAI_ACTIVITY_BACKFILL_TASK_ID = 'cai_activity_backfill_task';
25+
26+
export const CAI_ACTIVITY_SYNCHRONIZATION_TASK_ID = 'cai_cases_activity_synchronization_task';
27+
28+
export const getActivitySynchronizationSourceQuery = (
29+
lastSyncAt: Date
30+
): QueryDslQueryContainer => ({
31+
bool: {
32+
must: [
33+
{
34+
term: {
35+
type: 'cases-user-actions',
36+
},
37+
},
38+
{
39+
range: {
40+
'cases-user-actions.created_at': {
41+
gte: lastSyncAt.toISOString(),
42+
},
43+
},
44+
},
45+
],
46+
},
47+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 type { ElasticsearchClient, Logger } from '@kbn/core/server';
9+
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
10+
import { AnalyticsIndex } from '../analytics_index';
11+
import {
12+
CAI_ACTIVITY_INDEX_NAME,
13+
CAI_ACTIVITY_INDEX_VERSION,
14+
CAI_ACTIVITY_SOURCE_INDEX,
15+
CAI_ACTIVITY_SOURCE_QUERY,
16+
CAI_ACTIVITY_BACKFILL_TASK_ID,
17+
CAI_ACTIVITY_SYNCHRONIZATION_TASK_ID,
18+
} from './constants';
19+
import { CAI_ACTIVITY_INDEX_MAPPINGS } from './mappings';
20+
import { CAI_ACTIVITY_INDEX_SCRIPT, CAI_ACTIVITY_INDEX_SCRIPT_ID } from './painless_scripts';
21+
import { scheduleCAISynchronizationTask } from '../tasks/synchronization_task';
22+
23+
export const createActivityAnalyticsIndex = ({
24+
esClient,
25+
logger,
26+
isServerless,
27+
taskManager,
28+
}: {
29+
esClient: ElasticsearchClient;
30+
logger: Logger;
31+
isServerless: boolean;
32+
taskManager: TaskManagerStartContract;
33+
}): AnalyticsIndex =>
34+
new AnalyticsIndex({
35+
logger,
36+
esClient,
37+
isServerless,
38+
taskManager,
39+
indexName: CAI_ACTIVITY_INDEX_NAME,
40+
indexVersion: CAI_ACTIVITY_INDEX_VERSION,
41+
mappings: CAI_ACTIVITY_INDEX_MAPPINGS,
42+
painlessScriptId: CAI_ACTIVITY_INDEX_SCRIPT_ID,
43+
painlessScript: CAI_ACTIVITY_INDEX_SCRIPT,
44+
taskId: CAI_ACTIVITY_BACKFILL_TASK_ID,
45+
sourceIndex: CAI_ACTIVITY_SOURCE_INDEX,
46+
sourceQuery: CAI_ACTIVITY_SOURCE_QUERY,
47+
});
48+
49+
export const scheduleActivityAnalyticsSyncTask = ({
50+
taskManager,
51+
logger,
52+
}: {
53+
taskManager: TaskManagerStartContract;
54+
logger: Logger;
55+
}) => {
56+
scheduleCAISynchronizationTask({
57+
taskId: CAI_ACTIVITY_SYNCHRONIZATION_TASK_ID,
58+
sourceIndex: CAI_ACTIVITY_SOURCE_INDEX,
59+
destIndex: CAI_ACTIVITY_INDEX_NAME,
60+
taskManager,
61+
logger,
62+
}).catch((e) => {
63+
logger.error(
64+
`Error scheduling ${CAI_ACTIVITY_SYNCHRONIZATION_TASK_ID} task, received ${e.message}`
65+
);
66+
});
67+
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types';
9+
10+
export const CAI_ACTIVITY_INDEX_MAPPINGS: MappingTypeMapping = {
11+
dynamic: false,
12+
properties: {
13+
'@timestamp': {
14+
type: 'date',
15+
},
16+
case_id: {
17+
type: 'keyword',
18+
},
19+
action: {
20+
type: 'keyword',
21+
},
22+
type: {
23+
type: 'keyword',
24+
},
25+
payload: {
26+
properties: {
27+
status: {
28+
type: 'keyword',
29+
},
30+
tags: {
31+
type: 'keyword',
32+
},
33+
category: {
34+
type: 'keyword',
35+
},
36+
severity: {
37+
type: 'keyword',
38+
},
39+
},
40+
},
41+
created_at: {
42+
type: 'date',
43+
},
44+
created_at_ms: {
45+
type: 'long',
46+
},
47+
created_by: {
48+
properties: {
49+
username: {
50+
type: 'keyword',
51+
},
52+
profile_uid: {
53+
type: 'keyword',
54+
},
55+
full_name: {
56+
type: 'keyword',
57+
},
58+
email: {
59+
type: 'keyword',
60+
},
61+
},
62+
},
63+
owner: {
64+
type: 'keyword',
65+
},
66+
space_ids: {
67+
type: 'keyword',
68+
},
69+
},
70+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 type { StoredScript } from '@elastic/elasticsearch/lib/api/types';
9+
import { CAI_ACTIVITY_INDEX_VERSION } from './constants';
10+
11+
export const CAI_ACTIVITY_INDEX_SCRIPT_ID = `cai_activity_script_${CAI_ACTIVITY_INDEX_VERSION}`;
12+
export const CAI_ACTIVITY_INDEX_SCRIPT: StoredScript = {
13+
lang: 'painless',
14+
source: `
15+
def source = [:];
16+
source.putAll(ctx._source);
17+
ctx._source.clear();
18+
19+
ctx._source.action = source["cases-user-actions"].action;
20+
ctx._source.type = source["cases-user-actions"].type;
21+
22+
ZonedDateTime zdt_created =
23+
ZonedDateTime.parse(source["cases-user-actions"].created_at);
24+
ctx._source.created_at_ms = zdt_created.toInstant().toEpochMilli();
25+
ctx._source.created_at = source["cases-user-actions"].created_at;
26+
27+
if (source["cases-user-actions"].created_by != null) {
28+
ctx._source.created_by = new HashMap();
29+
ctx._source.created_by.full_name = source["cases-user-actions"].created_by.full_name;
30+
ctx._source.created_by.username = source["cases-user-actions"].created_by.username;
31+
ctx._source.created_by.profile_uid = source["cases-user-actions"].created_by.profile_uid;
32+
ctx._source.created_by.email = source["cases-user-actions"].created_by.email;
33+
}
34+
35+
if (source["cases-user-actions"].payload != null) {
36+
ctx._source.payload = new HashMap();
37+
38+
if (source["cases-user-actions"].type == "severity" && source["cases-user-actions"].payload.severity != null) {
39+
ctx._source.payload.severity = source["cases-user-actions"].payload.severity;
40+
}
41+
42+
if (source["cases-user-actions"].type == "category" && source["cases-user-actions"].payload.category != null) {
43+
ctx._source.payload.category = source["cases-user-actions"].payload.category;
44+
}
45+
46+
if (source["cases-user-actions"].type == "status" && source["cases-user-actions"].payload.status != null) {
47+
ctx._source.payload.status = source["cases-user-actions"].payload.status;
48+
}
49+
50+
if (source["cases-user-actions"].type == "tags" && source["cases-user-actions"].payload.tags != null) {
51+
ctx._source.payload.tags = source["cases-user-actions"].payload.tags;
52+
}
53+
}
54+
55+
for (item in source.references) {
56+
if (item.type == "cases") {
57+
ctx._source.case_id = item.id;
58+
}
59+
}
60+
61+
ctx._source.owner = source["cases-user-actions"].owner;
62+
ctx._source.space_ids = source.namespaces;
63+
`,
64+
};

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import {
1616
CAI_COMMENTS_INDEX_NAME,
1717
getCommentsSynchronizationSourceQuery,
1818
} from './comments_index/constants';
19+
import {
20+
CAI_ACTIVITY_INDEX_NAME,
21+
getActivitySynchronizationSourceQuery,
22+
} from './activity_index/constants';
1923

2024
export const CAI_NUMBER_OF_SHARDS = 1;
2125
/** Allocate 1 replica if there are enough data nodes, otherwise continue with 0 */
@@ -41,4 +45,5 @@ export const SYNCHRONIZATION_QUERIES_DICTIONARY: Record<
4145
[CAI_CASES_INDEX_NAME]: getCasesSynchronizationSourceQuery,
4246
[CAI_COMMENTS_INDEX_NAME]: getCommentsSynchronizationSourceQuery,
4347
[CAI_ATTACHMENTS_INDEX_NAME]: getAttachmentsSynchronizationSourceQuery,
48+
[CAI_ACTIVITY_INDEX_NAME]: getActivitySynchronizationSourceQuery,
4449
};

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from './attachments_index';
2020
import { createCasesAnalyticsIndex, scheduleCasesAnalyticsSyncTask } from './cases_index';
2121
import { createCommentsAnalyticsIndex, scheduleCommentsAnalyticsSyncTask } from './comments_index';
22+
import { createActivityAnalyticsIndex, scheduleActivityAnalyticsSyncTask } from './activity_index';
2223

2324
export const createCasesAnalyticsIndexes = ({
2425
esClient,
@@ -49,11 +50,18 @@ export const createCasesAnalyticsIndexes = ({
4950
isServerless,
5051
taskManager,
5152
});
53+
const casesActivityIndex = createActivityAnalyticsIndex({
54+
logger,
55+
esClient,
56+
isServerless,
57+
taskManager,
58+
});
5259

5360
return Promise.all([
5461
casesIndex.upsertIndex(),
5562
casesAttachmentsIndex.upsertIndex(),
5663
casesCommentsIndex.upsertIndex(),
64+
casesActivityIndex.upsertIndex(),
5765
]);
5866
};
5967

@@ -77,6 +85,7 @@ export const scheduleCasesAnalyticsSyncTasks = ({
7785
taskManager: TaskManagerStartContract;
7886
logger: Logger;
7987
}) => {
88+
scheduleActivityAnalyticsSyncTask({ taskManager, logger });
8089
scheduleCasesAnalyticsSyncTask({ taskManager, logger });
8190
scheduleCommentsAnalyticsSyncTask({ taskManager, logger });
8291
scheduleAttachmentsAnalyticsSyncTask({ taskManager, logger });

x-pack/platform/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,9 @@ export default function ({ getService }: FtrProviderContext) {
141141
'alerts_invalidate_api_keys',
142142
'apm-source-map-migration-task',
143143
'apm-telemetry-task',
144+
'cai:cases_analytics_index_backfill',
145+
'cai:cases_analytics_index_synchronization',
144146
'cases-telemetry-task',
145-
'cases:analytics_index_backfill',
146-
'cases:analytics_index_synchronization',
147147
'cloud_security_posture-stats_task',
148148
'dashboard_telemetry',
149149
'endpoint:complete-external-response-actions',

0 commit comments

Comments
 (0)