Skip to content

Commit d3a6ae8

Browse files
nkhristininkibanamachineelasticmachine
authored
[9.1] Add event based telemetry for detected gaps (#231287) (#232340)
# Backport This will backport the following commits from `main` to `9.1`: - [Add event based telemetry for detected gaps (#231287)](#231287) <!--- Backport version: 10.0.1 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Khristinin Nikita","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-08-19T14:32:19Z","message":"Add event based telemetry for detected gaps (#231287)\n\n## Summary\n\nAdd a telemetry event for each detected gap.\n\nWe report:\n\n* **gapDuration** — duration of the gap, stored in the event log (after\nremediation).\n* **intervalDuration** — duration of the interval in milliseconds.\n* **intervalAndLookbackDuration** — duration of the interval plus\nlookback, in milliseconds (taken from a field in the rule object).\n\n### How to test\n\nIn `kibana.dev.yaml`, add:\n\n```yaml\ntelemetry.optIn: true\n```\n\n1. Create a rule with a small interval and a lookback time of `1m + 1s`.\n2. Enable the rule, wait for execution, then disable the rule.\n3. Wait 5 minutes, then re-enable the rule.\n4. Observe that the rule fails with a gap error message.\n\nAfter that, the telemetry event should appear in\n[staging](https://telemetry-v2-staging.elastic.dev/s/securitysolution/app/discover/app/discover#/view/77cacf50-36b3-11ee-adde-d5df298171dd?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now%2Fd,to:now%2Fd))&_a=(breakdownField:event_type,columns:!(),dataSource:(dataViewId:security-solution-ebt-kibana-server,type:dataView),filters:!(),grid:(),hideChart:!f,interval:auto,query:(language:kuery,query:'event_type:%20%22gap_detected_event%22'),sort:!(!(timestamp,desc))))\nafter some time (in my experience, anywhere from 5 minutes to a couple\nof hours).\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>\nCo-authored-by: Elastic Machine <[email protected]>","sha":"7263f300d1933fd2fcbb83fff4797062cfb67fb7","branchLabelMapping":{"^v9.2.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:skip","v9.2.0"],"title":"Add event based telemetry for detected gaps","number":231287,"url":"https://github.com/elastic/kibana/pull/231287","mergeCommit":{"message":"Add event based telemetry for detected gaps (#231287)\n\n## Summary\n\nAdd a telemetry event for each detected gap.\n\nWe report:\n\n* **gapDuration** — duration of the gap, stored in the event log (after\nremediation).\n* **intervalDuration** — duration of the interval in milliseconds.\n* **intervalAndLookbackDuration** — duration of the interval plus\nlookback, in milliseconds (taken from a field in the rule object).\n\n### How to test\n\nIn `kibana.dev.yaml`, add:\n\n```yaml\ntelemetry.optIn: true\n```\n\n1. Create a rule with a small interval and a lookback time of `1m + 1s`.\n2. Enable the rule, wait for execution, then disable the rule.\n3. Wait 5 minutes, then re-enable the rule.\n4. Observe that the rule fails with a gap error message.\n\nAfter that, the telemetry event should appear in\n[staging](https://telemetry-v2-staging.elastic.dev/s/securitysolution/app/discover/app/discover#/view/77cacf50-36b3-11ee-adde-d5df298171dd?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now%2Fd,to:now%2Fd))&_a=(breakdownField:event_type,columns:!(),dataSource:(dataViewId:security-solution-ebt-kibana-server,type:dataView),filters:!(),grid:(),hideChart:!f,interval:auto,query:(language:kuery,query:'event_type:%20%22gap_detected_event%22'),sort:!(!(timestamp,desc))))\nafter some time (in my experience, anywhere from 5 minutes to a couple\nof hours).\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>\nCo-authored-by: Elastic Machine <[email protected]>","sha":"7263f300d1933fd2fcbb83fff4797062cfb67fb7"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.2.0","branchLabelMappingKey":"^v9.2.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/231287","number":231287,"mergeCommit":{"message":"Add event based telemetry for detected gaps (#231287)\n\n## Summary\n\nAdd a telemetry event for each detected gap.\n\nWe report:\n\n* **gapDuration** — duration of the gap, stored in the event log (after\nremediation).\n* **intervalDuration** — duration of the interval in milliseconds.\n* **intervalAndLookbackDuration** — duration of the interval plus\nlookback, in milliseconds (taken from a field in the rule object).\n\n### How to test\n\nIn `kibana.dev.yaml`, add:\n\n```yaml\ntelemetry.optIn: true\n```\n\n1. Create a rule with a small interval and a lookback time of `1m + 1s`.\n2. Enable the rule, wait for execution, then disable the rule.\n3. Wait 5 minutes, then re-enable the rule.\n4. Observe that the rule fails with a gap error message.\n\nAfter that, the telemetry event should appear in\n[staging](https://telemetry-v2-staging.elastic.dev/s/securitysolution/app/discover/app/discover#/view/77cacf50-36b3-11ee-adde-d5df298171dd?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now%2Fd,to:now%2Fd))&_a=(breakdownField:event_type,columns:!(),dataSource:(dataViewId:security-solution-ebt-kibana-server,type:dataView),filters:!(),grid:(),hideChart:!f,interval:auto,query:(language:kuery,query:'event_type:%20%22gap_detected_event%22'),sort:!(!(timestamp,desc))))\nafter some time (in my experience, anywhere from 5 minutes to a couple\nof hours).\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>\nCo-authored-by: Elastic Machine <[email protected]>","sha":"7263f300d1933fd2fcbb83fff4797062cfb67fb7"}}]}] BACKPORT--> Co-authored-by: kibanamachine <[email protected]> Co-authored-by: Elastic Machine <[email protected]>
1 parent ae5144f commit d3a6ae8

File tree

6 files changed

+206
-1
lines changed

6 files changed

+206
-1
lines changed

x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { TIMESTAMP_RUNTIME_FIELD } from './constants';
4848
import { buildTimestampRuntimeMapping } from './utils/build_timestamp_runtime_mapping';
4949
import { alertsFieldMap, rulesFieldMap } from '../../../../common/field_maps';
5050
import { sendAlertSuppressionTelemetryEvent } from './utils/telemetry/send_alert_suppression_telemetry_event';
51+
import { sendGapDetectedTelemetryEvent } from './utils/telemetry/send_gap_detected_telemetry_event';
5152
import type { RuleParams } from '../rule_schema';
5253
import {
5354
SECURITY_FROM,
@@ -380,6 +381,8 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
380381
remainingGap,
381382
warningStatusMessage: rangeTuplesWarningMessage,
382383
gap,
384+
originalFrom,
385+
originalTo,
383386
} = await getRuleRangeTuples({
384387
startedAt,
385388
previousStartedAt,
@@ -399,6 +402,16 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
399402
if (remainingGap.asMilliseconds() > 0) {
400403
const gapDuration = `${remainingGap.humanize()} (${remainingGap.asMilliseconds()}ms)`;
401404
const gapErrorMessage = `${gapDuration} were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances`;
405+
if (analytics) {
406+
sendGapDetectedTelemetryEvent({
407+
analytics,
408+
interval,
409+
gapDuration: remainingGap,
410+
originalFrom,
411+
originalTo,
412+
ruleParams: params,
413+
});
414+
}
402415
wrapperErrors.push(gapErrorMessage);
403416
await ruleExecutionLogger.logStatusChange({
404417
newStatus: RuleExecutionStatusEnum.failed,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
import { coreMock } from '@kbn/core/server/mocks';
8+
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
9+
import moment from 'moment';
10+
import { GAP_DETECTED_EVENT } from '../../../../telemetry/event_based/events';
11+
12+
import { sendGapDetectedTelemetryEvent } from './send_gap_detected_telemetry_event';
13+
import type { RuleParams } from '../../../rule_schema';
14+
15+
describe('sendGapDetectedTelemetryEvent', () => {
16+
let mockAnalytics: jest.Mocked<AnalyticsServiceSetup>;
17+
let mockCore: ReturnType<typeof coreMock.createSetup>;
18+
19+
beforeEach(() => {
20+
mockCore = coreMock.createSetup();
21+
mockAnalytics = mockCore.analytics;
22+
});
23+
24+
it('should report correct event data with valid parameters', () => {
25+
const interval = '5m';
26+
const gapDuration = moment.duration(10, 'minutes');
27+
const originalFrom = moment('2023-01-01T00:00:00Z');
28+
const originalTo = moment('2023-01-01T01:00:00Z');
29+
const ruleParams = {
30+
type: 'query',
31+
ruleSource: { type: 'external', isCustomized: true },
32+
} as unknown as RuleParams;
33+
34+
sendGapDetectedTelemetryEvent({
35+
analytics: mockAnalytics,
36+
interval,
37+
gapDuration,
38+
originalFrom,
39+
originalTo,
40+
ruleParams,
41+
});
42+
43+
expect(mockAnalytics.reportEvent).toHaveBeenCalledWith(GAP_DETECTED_EVENT.eventType, {
44+
gapDuration: 600000, // 10 minutes in milliseconds
45+
intervalDuration: 300000, // 5 minutes in milliseconds
46+
intervalAndLookbackDuration: 3600000, // 1 hour in milliseconds
47+
ruleType: 'query',
48+
ruleSource: 'external',
49+
isCustomized: true,
50+
});
51+
});
52+
53+
it('should not report event when interval parsing fails', () => {
54+
const invalidInterval = 'invalid-interval';
55+
const gapDuration = moment.duration(10, 'minutes');
56+
const originalFrom = moment('2023-01-01T00:00:00Z');
57+
const originalTo = moment('2023-01-01T01:00:00Z');
58+
const ruleParams = { type: 'query' } as unknown as RuleParams;
59+
60+
sendGapDetectedTelemetryEvent({
61+
analytics: mockAnalytics,
62+
interval: invalidInterval,
63+
gapDuration,
64+
originalFrom,
65+
originalTo,
66+
ruleParams,
67+
});
68+
69+
expect(mockAnalytics.reportEvent).not.toHaveBeenCalled();
70+
});
71+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
8+
import moment from 'moment';
9+
import { GAP_DETECTED_EVENT } from '../../../../telemetry/event_based/events';
10+
import { parseInterval } from '../utils';
11+
import type { RuleParams } from '../../../rule_schema';
12+
13+
export const sendGapDetectedTelemetryEvent = ({
14+
analytics,
15+
interval,
16+
gapDuration,
17+
originalFrom,
18+
originalTo,
19+
ruleParams,
20+
}: {
21+
analytics: AnalyticsServiceSetup;
22+
interval: string;
23+
gapDuration: moment.Duration;
24+
originalFrom: moment.Moment;
25+
originalTo: moment.Moment;
26+
ruleParams: RuleParams;
27+
}) => {
28+
const intervalDuration = parseInterval(interval);
29+
30+
if (!intervalDuration) {
31+
return;
32+
}
33+
34+
const ruleType = ruleParams.type;
35+
const ruleSource = ruleParams.ruleSource;
36+
const isCustomized = ruleSource?.type === 'external' ? ruleSource.isCustomized : false;
37+
38+
analytics.reportEvent(GAP_DETECTED_EVENT.eventType, {
39+
gapDuration: gapDuration.asMilliseconds(),
40+
intervalDuration: intervalDuration.asMilliseconds(),
41+
intervalAndLookbackDuration: moment.duration(originalTo.diff(originalFrom)).asMilliseconds(),
42+
ruleType,
43+
ruleSource: ruleSource?.type,
44+
isCustomized,
45+
});
46+
};

x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,23 @@ describe('utils', () => {
351351
expect(tuples.length).toEqual(1);
352352
expect(warningStatusMessage).toEqual(undefined);
353353
});
354+
355+
test('should return originalFrom and originalTo in response', async () => {
356+
const { originalFrom, originalTo } = await getRuleRangeTuples({
357+
previousStartedAt: moment().subtract(30, 's').toDate(),
358+
startedAt: moment().toDate(),
359+
interval: '30s',
360+
from: 'now-30s',
361+
to: 'now',
362+
maxSignals: 20,
363+
ruleExecutionLogger,
364+
alerting,
365+
});
366+
367+
expect(originalFrom).toBeDefined();
368+
expect(originalTo).toBeDefined();
369+
expect(moment.duration(originalTo.diff(originalFrom)).asSeconds()).toEqual(30);
370+
});
354371
});
355372

356373
describe('calculateFromValue', () => {

x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,13 @@ export const getRuleRangeTuples = async ({
427427
interval
428428
)}"`
429429
);
430-
return { tuples, remainingGap: moment.duration(0), warningStatusMessage };
430+
return {
431+
tuples,
432+
remainingGap: moment.duration(0),
433+
warningStatusMessage,
434+
originalFrom,
435+
originalTo,
436+
};
431437
}
432438

433439
const gap = getGapBetweenRuns({
@@ -469,6 +475,8 @@ export const getRuleRangeTuples = async ({
469475
remainingGap: moment.duration(remainingGapMilliseconds),
470476
warningStatusMessage,
471477
gap: gapRange,
478+
originalFrom,
479+
originalTo,
472480
};
473481
};
474482

x-pack/solutions/security/plugins/security_solution/server/lib/telemetry/event_based/events.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,6 +1567,55 @@ export const ENDPOINT_WORKFLOW_INSIGHTS_REMEDIATED_EVENT: EventTypeOpts<{
15671567
},
15681568
};
15691569

1570+
export const GAP_DETECTED_EVENT: EventTypeOpts<{
1571+
gapDuration: number;
1572+
intervalDuration: number;
1573+
intervalAndLookbackDuration: number;
1574+
ruleType: string;
1575+
ruleSource: string;
1576+
isCustomized: boolean;
1577+
}> = {
1578+
eventType: 'gap_detected_event',
1579+
schema: {
1580+
gapDuration: {
1581+
type: 'long',
1582+
_meta: {
1583+
description: 'The duration of the gap',
1584+
},
1585+
},
1586+
intervalDuration: {
1587+
type: 'long',
1588+
_meta: {
1589+
description: 'The duration of the interval',
1590+
},
1591+
},
1592+
intervalAndLookbackDuration: {
1593+
type: 'long',
1594+
_meta: {
1595+
description: 'The duration of the interval and lookback',
1596+
},
1597+
},
1598+
ruleType: {
1599+
type: 'keyword',
1600+
_meta: {
1601+
description: 'The type of the rule',
1602+
},
1603+
},
1604+
ruleSource: {
1605+
type: 'keyword',
1606+
_meta: {
1607+
description: 'The source of the rule',
1608+
},
1609+
},
1610+
isCustomized: {
1611+
type: 'boolean',
1612+
_meta: {
1613+
description: 'Whether the prebuilt rule is customized',
1614+
},
1615+
},
1616+
},
1617+
};
1618+
15701619
export const events = [
15711620
RISK_SCORE_EXECUTION_SUCCESS_EVENT,
15721621
RISK_SCORE_EXECUTION_ERROR_EVENT,
@@ -1600,4 +1649,5 @@ export const events = [
16001649
SIEM_MIGRATIONS_RULE_TRANSLATION_FAILURE,
16011650
SIEM_MIGRATIONS_PREBUILT_RULES_MATCH,
16021651
SIEM_MIGRATIONS_INTEGRATIONS_MATCH,
1652+
GAP_DETECTED_EVENT,
16031653
];

0 commit comments

Comments
 (0)