Skip to content

Commit 7e02331

Browse files
authored
[SigEvents] Only show rule-backed queries in stream details Significant Events tab (elastic#254953)
It closes elastic#254950 ## Summary This PR refines the stream details Significant Events tab to only display rule-backed queries and changes the navigation in SigEvents Discovery to land in the Significant Events tab when clicking on a stream name. ### Changes - **Rule-backed filter**: Adds a `ruleBacked` query parameter to the `GET /internal/streams/_significant_events` API, threaded through to the `QueryClient` methods (`getQueryLinks`, `findQueries`). The stream details Significant Events tab passes `ruleBacked: true` to only show queries that have been promoted to a backing Kibana rule. The Discovery page continues to show all queries (backed and unbacked). - **Discovery navigation**: Clicking a stream name in the SigEvents Discovery streams table now navigates directly to the Significant Events management tab (`/{key}/management/significantEvents`) instead of the default stream overview. Before: https://github.com/user-attachments/assets/c88e0a23-5d38-4c1e-aa69-e092dc01fbe6 After: https://github.com/user-attachments/assets/b05861f2-fee6-4b66-b0c7-f4812a35b7e2
1 parent a420404 commit 7e02331

8 files changed

Lines changed: 115 additions & 82 deletions

File tree

x-pack/platform/plugins/shared/streams/common/queries.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,14 @@
77

88
import type { StreamQuery, StreamQueryInput } from '@kbn/streams-schema';
99

10-
// Legacy stored query links may not include rule_backed and should be treated as already backed.
11-
export const LEGACY_RULE_BACKED_FALLBACK = true;
12-
1310
export interface QueryLink {
1411
'asset.uuid': string;
1512
'asset.type': 'query';
1613
'asset.id': string;
1714
query: StreamQuery;
1815
stream_name: string;
1916
/** Whether a Kibana rule exists for this query. */
20-
rule_backed?: boolean;
17+
rule_backed: boolean;
2118
/** The deterministic ID of the Kibana rule associated with this query. */
2219
rule_id: string;
2320
}

x-pack/platform/plugins/shared/streams/server/lib/significant_events/read_significant_events_from_alerts_indices.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,31 @@ import type { IScopedClusterClient } from '@kbn/core/server';
1313
import type { ChangePointType } from '@kbn/es-types/src';
1414
import type { StreamQuery, SignificantEventsGetResponse } from '@kbn/streams-schema';
1515
import { get, isArray, isEmpty, keyBy } from 'lodash';
16-
import { LEGACY_RULE_BACKED_FALLBACK, type QueryLink } from '../../../common/queries';
17-
import type { QueryClient } from '../streams/assets/query/query_client';
16+
import type { QueryLink } from '../../../common/queries';
17+
import type { QueryClient, QueryLinkFilters } from '../streams/assets/query/query_client';
1818
import { parseError } from '../streams/errors/parse_error';
1919
import { SecurityError } from '../streams/errors/security_error';
2020

2121
export async function readSignificantEventsFromAlertsIndices(
22-
params: { streamNames?: string[]; from: Date; to: Date; bucketSize: string; query?: string },
22+
params: {
23+
streamNames?: string[];
24+
from: Date;
25+
to: Date;
26+
bucketSize: string;
27+
query?: string;
28+
filters?: QueryLinkFilters;
29+
},
2330
dependencies: {
2431
queryClient: QueryClient;
2532
scopedClusterClient: IScopedClusterClient;
2633
}
2734
): Promise<SignificantEventsGetResponse> {
2835
const { queryClient, scopedClusterClient } = dependencies;
29-
const { streamNames = [], from, to, bucketSize, query } = params;
36+
const { streamNames = [], from, to, bucketSize, query, filters } = params;
3037

3138
const queryLinks = query
32-
? await queryClient.findQueries(streamNames, query)
33-
: await queryClient.getQueryLinks(streamNames);
39+
? await queryClient.findQueries(streamNames, query, filters)
40+
: await queryClient.getQueryLinks(streamNames, filters);
3441

3542
if (isEmpty(queryLinks)) {
3643
return { significant_events: [], aggregated_occurrences: [] };
@@ -142,7 +149,7 @@ export async function readSignificantEventsFromAlertsIndices(
142149
stationary: { p_value: 0, change_point: 0 },
143150
},
144151
},
145-
rule_backed: queryLink.rule_backed ?? LEGACY_RULE_BACKED_FALLBACK,
152+
rule_backed: queryLink.rule_backed,
146153
})),
147154
aggregated_occurrences: [],
148155
};
@@ -168,7 +175,7 @@ export async function readSignificantEventsFromAlertsIndices(
168175
count: occurrence.doc_count,
169176
}))
170177
: [],
171-
rule_backed: queryLink.rule_backed ?? LEGACY_RULE_BACKED_FALLBACK,
178+
rule_backed: queryLink.rule_backed,
172179
change_points: changePoints,
173180
};
174181
});
@@ -185,7 +192,7 @@ export async function readSignificantEventsFromAlertsIndices(
185192
stationary: { p_value: 0, change_point: 0 },
186193
},
187194
},
188-
rule_backed: queryLink.rule_backed ?? LEGACY_RULE_BACKED_FALLBACK,
195+
rule_backed: queryLink.rule_backed,
189196
}));
190197

