Skip to content

Commit 0cadb16

Browse files
cesco-fCopilot
authored andcommitted
[Observability] Add View in discover button in alert details page for SLO burn rate and ES query rules (#233855)
This PR is part of issue #230058. For the ES query rule I'm using the link that is returned in the alert document. The logic to create the link is quite complicated and hard to extract from the rule executor so I thought it would be easier this way. One downside of this is that we cannot set our own time range. For the SLO burn rate rule I've added the data view id to the alert document so that I can create the discover app params using that in the FE. https://github.com/user-attachments/assets/b443e652-584d-478c-b052-23436dfa0404 --------- Co-authored-by: Copilot <[email protected]>
1 parent 6cc8416 commit 0cadb16

File tree

12 files changed

+263
-73
lines changed

12 files changed

+263
-73
lines changed

src/platform/packages/shared/kbn-alerts-as-data-utils/src/schemas/create_schema_from_field_map.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,13 @@ const getSchemaFileContents = (lineWriters: Record<string, LineWriter>, schemaPr
347347
const writeGeneratedFile = (fileName: string, contents: string) => {
348348
const genFileName = path.join(PLUGIN_DIR, fileName);
349349
try {
350+
// Check if file exists and content is the same
351+
if (fs.existsSync(genFileName)) {
352+
const existingContent = fs.readFileSync(genFileName, 'utf8');
353+
if (existingContent === contents) {
354+
return;
355+
}
356+
}
350357
fs.writeFileSync(genFileName, contents);
351358
} catch (err) {
352359
logError(`error writing file: ${genFileName}: ${err.message}`);

src/platform/packages/shared/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const ObservabilitySloAlertOptional = rt.partial({
8484
})
8585
),
8686
'kibana.alert.grouping': schemaUnknown,
87+
'slo.dataViewId': schemaString,
8788
'slo.id': schemaString,
8889
'slo.instanceId': schemaString,
8990
'slo.revision': schemaStringOrNumber,

x-pack/platform/plugins/shared/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 { CustomThresholdParams } from '@kbn/response-ops-rule-params/custom_threshold';
9+
import type { Rule } from '@kbn/alerts-ui-shared';
10+
import type { Filter } from '@kbn/es-query';
11+
import {
12+
FilterStateStore,
13+
buildCustomFilter,
14+
fromKueryExpression,
15+
toElasticsearchQuery,
16+
} from '@kbn/es-query';
17+
import type { DataViewSpec } from '@kbn/data-views-plugin/common';
18+
import { getViewInAppLocatorParams } from '../../../../../../common/custom_threshold_rule/get_view_in_app_url';
19+
20+
export const getCustomThresholdRuleData = ({ rule }: { rule: Rule }) => {
21+
const ruleParams = rule.params as CustomThresholdParams;
22+
const { index } = ruleParams.searchConfiguration;
23+
let dataViewId: string | undefined;
24+
if (typeof index === 'string') {
25+
dataViewId = index;
26+
} else if (index) {
27+
dataViewId = index.title;
28+
}
29+
30+
const filters = ruleParams.criteria
31+
.flatMap(({ metrics }) =>
32+
metrics.map((metric) => {
33+
return metric.filter && dataViewId
34+
? buildCustomFilter(
35+
dataViewId,
36+
toElasticsearchQuery(fromKueryExpression(metric.filter)),
37+
true,
38+
false,
39+
null,
40+
FilterStateStore.APP_STATE
41+
)
42+
: undefined;
43+
})
44+
)
45+
.filter((f): f is Filter => f !== undefined);
46+
47+
return {
48+
discoverAppLocatorParams: {
49+
...getViewInAppLocatorParams({
50+
dataViewId,
51+
searchConfiguration: {
52+
index: ruleParams.searchConfiguration.index as DataViewSpec | string,
53+
query: ruleParams.searchConfiguration.query,
54+
filter: ruleParams.searchConfiguration.filter,
55+
},
56+
}),
57+
filters,
58+
},
59+
};
60+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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 { ALERT_URL } from '@kbn/rule-data-utils';
9+
import type { TopAlert } from '../../../../../typings/alerts';
10+
11+
export const getEsQueryRuleData = ({ alert }: { alert: TopAlert }) => {
12+
const discoverUrl = alert.fields[ALERT_URL];
13+
14+
return discoverUrl ? { discoverUrl } : {};
15+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
export { getCustomThresholdRuleData } from './custom_threshold_rule';
9+
export { getEsQueryRuleData } from './es_query_rule';
10+
export { getSLOBurnRateRuleData } from './slo_burn_rate_rule';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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 { TopAlert } from '../../../../../typings/alerts';
9+
10+
export const getSLOBurnRateRuleData = ({ alert }: { alert: TopAlert }) => {
11+
const dataViewId = 'slo.dataViewId' in alert.fields ? alert.fields['slo.dataViewId'] : undefined;
12+
13+
return typeof dataViewId === 'string' ? { discoverAppLocatorParams: { dataViewId } } : {};
14+
};

x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_discover_url/use_discover_url.test.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
1414
SYNTHETICS_STATUS_RULE,
1515
SYNTHETICS_TLS_RULE,
16+
ES_QUERY_ID,
17+
SLO_BURN_RATE_RULE_TYPE_ID,
1618
} from '@kbn/rule-data-utils';
1719
import type { Rule } from '@kbn/alerts-ui-shared';
1820
import type { TopAlert } from '../../../../typings/alerts';
@@ -173,6 +175,94 @@ describe('useDiscoverUrl', () => {
173175
});
174176
});
175177

178+
it('returns the discover URL from alert for ES query rule', () => {
179+
const expectedDiscoverUrl = '/app/discover#/view/some-saved-search';
180+
const alertWithUrl = {
181+
...MOCK_ALERT,
182+
fields: {
183+
'kibana.alert.url': expectedDiscoverUrl,
184+
},
185+
} as unknown as TopAlert;
186+
187+
const rule = {
188+
ruleTypeId: ES_QUERY_ID,
189+
params: {},
190+
} as unknown as Rule;
191+
192+
const { result } = renderHook(() => useDiscoverUrl({ alert: alertWithUrl, rule }));
193+
194+
expect(result.current.discoverUrl).toBe(expectedDiscoverUrl);
195+
expect(mockGetRedirectUrl).not.toHaveBeenCalled();
196+
});
197+
198+
it('returns null for ES query rule when alert has no URL', () => {
199+
const alertWithoutUrl = {
200+
...MOCK_ALERT,
201+
fields: {},
202+
} as unknown as TopAlert;
203+
204+
const rule = {
205+
ruleTypeId: ES_QUERY_ID,
206+
params: {},
207+
} as unknown as Rule;
208+
209+
const { result } = renderHook(() => useDiscoverUrl({ alert: alertWithoutUrl, rule }));
210+
211+
expect(result.current.discoverUrl).toBeNull();
212+
expect(mockGetRedirectUrl).not.toHaveBeenCalled();
213+
});
214+
215+
it('builds Discover url for SLO burn rate rule when alert has dataViewId', () => {
216+
const expectedDataViewId = 'slo-data-view-id';
217+
const alertWithDataViewId = {
218+
...MOCK_ALERT,
219+
fields: {
220+
'slo.dataViewId': expectedDataViewId,
221+
},
222+
} as unknown as TopAlert;
223+
224+
const rule = {
225+
ruleTypeId: SLO_BURN_RATE_RULE_TYPE_ID,
226+
params: {
227+
sloId: 'test-slo-id',
228+
},
229+
} as unknown as Rule;
230+
231+
const expectedTimeRange = {
232+
from: moment(MOCK_ALERT.start).subtract(30, 'minutes').toISOString(),
233+
to: moment(MOCK_ALERT.start).add(30, 'minutes').toISOString(),
234+
};
235+
236+
mockGetRedirectUrl.mockReturnValue('slo-discover-url');
237+
238+
const { result } = renderHook(() => useDiscoverUrl({ alert: alertWithDataViewId, rule }));
239+
240+
expect(mockGetRedirectUrl).toHaveBeenCalledWith({
241+
dataViewId: expectedDataViewId,
242+
timeRange: expectedTimeRange,
243+
});
244+
expect(result.current.discoverUrl).toBe('slo-discover-url');
245+
});
246+
247+
it('returns null for SLO burn rate rule when alert has no dataViewId', () => {
248+
const alertWithoutDataViewId = {
249+
...MOCK_ALERT,
250+
fields: {},
251+
} as unknown as TopAlert;
252+
253+
const rule = {
254+
ruleTypeId: SLO_BURN_RATE_RULE_TYPE_ID,
255+
params: {
256+
sloId: 'test-slo-id',
257+
},
258+
} as unknown as Rule;
259+
260+
const { result } = renderHook(() => useDiscoverUrl({ alert: alertWithoutDataViewId, rule }));
261+
262+
expect(result.current.discoverUrl).toBeNull();
263+
expect(mockGetRedirectUrl).not.toHaveBeenCalled();
264+
});
265+
176266
it('ignores unsupported rule types', () => {
177267
const rule = {
178268
ruleTypeId: 'some_unsupported_rule',

0 commit comments

Comments
 (0)