Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM] Use excluded data tiers setting #192373

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,8 @@
* 2.0.
*/
import type { estypes } from '@elastic/elasticsearch';
import { excludeTiersQuery } from './exclude_tiers_query';

export function excludeFrozenQuery(): estypes.QueryDslQueryContainer[] {
return [
{
bool: {
must_not: [
{
term: {
_tier: 'data_frozen',
},
},
],
},
},
];
return excludeTiersQuery(['data_frozen']);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { estypes } from '@elastic/elasticsearch';

export function excludeTiersQuery(
excludedDataTiers: Array<'data_frozen' | 'data_cold' | 'data_warm' | 'data_hot'>
): estypes.QueryDslQueryContainer[] {
return [
{
bool: {
must_not: [
{
terms: {
_tier: excludedDataTiers,
},
},
],
},
},
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,13 @@
* 2.0.
*/

import {
IndexLifecyclePhaseSelectOption,
indexLifeCyclePhaseToDataTier,
miloszmarcinkowski marked this conversation as resolved.
Show resolved Hide resolved
} from '@kbn/observability-shared-plugin/common';
import * as t from 'io-ts';

export enum IndexLifecyclePhaseSelectOption {
All = 'all',
Hot = 'hot',
Warm = 'warm',
Cold = 'cold',
Frozen = 'frozen',
}

export const indexLifeCyclePhaseToDataTier = {
[IndexLifecyclePhaseSelectOption.Hot]: 'data_hot',
[IndexLifecyclePhaseSelectOption.Warm]: 'data_warm',
[IndexLifecyclePhaseSelectOption.Cold]: 'data_cold',
[IndexLifecyclePhaseSelectOption.Frozen]: 'data_frozen',
};

export { IndexLifecyclePhaseSelectOption, indexLifeCyclePhaseToDataTier };
miloszmarcinkowski marked this conversation as resolved.
Show resolved Hide resolved
export const indexLifecyclePhaseRt = t.type({
indexLifecyclePhase: t.union([
t.literal(IndexLifecyclePhaseSelectOption.All),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { type ApmAlertsRequiredParams, getApmAlertsClient } from './get_apm_alerts_client';
import type {
IScopedClusterClient,
IUiSettingsClient,
KibanaRequest,
SavedObjectsClientContract,
} from '@kbn/core/server';
import { AlertsClient, RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';

describe('get_apm_alerts_client', () => {
let ruleRegistryMock: jest.Mocked<RuleRegistryPluginStartContract>;
let alertClient: jest.Mocked<AlertsClient>;
let uiSettingsClientMock: jest.Mocked<IUiSettingsClient>;

const params: ApmAlertsRequiredParams = {
size: 10,
track_total_hits: true,
query: {
match: { field: 'value' },
},
};

beforeEach(async () => {
uiSettingsClientMock = {
get: jest.fn().mockResolvedValue(undefined),
} as unknown as jest.Mocked<IUiSettingsClient>;

alertClient = {
find: jest.fn().mockResolvedValue({}),
getAuthorizedAlertsIndices: jest.fn().mockResolvedValue(['apm']),
} as unknown as jest.Mocked<AlertsClient>;

ruleRegistryMock = {
getRacClientWithRequest: jest.fn().mockResolvedValue(alertClient),
alerting: jest.fn(),
} as unknown as jest.Mocked<RuleRegistryPluginStartContract>;
});

afterEach(() => {
jest.resetAllMocks();
});

// Helper function to create the APM alerts client
const createApmAlertsClient = async () => {
return await getApmAlertsClient({
context: {
core: Promise.resolve({
uiSettings: { client: uiSettingsClientMock },
elasticsearch: { client: {} as IScopedClusterClient },
savedObjects: { client: {} as SavedObjectsClientContract },
}),
} as any,
plugins: {
ruleRegistry: {
start: jest.fn().mockResolvedValue(ruleRegistryMock),
setup: {} as any,
},
} as any,
request: {} as KibanaRequest,
});
};

it('should call search', async () => {
const apmAlertsClient = await createApmAlertsClient();

await apmAlertsClient.search(params);

const searchParams = alertClient.find.mock.calls[0][0] as ApmAlertsRequiredParams;
expect(searchParams.query).toEqual({ match: { field: 'value' } });
});

it('should call search with filters containing excluded data tiers', async () => {
const excludedDataTiers = ['data_warm', 'data_cold'];
uiSettingsClientMock.get.mockResolvedValue(excludedDataTiers);

const apmAlertsClient = await createApmAlertsClient();

await apmAlertsClient.search(params);

const searchParams = alertClient.find.mock.calls[0][0] as ApmAlertsRequiredParams;
expect(searchParams.query?.bool).toEqual({
must: [
{ match: { field: 'value' } },
{ bool: { must_not: [{ terms: { _tier: ['data_warm', 'data_cold'] } }] } },
],
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,27 @@
import { isEmpty } from 'lodash';
import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import { DataTier } from '@kbn/observability-shared-plugin/common';
import { searchExcludedDataTiers } from '@kbn/observability-plugin/common/ui_settings_keys';
import { estypes } from '@elastic/elasticsearch';
import { getDataTierFilterCombined } from '@kbn/apm-data-access-plugin/server/utils';
import type { MinimalAPMRouteHandlerResources } from '../../routes/apm_routes/register_apm_server_routes';

export type ApmAlertsClient = Awaited<ReturnType<typeof getApmAlertsClient>>;

export type ApmAlertsRequiredParams = ESSearchRequest & {
size: number;
track_total_hits: boolean | number;
query?: estypes.QueryDslQueryContainer;
};

export async function getApmAlertsClient({
context,
plugins,
request,
}: Pick<MinimalAPMRouteHandlerResources, 'plugins' | 'request'>) {
}: Pick<MinimalAPMRouteHandlerResources, 'context' | 'plugins' | 'request'>) {
const coreContext = await context.core;

const ruleRegistryPluginStart = await plugins.ruleRegistry.start();
const alertsClient = await ruleRegistryPluginStart.getRacClientWithRequest(request);
const apmAlertsIndices = await alertsClient.getAuthorizedAlertsIndices(['apm']);
Expand All @@ -24,17 +37,20 @@ export async function getApmAlertsClient({
throw Error('No alert indices exist for "apm"');
}

type RequiredParams = ESSearchRequest & {
size: number;
track_total_hits: boolean | number;
};
const excludedDataTiers = await coreContext.uiSettings.client.get<DataTier[]>(
searchExcludedDataTiers
);

return {
search<TParams extends RequiredParams>(
search<TParams extends ApmAlertsRequiredParams>(
searchParams: TParams
): Promise<InferSearchResponseOf<ParsedTechnicalFields, TParams>> {
return alertsClient.find({
...searchParams,
query: getDataTierFilterCombined({
filter: searchParams.query,
excludedDataTiers,
}),
index: apmAlertsIndices.join(','),
}) as Promise<any>;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*/

import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { DataTier } from '@kbn/observability-shared-plugin/common';
import { searchExcludedDataTiers } from '@kbn/observability-plugin/common/ui_settings_keys';
import { APMEventClient } from './create_es_client/create_apm_event_client';
import { withApmSpan } from '../../utils/with_apm_span';
import { MinimalAPMRouteHandlerResources } from '../../routes/apm_routes/register_apm_server_routes';
Expand All @@ -22,11 +24,18 @@ export async function getApmEventClient({
>): Promise<APMEventClient> {
return withApmSpan('get_apm_event_client', async () => {
const coreContext = await context.core;
const [indices, includeFrozen] = await Promise.all([
const [indices, uiSettings] = await Promise.all([
getApmIndices(),
withApmSpan('get_ui_settings', () =>
coreContext.uiSettings.client.get<boolean>(UI_SETTINGS.SEARCH_INCLUDE_FROZEN)
),
withApmSpan('get_ui_settings', async () => {
const includeFrozen = await coreContext.uiSettings.client.get<boolean>(
UI_SETTINGS.SEARCH_INCLUDE_FROZEN
);
const excludedDataTiers = await coreContext.uiSettings.client.get<DataTier[]>(
searchExcludedDataTiers
);

return { includeFrozen, excludedDataTiers };
}),
]);

return new APMEventClient({
Expand All @@ -35,7 +44,8 @@ export async function getApmEventClient({
request,
indices,
options: {
includeFrozen,
includeFrozen: uiSettings.includeFrozen,
excludedDataTiers: uiSettings.excludedDataTiers,
inspectableEsQueriesMap,
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { type APMEventESSearchRequestParams, alertingEsClient } from './alerting_es_client';
import type { RuleExecutorServices } from '@kbn/alerting-plugin/server';
import type { ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server';
import type { ESSearchResponse } from '@kbn/es-types';

describe('alertingEsClient', () => {
let scopedClusterClientMock: jest.Mocked<{
asCurrentUser: jest.Mocked<ElasticsearchClient>;
}>;

let uiSettingsClientMock: jest.Mocked<IUiSettingsClient>;

const params = {
body: {
size: 10,
track_total_hits: true,
query: {
match: { field: 'value' },
},
},
};

const mockSearchResponse = {
hits: {
total: { value: 1, relation: 'eq' },
hits: [{ _source: {}, _index: '' }],
max_score: 1,
},
took: 1,
_shards: { total: 1, successful: 1, skipped: 0, failed: 0 },
timed_out: false,
} as unknown as ESSearchResponse<unknown, typeof params>;

beforeEach(() => {
scopedClusterClientMock = {
asCurrentUser: {
search: jest.fn().mockResolvedValue(mockSearchResponse),
} as unknown as jest.Mocked<ElasticsearchClient>,
};

uiSettingsClientMock = {
get: jest.fn().mockResolvedValue(undefined),
} as unknown as jest.Mocked<IUiSettingsClient>;
});

afterEach(() => {
jest.resetAllMocks();
});

// Helper function to perform the search
const performSearch = async (searchParams: APMEventESSearchRequestParams) => {
return await alertingEsClient({
scopedClusterClient: scopedClusterClientMock as unknown as RuleExecutorServices<
never,
never,
never
>['scopedClusterClient'],
uiSettingsClient: uiSettingsClientMock,
params: searchParams,
});
};

it('should call search with default params', async () => {
await performSearch(params);

const searchParams = scopedClusterClientMock.asCurrentUser.search.mock
.calls[0][0] as APMEventESSearchRequestParams;
expect(searchParams.body?.query).toEqual({ match: { field: 'value' } });
});

it('should call search with filters containing excluded data tiers', async () => {
const excludedDataTiers = ['data_warm', 'data_cold'];
uiSettingsClientMock.get.mockResolvedValue(excludedDataTiers);

await performSearch(params);

const searchParams = scopedClusterClientMock.asCurrentUser.search.mock
.calls[0][0] as APMEventESSearchRequestParams;
expect(searchParams.body?.query?.bool).toEqual({
must: [
{ match: { field: 'value' } },
{ bool: { must_not: [{ terms: { _tier: ['data_warm', 'data_cold'] } }] } },
],
});
});
});
Loading