191198
return {

x-pack/platform/plugins/shared/streams/server/lib/streams/assets/query/query_client.ts

Lines changed: 71 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { isEqual } from 'lodash';
1616
import objectHash from 'object-hash';
1717
import pLimit from 'p-limit';
1818
import {
19-
LEGACY_RULE_BACKED_FALLBACK,
2019
type Query,
2120
type QueryLink,
2221
type QueryLinkRequest,
@@ -45,6 +44,10 @@ import { computeRuleId } from './helpers/query';
4544

4645
type TermQueryFieldValue = string | boolean | number | null;
4746

47+
export interface QueryLinkFilters {
48+
ruleBacked?: boolean;
49+
}
50+
4851
interface TermQueryOpts {
4952
queryEmptyString: boolean;
5053
}
@@ -61,6 +64,30 @@ function termQuery<T extends string>(
6164
return [{ term: { [field]: value } }];
6265
}
6366

67+
function ruleBackedFilter(value: boolean | undefined): QueryDslQueryContainer[] {
68+
if (value === undefined) {
69+
return [];
70+
}
71+
72+
if (!value) {
73+
return termQuery(RULE_BACKED, false);
74+
}
75+
76+
// When filtering for rule-backed queries, also include legacy docs
77+
// that predate the rule_backed field.
78+
return [
79+
{
80+
bool: {
81+
should: [
82+
{ term: { [RULE_BACKED]: true } },
83+
{ bool: { must_not: [{ exists: { field: RULE_BACKED } }] } },
84+
],
85+
minimum_should_match: 1,
86+
},
87+
},
88+
];
89+
}
90+
6491
function termsQuery<T extends string>(
6592
field: T,
6693
values: Array<TermQueryFieldValue | undefined> | null | undefined
@@ -114,7 +141,6 @@ type QueryLinkStorageFields = Omit<QueryLink, 'query' | 'stream_name'> & {
114141
[QUERY_KQL_BODY]: string;
115142
[QUERY_ESQL_QUERY]: string;
116143
[QUERY_SEVERITY_SCORE]?: number;
117-
[RULE_BACKED]?: boolean;
118144
};
119145

120146
export type StoredQueryLink = QueryLinkStorageFields & {
@@ -136,18 +162,16 @@ function fromStorage(link: StoredQueryLink): QueryLink {
136162
[QUERY_FEATURE_FILTER]: string;
137163
[QUERY_FEATURE_TYPE]: 'system';
138164
[QUERY_EVIDENCE]?: string[];
139-
[RULE_BACKED]?: boolean;
140165
} = link as StoredQueryLink & {
141166
[QUERY_FEATURE_NAME]: string;
142167
[QUERY_FEATURE_FILTER]: string;
143168
[QUERY_FEATURE_TYPE]: 'system';
144169
[QUERY_EVIDENCE]?: string[];
145-
[RULE_BACKED]?: boolean;
146170
};
147171
return {
148172
...storageFields,
149173
stream_name: link[STREAM_NAME],
150-
rule_backed: storageFields[RULE_BACKED] ?? LEGACY_RULE_BACKED_FALLBACK,
174+
rule_backed: storageFields[RULE_BACKED],
151175
rule_id: storageFields[RULE_ID],
152176
query: {
153177
id: storageFields[ASSET_ID],
@@ -171,15 +195,9 @@ function fromStorage(link: StoredQueryLink): QueryLink {
171195
} satisfies QueryLink;
172196
}
173197

174-
type QueryLinkRequestWithRuleBacked = QueryLinkRequest & { rule_backed?: boolean };
175-
176-
function toStorage(
177-
definition: Streams.all.Definition,
178-
request: QueryLinkRequestWithRuleBacked
179-
): StoredQueryLink {
198+
function toStorage(definition: Streams.all.Definition, request: QueryLinkRequest): StoredQueryLink {
180199
const link = toQueryLink(definition, request);
181200
const { query, stream_name, ...rest } = link;
182-
const ruleBacked = request.rule_backed ?? LEGACY_RULE_BACKED_FALLBACK;
183201
return {
184202
...rest,
185203
[STREAM_NAME]: definition.name,
@@ -191,7 +209,7 @@ function toStorage(
191209
[QUERY_FEATURE_TYPE]: query.feature ? query.feature.type : '',
192210
[QUERY_SEVERITY_SCORE]: query.severity_score,
193211
[QUERY_EVIDENCE]: query.evidence,
194-
[RULE_BACKED]: ruleBacked,
212+
[RULE_BACKED]: request.rule_backed,
195213
[RULE_ID]: link.rule_id,
196214
} as StoredQueryLink;
197215
}
@@ -203,14 +221,23 @@ function hasBreakingChange(currentQuery: StreamQuery, nextQuery: StreamQuery): b
203221
);
204222
}
205223

206-
function toQueryLinkFromQuery(query: StreamQuery, stream: string): QueryLink {
224+
function toQueryLinkFromQuery({
225+
query,
226+
stream,
227+
ruleBacked = true,
228+
}: {
229+
query: StreamQuery;
230+
stream: string;
231+
ruleBacked?: boolean;
232+
}): QueryLink {
207233
const assetUuid = getQueryLinkUuid(stream, { 'asset.type': 'query', 'asset.id': query.id });
208234
return {
209235
'asset.uuid': assetUuid,
210236
'asset.type': 'query',
211237
'asset.id': query.id,
212238
query,
213239
stream_name: stream,
240+
rule_backed: ruleBacked,
214241
rule_id: computeRuleId(assetUuid, query.esql.query),
215242
};
216243
}
@@ -230,7 +257,7 @@ export class QueryClient {
230257

231258
async syncQueryList(
232259
definition: Streams.all.Definition,
233-
links: QueryLinkRequestWithRuleBacked[]
260+
links: QueryLinkRequest[]
234261
): Promise<{ deleted: QueryLink[]; indexed: QueryLink[] }> {
235262
const name = definition.name;
236263
const assetsResponse = await this.dependencies.storageClient.search({
@@ -313,8 +340,12 @@ export class QueryClient {
313340
* Returns all query links for given streams or
314341
* all query links if no stream names are provided.
315342
*/
316-
async getQueryLinks(streamNames: string[]): Promise<QueryLink[]> {
317-
const filter = [...termsQuery(STREAM_NAME, streamNames), ...termQuery(ASSET_TYPE, 'query')];
343+
async getQueryLinks(streamNames: string[], filters?: QueryLinkFilters): Promise<QueryLink[]> {
344+
const filter = [
345+
...termsQuery(STREAM_NAME, streamNames),
346+
...termQuery(ASSET_TYPE, 'query'),
347+
...ruleBackedFilter(filters?.ruleBacked),
348+
];
318349

319350
const queriesResponse = await this.dependencies.storageClient.search({
320351
size: 10_000,
@@ -334,23 +365,7 @@ export class QueryClient {
334365
* Used internally by promoteQueries.
335366
*/
336367
private async getUnbackedQueries(streamName: string): Promise<QueryLink[]> {
337-
const filter = [
338-
...termQuery(STREAM_NAME, streamName),
339-
...termQuery(ASSET_TYPE, 'query'),
340-
...termQuery(RULE_BACKED, false),
341-
];
342-
343-
const assetsResponse = await this.dependencies.storageClient.search({
344-
size: 10_000,
345-
track_total_hits: false,
346-
query: {
347-
bool: {
348-
filter,
349-
},
350-
},
351-
});
352-
353-
return assetsResponse.hits.hits.map((hit) => fromStorage(hit._source));
368+
return this.getQueryLinks([streamName], { ruleBacked: false });
354369
}
355370

356371
/**
@@ -377,19 +392,7 @@ export class QueryClient {
377392
* Returns all query links across streams that do not have a backing Kibana rule.
378393
*/
379394
async getAllUnbackedQueries(): Promise<QueryLink[]> {
380-
const filter = [...termQuery(ASSET_TYPE, 'query'), ...termQuery(RULE_BACKED, false)];
381-
382-
const assetsResponse = await this.dependencies.storageClient.search({
383-
size: 10_000,
384-
track_total_hits: false,
385-
query: {
386-
bool: {
387-
filter,
388-
},
389-
},
390-
});
391-
392-
return assetsResponse.hits.hits.map((hit) => fromStorage(hit._source));
395+
return this.getQueryLinks([], { ruleBacked: false });
393396
}
394397

395398
async bulkGetByIds(name: string, ids: string[]) {
@@ -413,8 +416,16 @@ export class QueryClient {
413416
return assetsResponse.hits.hits.map((hit) => fromStorage(hit._source));
414417
}
415418

416-
async findQueries(streamNames: string[], query: string): Promise<QueryLink[]> {
417-
const filter = [...termsQuery(STREAM_NAME, streamNames), ...termQuery(ASSET_TYPE, 'query')];
419+
async findQueries(
420+
streamNames: string[],
421+
query: string,
422+
filters?: QueryLinkFilters
423+
): Promise<QueryLink[]> {
424+
const filter = [
425+
...termsQuery(STREAM_NAME, streamNames),
426+
...termQuery(ASSET_TYPE, 'query'),
427+
...ruleBackedFilter(filters?.ruleBacked),
428+
];
418429

419430
const assetsResponse = await this.dependencies.storageClient.search({
420431
size: 10_000,
@@ -446,7 +457,7 @@ export class QueryClient {
446457
if ('index' in operation) {
447458
const document = toStorage(
448459
definition,
449-
Object.values(operation)[0].asset as QueryLinkRequestWithRuleBacked
460+
Object.values(operation)[0].asset as QueryLinkRequest
450461
);
451462
return {
452463
index: {
@@ -482,6 +493,7 @@ export class QueryClient {
482493
query: link.query,
483494
title: link.query.title,
484495
stream_name: link.stream_name,
496+
rule_backed: link.rule_backed,
485497
rule_id: link.rule_id,
486498
};
487499
});
@@ -520,11 +532,11 @@ export class QueryClient {
520532
for (const query of queries) {
521533
const currentLink = currentLinkByQueryId.get(query.id);
522534
if (!currentLink) {
523-
const link = toQueryLinkFromQuery(query, stream);
535+
const link = toQueryLinkFromQuery({ query, stream });
524536
nextQueriesToCreate.push(link);
525537
allNextQueryLinks.push(link);
526538
} else if (hasBreakingChange(currentLink.query, query)) {
527-
const link = toQueryLinkFromQuery(query, stream);
539+
const link = toQueryLinkFromQuery({ query, stream });
528540
nextQueriesUpdatedWithBreakingChange.push(link);
529541
allNextQueryLinks.push(link);
530542
} else {
@@ -638,28 +650,23 @@ export class QueryClient {
638650
...operations
639651
.filter((operation) => operation.index && !currentIds.has(operation.index!.id))
640652
.map((operation) =>
641-
toQueryLinkFromQuery(
642-
{
653+
toQueryLinkFromQuery({
654+
query: {
643655
...operation.index!,
644656
esql: {
645657
query: buildEsqlQuery(indices, operation.index!),
646658
},
647659
},
648-
stream
649-
)
660+
stream,
661+
ruleBacked: options?.createRules !== false,
662+
})
650663
),
651664
];
652665

653666
if (options?.createRules === false) {
654-
const nextQueriesWithRuleBacked = nextQueries.map((link) => ({
655-
...link,
656-
rule_backed: currentIds.has(link.query.id)
657-
? link.rule_backed ?? LEGACY_RULE_BACKED_FALLBACK
658-
: false,
659-
}));
660667
await this.syncQueryList(
661668
definition,
662-
nextQueriesWithRuleBacked.map((link) => ({
669+
nextQueries.map((link) => ({
663670
[ASSET_ID]: link[ASSET_ID],
664671
[ASSET_TYPE]: link[ASSET_TYPE],
665672
query: link.query,
@@ -696,7 +703,7 @@ export class QueryClient {
696703
const idSet = new Set(queryIds);
697704
const toPromote = unbacked
698705
.filter((link) => idSet.has(link.query.id))
699-
.map((link) => toQueryLinkFromQuery(link.query, streamName));
706+
.map((link) => toQueryLinkFromQuery({ query: link.query, stream: streamName }));
700707

701708
if (toPromote.length === 0) {
702709
return { promoted: 0 };

x-pack/platform/plugins/shared/streams/server/lib/streams/assets/query/query_service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
QUERY_FEATURE_NAME,
2020
STREAM_NAME,
2121
RULE_ID,
22+
RULE_BACKED,
2223
ASSET_UUID,
2324
} from '../fields';
2425
import { queryStorageSettings, type QueryStorageSettings } from '../storage_settings';
@@ -95,6 +96,11 @@ export class QueryService {
9596
migrated = { ...migrated, [RULE_ID]: computeRuleId(uuid, kqlQuery) };
9697
}
9798

99+
// Pre-existing queries were all rule-backed; back-fill the flag so it is persisted.
100+
if (!(RULE_BACKED in migrated)) {
101+
migrated = { ...migrated, [RULE_BACKED]: true };
102+
}
103+
98104
return migrated as StoredQueryLink;
99105
},
100106
}

0 commit comments

Comments
 (0)