Skip to content

Commit

Permalink
[APM] Use excluded data tiers setting (#192373)
Browse files Browse the repository at this point in the history
closes [#190559](#190559)

## Summary

This PR updates the ES clients in APM to respect the excluded tier
configuration. When this config is set, the ES clients will
automatically add a filter to exclude the specified tiers from queries.

<img width="600" alt="image"
src="https://github.com/user-attachments/assets/9b0de76d-242c-4343-bc30-d5c787316f59">

All queries in APM should have the `_tier` filter (via
`get_apm_events_client`)
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/c525602f-f239-4be8-99c4-65d617962656">

This change also affects alerting (via `alerting_es_client`)
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/750df4d7-5b49-4de5-9294-7afedf11d7e5">

And it impacts the alerts column (via `get_apm_alert_client`)
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/44bd9129-1e72-4a3a-af32-d42a9cd9164d">

### What won't automatically add a filter for `_tier`

- Embeddables
- ML queries

### How to test
- Set the config in Advanced Settings to exclude `data_frozen` and
`data_cold` (optional)
- Navigate to APM and check the query `Inspect` to see if the filter is
present.
- Click through APM to confirm things still work.
- Create one of each type of APM alerts
- Without the config set, queries should not include the `_tier` filter`

---------

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
crespocarlos and elasticmachine authored Sep 23, 2024
1 parent 886d009 commit ee5ef81
Show file tree
Hide file tree
Showing 25 changed files with 612 additions and 117 deletions.
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,
} 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 };
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

0 comments on commit ee5ef81

Please sign in to comment.