From 1def8b58f9946d5c1002b98c185da03778cdf58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 19 Oct 2022 20:38:06 +0200 Subject: [PATCH] =?UTF-8?q?[APM]=20Fallback=20to=20terms=20agg=20search=20?= =?UTF-8?q?if=20terms=20enum=20doesn=E2=80=99t=20return=20results=20(#1436?= =?UTF-8?q?19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [APM] Fallback to terms agg search if terms enum doesn’t return results * Add api test for suggestions --- .../get_suggestions_with_terms_aggregation.ts | 2 +- ....ts => get_suggestions_with_terms_enum.ts} | 47 +-- .../apm/server/routes/suggestions/route.ts | 51 +-- .../tests/suggestions/generate_data.ts | 77 ++++ .../tests/suggestions/suggestions.spec.ts | 361 ++++++++++++------ 5 files changed, 386 insertions(+), 152 deletions(-) rename x-pack/plugins/apm/server/routes/suggestions/{get_suggestions.ts => get_suggestions_with_terms_enum.ts} (56%) create mode 100644 x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts diff --git a/x-pack/plugins/apm/server/routes/suggestions/get_suggestions_with_terms_aggregation.ts b/x-pack/plugins/apm/server/routes/suggestions/get_suggestions_with_terms_aggregation.ts index 77a7528fbb1a3..56ed34805c2fb 100644 --- a/x-pack/plugins/apm/server/routes/suggestions/get_suggestions_with_terms_aggregation.ts +++ b/x-pack/plugins/apm/server/routes/suggestions/get_suggestions_with_terms_aggregation.ts @@ -23,7 +23,7 @@ export async function getSuggestionsWithTermsAggregation({ fieldName: string; fieldValue: string; searchAggregatedTransactions: boolean; - serviceName: string; + serviceName?: string; setup: Setup; size: number; start: number; diff --git a/x-pack/plugins/apm/server/routes/suggestions/get_suggestions.ts b/x-pack/plugins/apm/server/routes/suggestions/get_suggestions_with_terms_enum.ts similarity index 56% rename from x-pack/plugins/apm/server/routes/suggestions/get_suggestions.ts rename to x-pack/plugins/apm/server/routes/suggestions/get_suggestions_with_terms_enum.ts index dcab43ca26abc..4437a36151895 100644 --- a/x-pack/plugins/apm/server/routes/suggestions/get_suggestions.ts +++ b/x-pack/plugins/apm/server/routes/suggestions/get_suggestions_with_terms_enum.ts @@ -8,7 +8,7 @@ import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { getProcessorEventForTransactions } from '../../lib/helpers/transactions'; import { Setup } from '../../lib/helpers/setup_request'; -export async function getSuggestions({ +export async function getSuggestionsWithTermsEnum({ fieldName, fieldValue, searchAggregatedTransactions, @@ -27,30 +27,33 @@ export async function getSuggestions({ }) { const { apmEventClient } = setup; - const response = await apmEventClient.termsEnum('get_suggestions', { - apm: { - events: [ - getProcessorEventForTransactions(searchAggregatedTransactions), - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - body: { - case_insensitive: true, - field: fieldName, - size, - string: fieldValue, - index_filter: { - range: { - ['@timestamp']: { - gte: start, - lte: end, - format: 'epoch_millis', + const response = await apmEventClient.termsEnum( + 'get_suggestions_with_terms_enum', + { + apm: { + events: [ + getProcessorEventForTransactions(searchAggregatedTransactions), + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + body: { + case_insensitive: true, + field: fieldName, + size, + string: fieldValue, + index_filter: { + range: { + ['@timestamp']: { + gte: start, + lte: end, + format: 'epoch_millis', + }, }, }, }, - }, - }); + } + ); return { terms: response.terms }; } diff --git a/x-pack/plugins/apm/server/routes/suggestions/route.ts b/x-pack/plugins/apm/server/routes/suggestions/route.ts index 92a64da42eca3..f0396ac62ca51 100644 --- a/x-pack/plugins/apm/server/routes/suggestions/route.ts +++ b/x-pack/plugins/apm/server/routes/suggestions/route.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { maxSuggestions } from '@kbn/observability-plugin/common'; -import { getSuggestions } from './get_suggestions'; +import { getSuggestionsWithTermsEnum } from './get_suggestions_with_terms_enum'; import { getSuggestionsWithTermsAggregation } from './get_suggestions_with_terms_aggregation'; import { getSearchTransactionsEvents } from '../../lib/helpers/transactions'; import { setupRequest } from '../../lib/helpers/setup_request'; @@ -41,28 +41,35 @@ const suggestionsRoute = createApmServerRoute({ maxSuggestions ); - const suggestions = serviceName - ? await getSuggestionsWithTermsAggregation({ - fieldName, - fieldValue, - searchAggregatedTransactions, - serviceName, - setup, - size, - start, - end, - }) - : await getSuggestions({ - fieldName, - fieldValue, - searchAggregatedTransactions, - setup, - size, - start, - end, - }); + if (!serviceName) { + const suggestions = await getSuggestionsWithTermsEnum({ + fieldName, + fieldValue, + searchAggregatedTransactions, + setup, + size, + start, + end, + }); - return suggestions; + // if no terms are found using terms enum it will fall back to using ordinary terms agg search + // This is useful because terms enum can only find terms that start with the search query + // whereas terms agg approach can find terms that contain the search query + if (suggestions.terms.length > 0) { + return suggestions; + } + } + + return getSuggestionsWithTermsAggregation({ + fieldName, + fieldValue, + searchAggregatedTransactions, + serviceName, + setup, + size, + start, + end, + }); }, }); diff --git a/x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts b/x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts new file mode 100644 index 0000000000000..13d6359e0a733 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts @@ -0,0 +1,77 @@ +/* + * 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 { apm, timerange } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { times } from 'lodash'; + +export async function generateData({ + synthtraceEsClient, + start, + end, +}: { + synthtraceEsClient: ApmSynthtraceEsClient; + start: number; + end: number; +}) { + const services = times(5).flatMap((serviceId) => { + return ['go', 'java'].flatMap((agentName) => { + return ['production', 'development', 'staging'].flatMap((environment) => { + return times(5).flatMap((envId) => { + const service = apm + .service({ + name: `${agentName}-${serviceId}`, + environment: `${environment}-${envId}`, + agentName, + }) + .instance('instance-a'); + + return service; + }); + }); + }); + }); + + const transactionNames = [ + 'GET /api/product/:id', + 'PUT /api/product/:id', + 'GET /api/user/:id', + 'PUT /api/user/:id', + ]; + + const phpService = apm + .service({ + name: `custom-php-service`, + environment: `custom-php-environment`, + agentName: 'php', + }) + .instance('instance-a'); + + const docs = timerange(start, end) + .ratePerMinute(1) + .generator((timestamp) => { + const autoGeneratedDocs = services.flatMap((service) => { + return transactionNames.flatMap((transactionName) => { + return service + .transaction({ transactionName, transactionType: 'my-custom-type' }) + .timestamp(timestamp) + .duration(1000); + }); + }); + + const customDoc = phpService + .transaction({ + transactionName: 'GET /api/php/memory', + transactionType: 'custom-php-type', + }) + .timestamp(timestamp) + .duration(1000); + + return [...autoGeneratedDocs, customDoc]; + }); + + return await synthtraceEsClient.index(docs); +} diff --git a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts b/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts index 692cd1c0cf7f1..db15db23776c7 100644 --- a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts +++ b/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts @@ -7,139 +7,286 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, + TRANSACTION_NAME, TRANSACTION_TYPE, } from '@kbn/apm-plugin/common/elasticsearch_fieldnames'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { generateData } from './generate_data'; + +const startNumber = new Date('2021-01-01T00:00:00.000Z').getTime(); +const endNumber = new Date('2021-01-01T00:05:00.000Z').getTime() - 1; + +const start = new Date(startNumber).toISOString(); +const end = new Date(endNumber).toISOString(); export default function suggestionsTests({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const archiveName = 'apm_8.0.0'; - const { start, end } = archives_metadata[archiveName]; - - registry.when( - 'suggestions when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - describe('with environment', () => { - describe('with an empty string parameter', () => { - it('returns all environments', async () => { - const { body } = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/suggestions', - params: { query: { fieldName: SERVICE_ENVIRONMENT, fieldValue: '', start, end } }, - }); - - expectSnapshot(body).toMatchInline(` - Object { - "terms": Array [ - "production", - "testing", - ], - } - `); + const synthtraceEsClient = getService('synthtraceEsClient'); + + registry.when('suggestions when data is loaded', { config: 'basic', archives: [] }, async () => { + before(async () => { + await generateData({ + synthtraceEsClient, + start: startNumber, + end: endNumber, + }); + }); + + after(() => synthtraceEsClient.clean()); + + describe(`field: ${SERVICE_ENVIRONMENT}`, () => { + describe('when fieldValue is empty', () => { + it('returns all environments', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { + query: { fieldName: SERVICE_ENVIRONMENT, fieldValue: '', start, end }, + }, + }); + + expectSnapshot(body.terms).toMatchInline(` + Array [ + "custom-php-environment", + "development-0", + "development-1", + "development-2", + "development-3", + "development-4", + "production-0", + "production-1", + "production-2", + "production-3", + "production-4", + "staging-0", + "staging-1", + "staging-2", + "staging-3", + "staging-4", + ] + `); + }); + }); + + describe('when fieldValue is not empty', () => { + it('returns environments that start with the fieldValue', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { fieldName: SERVICE_ENVIRONMENT, fieldValue: 'prod', start, end } }, + }); + + expectSnapshot(body.terms).toMatchInline(` + Array [ + "production-0", + "production-1", + "production-2", + "production-3", + "production-4", + ] + `); + }); + + it('returns environments that contain the fieldValue', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { + query: { fieldName: SERVICE_ENVIRONMENT, fieldValue: 'evelopment', start, end }, + }, + }); + + expectSnapshot(body.terms).toMatchInline(` + Array [ + "development-0", + "development-1", + "development-2", + "development-3", + "development-4", + ] + `); + }); + + it('returns no results if nothing matches', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { + query: { fieldName: SERVICE_ENVIRONMENT, fieldValue: 'foobar', start, end }, + }, + }); + + expect(body.terms).to.eql([]); + }); + }); + }); + + describe(`field: ${SERVICE_NAME}`, () => { + describe('when fieldValue is empty', () => { + it('returns all service names', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { fieldName: SERVICE_NAME, fieldValue: '', start, end } }, + }); + + expectSnapshot(body.terms).toMatchInline(` + Array [ + "custom-php-service", + "go-0", + "go-1", + "go-2", + "go-3", + "go-4", + "java-0", + "java-1", + "java-2", + "java-3", + "java-4", + ] + `); + }); + }); + + describe('when fieldValue is not empty', () => { + it('returns services that start with the fieldValue', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { fieldName: SERVICE_NAME, fieldValue: 'java', start, end } }, + }); + + expectSnapshot(body.terms).toMatchInline(` + Array [ + "java-0", + "java-1", + "java-2", + "java-3", + "java-4", + ] + `); + }); + + it('returns services that contains the fieldValue', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { fieldName: SERVICE_NAME, fieldValue: '1', start, end } }, }); + + expectSnapshot(body.terms).toMatchInline(` + Array [ + "go-1", + "java-1", + ] + `); }); + }); + }); + + describe(`field: ${TRANSACTION_TYPE}`, () => { + describe('when fieldValue is empty', () => { + it('returns all transaction types', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { fieldName: TRANSACTION_TYPE, fieldValue: '', start, end } }, + }); - describe('with a string parameter', () => { - it('returns items matching the string parameter', async () => { - const { body } = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/suggestions', - params: { query: { fieldName: SERVICE_ENVIRONMENT, fieldValue: 'pr', start, end } }, - }); - - expectSnapshot(body).toMatchInline(` - Object { - "terms": Array [ - "production", - ], - } + expectSnapshot(body.terms).toMatchInline(` + Array [ + "custom-php-type", + "my-custom-type", + ] `); + }); + }); + + describe('with a string parameter', () => { + it('returns items matching the string parameter', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { fieldName: TRANSACTION_TYPE, fieldValue: 'custom', start, end } }, }); + + expectSnapshot(body.terms).toMatchInline(` + Array [ + "custom-php-type", + ] + `); }); }); + }); - describe('with service name', () => { - describe('with an empty string parameter', () => { - it('returns all services', async () => { - const { body } = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/suggestions', - params: { query: { fieldName: SERVICE_NAME, fieldValue: '', start, end } }, - }); - - expectSnapshot(body).toMatchInline(` - Object { - "terms": Array [ - "auditbeat", - "opbeans-dotnet", - "opbeans-go", - "opbeans-java", - "opbeans-node", - "opbeans-python", - "opbeans-ruby", - "opbeans-rum", - ], - } - `); + describe(`field: ${TRANSACTION_NAME}`, () => { + describe('when fieldValue is empty', () => { + it('returns all transaction names', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { fieldName: TRANSACTION_NAME, fieldValue: '', start, end } }, }); + + expectSnapshot(body.terms).toMatchInline(` + Array [ + "GET /api/php/memory", + "GET /api/product/:id", + "GET /api/user/:id", + "PUT /api/product/:id", + "PUT /api/user/:id", + ] + `); }); + }); - describe('with a string parameter', () => { - it('returns items matching the string parameter', async () => { - const { body } = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/suggestions', - params: { query: { fieldName: SERVICE_NAME, fieldValue: 'aud', start, end } }, - }); - - expectSnapshot(body).toMatchInline(` - Object { - "terms": Array [ - "auditbeat", - ], - } - `); + describe('with a string parameter', () => { + it('returns items matching the string parameter', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { fieldName: TRANSACTION_NAME, fieldValue: 'product', start, end } }, }); + + expectSnapshot(body.terms).toMatchInline(` + Array [ + "GET /api/product/:id", + "PUT /api/product/:id", + ] + `); }); }); - describe('with transaction type', () => { - describe('with an empty string parameter', () => { - it('returns all transaction types', async () => { - const { body } = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/suggestions', - params: { query: { fieldName: TRANSACTION_TYPE, fieldValue: '', start, end } }, - }); - - expectSnapshot(body).toMatchInline(` - Object { - "terms": Array [ - "Worker", - "celery", - "page-load", - "request", - ], - } - `); + describe('when limiting the suggestions to a specific service', () => { + it('returns items matching the string parameter', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { + query: { + serviceName: 'custom-php-service', + fieldName: TRANSACTION_NAME, + fieldValue: '', + start, + end, + }, + }, }); + + expectSnapshot(body.terms).toMatchInline(` + Array [ + "GET /api/php/memory", + ] + `); }); - describe('with a string parameter', () => { - it('returns items matching the string parameter', async () => { - const { body } = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/suggestions', - params: { query: { fieldName: TRANSACTION_TYPE, fieldValue: 'w', start, end } }, - }); - - expectSnapshot(body).toMatchInline(` - Object { - "terms": Array [ - "Worker", - ], - } - `); + it('does not return transactions from other services', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { + query: { + serviceName: 'custom-php-service', + fieldName: TRANSACTION_NAME, + fieldValue: 'product', + start, + end, + }, + }, }); + + expect(body.terms).to.eql([]); }); }); - } - ); + }); + }); }