Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,44 +101,142 @@ describe('Index Pattern Fetcher - server', () => {
expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(0);
});

describe('getExistingIndices', () => {
test('getExistingIndices returns the valid matched indices', async () => {
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
indexPatterns.getFieldsForWildcard = jest
.fn()
.mockResolvedValueOnce({ indices: ['length'] })
.mockResolvedValue({ indices: [] });
const result = await indexPatterns.getIndexPatternsWithMatches([
'packetbeat-*',
'filebeat-*',
]);
expect(indexPatterns.getFieldsForWildcard).toBeCalledTimes(2);
expect(result.length).toBe(1);
describe('getIndexPatternMatches', () => {
describe('without negated index patterns', () => {
test('returns the valid matched index patterns', async () => {
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
indexPatterns.getFieldsForWildcard = jest
.fn()
.mockResolvedValueOnce({ indices: ['index1'] })
.mockResolvedValue({ indices: [] });

const result = await indexPatterns.getIndexPatternMatches(['packetbeat-*', 'filebeat-*']);

expect(result.matchedIndexPatterns).toEqual(['packetbeat-*']);
});

test('returns the valid matched indices', async () => {
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
indexPatterns.getFieldsForWildcard = jest
.fn()
.mockResolvedValueOnce({ indices: ['index1'] })
.mockResolvedValue({ indices: [] });

const result = await indexPatterns.getIndexPatternMatches(['packetbeat-*', 'filebeat-*']);

expect(result.matchedIndices).toEqual(['index1']);
});

test('returns the valid matched indices per index pattern', async () => {
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
indexPatterns.getFieldsForWildcard = jest
.fn()
.mockResolvedValueOnce({ indices: ['index1'] })
.mockResolvedValue({ indices: ['index2'] });

const result = await indexPatterns.getIndexPatternMatches(['packetbeat-*', 'filebeat-*']);

expect(result.matchesByIndexPattern).toEqual({
'packetbeat-*': ['index1'],
'filebeat-*': ['index2'],
});
});
});

test('getExistingIndices checks the positive pattern if provided with a negative pattern', async () => {
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
const mockFn = jest.fn().mockResolvedValue({ indices: ['length'] });
indexPatterns.getFieldsForWildcard = mockFn;
const result = await indexPatterns.getIndexPatternsWithMatches(['-filebeat-*', 'filebeat-*']);
expect(mockFn.mock.calls[0][0].pattern).toEqual('filebeat-*');
expect(mockFn.mock.calls[1][0].pattern).toEqual('filebeat-*');
expect(result).toEqual(['-filebeat-*', 'filebeat-*']);
describe('with negated index patterns', () => {
test('returns the valid matched index patterns', async () => {
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
const mockFn = jest.fn().mockResolvedValue({ indices: ['index1'] });
indexPatterns.getFieldsForWildcard = mockFn;

const result = await indexPatterns.getIndexPatternMatches([
'-filebeat-*',
'filebeat-*',
'logs-*',
'-logs-excluded-*',
]);

expect(result.matchedIndexPatterns).toEqual(['filebeat-*', 'logs-*']);
});

test('returns the valid matched indices', async () => {
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
const mockFn = jest.fn().mockResolvedValue({ indices: ['index1'] });
indexPatterns.getFieldsForWildcard = mockFn;

const result = await indexPatterns.getIndexPatternMatches([
'-filebeat-*',
'filebeat-*',
'logs-*',
'-logs-excluded-*',
]);

expect(result.matchedIndices).toEqual(['index1']);
});

test('returns the valid matched indices per index pattern', async () => {
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
const mockFn = jest
.fn()
.mockResolvedValueOnce({ indices: ['index1'] })
.mockResolvedValue({ indices: ['index2'] });
indexPatterns.getFieldsForWildcard = mockFn;

const result = await indexPatterns.getIndexPatternMatches([
'-filebeat-*',
'filebeat-*',
'logs-*',
'-logs-excluded-*',
]);

expect(result.matchesByIndexPattern).toEqual({
'filebeat-*': ['index1'],
'logs-*': ['index2'],
});
});

test('queries each positive pattern with all negated patterns for field caps', async () => {
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
const mockFn = jest.fn().mockResolvedValue({ indices: ['length'] });
indexPatterns.getFieldsForWildcard = mockFn;

await indexPatterns.getIndexPatternMatches([
'-filebeat-*',
'filebeat-*',
'logs-*',
'-logs-excluded-*',
]);

expect(mockFn.mock.calls[0][0].pattern).toEqual([
'filebeat-*',
'-filebeat-*',
'-logs-excluded-*',
]);
expect(mockFn.mock.calls[1][0].pattern).toEqual([
'logs-*',
'-filebeat-*',
'-logs-excluded-*',
]);
});
});

test('getExistingIndices handles an error', async () => {
test('handles an error', async () => {
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
indexPatterns.getFieldsForWildcard = jest
.fn()
.mockImplementationOnce(async () => {
throw new DataViewMissingIndices('Catch me if you can!');
})
.mockImplementation(() => Promise.resolve({ indices: ['length'] }));
const result = await indexPatterns.getIndexPatternsWithMatches([
'packetbeat-*',
'filebeat-*',
]);
expect(result).toEqual(['filebeat-*']);
.mockRejectedValueOnce(new DataViewMissingIndices('Catch me if you can!'))
.mockResolvedValue({ indices: ['index1'] });

const result = await indexPatterns.getIndexPatternMatches(['packetbeat-*', 'filebeat-*']);

expect(result).toMatchObject({
matchedIndexPatterns: ['filebeat-*'],
matchedIndices: ['index1'],
matchesByIndexPattern: {
'packetbeat-*': [],
'filebeat-*': ['index1'],
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import type { estypes } from '@elastic/elasticsearch';
import type { ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server';
import { keyBy } from 'lodash';
import { defer, from } from 'rxjs';
import { catchError, defer, from, map, of } from 'rxjs';
import { rateLimitingForkJoin } from '../../common/data_views/utils';
import type { QueryDslQueryContainer } from '../../common/types';

Expand Down Expand Up @@ -48,6 +48,12 @@ interface IndexPatternsFetcherOptionalParams {
rollupsEnabled?: boolean;
}

export interface GetIndexPatternMatchesResult {
matchedIndexPatterns: string[];
matchedIndices?: string[];
matchesByIndexPattern?: Record<string, string[]>;
}

export class IndexPatternsFetcher {
private readonly uiSettingsClient?: IUiSettingsClient;
private readonly allowNoIndices: boolean;
Expand Down Expand Up @@ -159,37 +165,84 @@ export class IndexPatternsFetcher {
}

/**
* Get existing index pattern list by providing string array index pattern list.
* @param indexPatterns - index pattern list
* @returns index pattern list of index patterns that match indices
* Checks whether the passed index pattern is an excluding one.
* The excluding index pattern starts with a dash, e.g. "-logs-excluded-*"
* meaning all indices matching "logs-excluded-*" will be excluded from search
*
* @param indexPattern - Index pattern to check
* @returns Whether the passed index pattern is a negated one
*/
async getIndexPatternsWithMatches(indexPatterns: string[]): Promise<string[]> {
const indexPatternsObs = indexPatterns.map((indexPattern) => {
// when checking a negative pattern, check if the positive pattern exists
const indexToQuery = indexPattern.trim().startsWith('-')
? indexPattern.trim().substring(1)
: indexPattern.trim();
isExcludingIndexPattern(indexPattern: string): boolean {
return indexPattern.trim().startsWith('-');
}

/**
* For each input pattern, checks whether it resolves to at least one backing index.
*
* Including index patterns (not starting with `-`) are checked with field caps using that pattern
* together with every excluding index pattern (starting with `-`) in the list, so resolution matches
* Elasticsearch multi-target syntax.
*
* @param indexPatterns - Index patterns to check (may include wildcards and excluded entries).
* @returns Resolves to {@link GetIndexPatternMatchesResult}:
* - `matchedIndexPatterns`: input patterns that matched at least one index.
* - `matchedIndices`: deduplicated concrete index names matching index patterns (omitted on failure).
* - `matchesByIndexPattern`: per-input-pattern matched indices (omitted on failure).
*/
async getIndexPatternMatches(indexPatterns: string[]): Promise<GetIndexPatternMatchesResult> {
Comment thread
maximpn marked this conversation as resolved.
const excludingIndexPatterns = indexPatterns.filter(this.isExcludingIndexPattern);
const indexPatternsToMatch = indexPatterns
.filter((indexPattern) => !this.isExcludingIndexPattern(indexPattern))
.map((indexPattern) => [indexPattern, ...excludingIndexPatterns]);

const matchIndexPatterns = indexPatternsToMatch.map((pattern) => {
return defer(() =>
from(
this.getFieldsForWildcard({
// check one field to keep request fast/small
fields: ['_id'],
pattern: indexToQuery,
pattern,
})
).pipe(
// expecting pattern[0] to contain an including index pattern
// and pattern[1..end] to contain excluding index patterns
map((match) => ({ ...match, indexPattern: pattern[0] })),
catchError(() => of({ fields: [], indices: [], indexPattern: pattern[0] }))
)
);
});

return new Promise<boolean[]>((resolve) => {
rateLimitingForkJoin(indexPatternsObs, 3, { fields: [], indices: [] }).subscribe((value) => {
resolve(value.map((v) => v.indices.length > 0));
return new Promise<GetIndexPatternMatchesResult>((resolve) => {
rateLimitingForkJoin(matchIndexPatterns, 3, {
fields: [],
indices: [],
indexPattern: '',
}).subscribe((indexPatternMatches) => {
const matchedIndexPatterns: string[] = [];
const uniqueMatchedIndices = new Set<string>();
const matchesByIndexPattern: Record<string, string[]> = {};

for (const indexPatternMatch of indexPatternMatches) {
const { indexPattern, indices } = indexPatternMatch;

matchesByIndexPattern[indexPattern] = indices;

if (indices.length === 0) {
continue;
}

matchedIndexPatterns.push(indexPattern);

for (const index of indices) {
uniqueMatchedIndices.add(index);
}
}

resolve({
matchedIndexPatterns,
matchedIndices: Array.from(uniqueMatchedIndices),
matchesByIndexPattern,
});
});
})
.then((allPatterns: boolean[]) =>
indexPatterns.filter(
(indexPattern, i, self) => self.indexOf(indexPattern) === i && allPatterns[i]
)
)
.catch(() => indexPatterns);
}).catch(() => ({ matchedIndexPatterns: [] }));
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export const handler: RequestHandler<{}, { indices: string | string[] }, string[
const elasticsearchClient = core.elasticsearch.client.asCurrentUser;
const indexPatterns = new IndexPatternsFetcher(elasticsearchClient);

const response: string[] = await indexPatterns.getIndexPatternsWithMatches(indexArray);
return res.ok({ body: response });
const { matchedIndexPatterns } = await indexPatterns.getIndexPatternMatches(indexArray);
return res.ok({ body: matchedIndexPatterns });
} catch (error) {
return res.badRequest();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,7 @@ export function updateEvent(event: IEvent, opts: UpdateEventOpts) {
if (consumerMetrics) {
set(event, 'kibana.alert.rule.execution.metrics', {
...event.kibana?.alert?.rule?.execution?.metrics,
matched_indices_count: consumerMetrics.matched_indices_count,
alerts_candidate_count: consumerMetrics.alerts_candidate_count,
alerts_suppressed_count: consumerMetrics.alerts_suppressed_count,
frozen_indices_queried_count: consumerMetrics.frozen_indices_queried_count,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@ describe('Ad Hoc Task Runner', () => {

test('passes consumer metrics to AlertingEventLogger', async () => {
const consumerMetrics = {
matched_indices_count: 3,
alerts_candidate_count: 100,
total_enrichment_duration_ms: 50,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,10 @@ describe('Task Runner', () => {

test('passes consumer metrics to AlertingEventLogger', async () => {
const consumerMetrics = {
matched_indices_count: 3,
frozen_indices_queried_count: 3,
alerts_candidate_count: 42,
alerts_suppressed_count: 7,
frozen_indices_queried_count: 3,
};
ruleType.executor.mockImplementation(async ({ services: { ruleMonitoringService } }) => {
ruleMonitoringService?.setMetrics(consumerMetrics);
Expand Down
1 change: 1 addition & 0 deletions x-pack/platform/plugins/shared/alerting/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ export interface ConsumerExecutionMetrics {
total_enrichment_duration_ms: number;
gap_duration_s: number;
gap_range: { lte: string; gte: string };
matched_indices_count: number;
alerts_candidate_count: number;
alerts_suppressed_count: number;
frozen_indices_queried_count: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,9 @@
}
}
},
"matched_indices_count": {
"type": "long"
},
"frozen_indices_queried_count": {
"type": "long"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export const EventSchema = schema.maybe(
type: ecsString(),
})
),
matched_indices_count: ecsStringOrNumber(),
frozen_indices_queried_count: ecsStringOrNumber(),
rule_type_run_duration_ms: ecsStringOrNumber(),
process_alerts_duration_ms: ecsStringOrNumber(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ exports.EcsCustomPropertyMappings = {
},
},
},
matched_indices_count: {
type: 'long',
},
frozen_indices_queried_count: {
type: 'long',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -589,9 +589,9 @@ describe('AIAssistantKnowledgeBaseDataClient', () => {

describe('getAssistantTools', () => {
it('should return structured tools for relevant index entries', async () => {
IndexPatternsFetcher.prototype.getIndexPatternsWithMatches = jest
IndexPatternsFetcher.prototype.getIndexPatternMatches = jest
.fn()
.mockResolvedValue(['test']);
.mockResolvedValue({ matchedIndexPatterns: ['test'] });
esClientMock.search.mockReturnValue(
// @ts-expect-error not full response interface
getKnowledgeBaseEntrySearchEsMock('index')
Expand Down
Loading
Loading