diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_duration_rule_type/index.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_duration_rule_type/index.tsx index 9d51adfeebc05..2b0e609704b8e 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_duration_rule_type/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_duration_rule_type/index.tsx @@ -121,6 +121,7 @@ export function TransactionDurationRuleType(props: Props) { interval, start, end, + groupBy: params.groupBy, }, }, } @@ -135,6 +136,7 @@ export function TransactionDurationRuleType(props: Props) { params.transactionName, params.windowSize, params.windowUnit, + params.groupBy, ] ); diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/chart_preview/index.tsx b/x-pack/plugins/apm/public/components/alerting/ui_components/chart_preview/index.tsx index 0e0b09109387c..737c36f9d3cc3 100644 --- a/x-pack/plugins/apm/public/components/alerting/ui_components/chart_preview/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ui_components/chart_preview/index.tsx @@ -20,7 +20,7 @@ import { TickFormatter, } from '@elastic/charts'; import { EuiSpacer } from '@elastic/eui'; -import React, { useState } from 'react'; +import React from 'react'; import { IUiSettingsClient } from '@kbn/core/public'; import { Coordinate } from '../../../../../typings/timeseries'; import { useTheme } from '../../../../hooks/use_theme'; @@ -39,8 +39,6 @@ export function ChartPreview({ uiSettings, series, }: ChartPreviewProps) { - const [yMax, setYMax] = useState(threshold); - const theme = useTheme(); const thresholdOpacity = 0.3; const timestamps = series.flatMap(({ data }) => data.map(({ x }) => x)); @@ -48,12 +46,6 @@ export function ChartPreview({ const xMax = Math.max(...timestamps); const xFormatter = niceTimeFormatter([xMin, xMax]); - function updateYMax() { - // Make the maximum Y value either the actual max or 20% more than the threshold - const values = series.flatMap(({ data }) => data.map((d) => d.y ?? 0)); - setYMax(Math.max(...values, threshold * 1.2)); - } - const style = { fill: theme.eui.euiColorVis2, line: { @@ -76,24 +68,35 @@ export function ChartPreview({ ]; const timeZone = getTimeZone(uiSettings); - const legendSize = Math.ceil(series.length / 2) * 30; + const legendSize = + series.length > 1 ? Math.ceil(series.length / 2) * 30 : series.length * 35; const chartSize = 150; + const domainYMax = () => { + // Make the maximum Y value either the actual max or 20% more than the threshold + const values = series.flatMap(({ data }) => data.map((d) => d.y ?? 0)); + return Math.max(...values, threshold * 1.2); + }; + + const domain = { + max: domainYMax(), + min: 0, + }; + return ( <> 1 ? chartSize + legendSize : chartSize, + height: chartSize + legendSize, }} data-test-subj="ChartPreview" > 1} + showLegend={true} legendPosition={'bottom'} legendSize={legendSize} - onLegendItemClick={updateYMax} /> {series.map(({ name, data }, index) => ( ; @@ -112,7 +115,6 @@ const transactionDurationChartPreview = createApmServerRoute({ export const alertsChartPreviewRouteRepository = { ...transactionErrorRateChartPreview, - ...transactionDurationChartPreview, ...transactionErrorCountChartPreview, ...transactionDurationChartPreview, }; diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/get_transaction_duration_chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/get_transaction_duration_chart_preview.ts index f7b226216aef0..6d39a59fe73a3 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/get_transaction_duration_chart_preview.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/get_transaction_duration_chart_preview.ts @@ -7,10 +7,12 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; -import { AggregationType } from '../../../../../common/rules/apm_rule_types'; +import { + AggregationType, + ApmRuleType, +} from '../../../../../common/rules/apm_rule_types'; import { SERVICE_NAME, - SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME, } from '../../../../../common/es_fields/apm'; @@ -23,12 +25,13 @@ import { getProcessorEventForTransactions, } from '../../../../lib/helpers/transactions'; import { - ENVIRONMENT_NOT_DEFINED, - getEnvironmentLabel, -} from '../../../../../common/environment_filter_values'; -import { averageOrPercentileAgg } from './average_or_percentile_agg'; + averageOrPercentileAgg, + getMultiTermsSortOrder, +} from './average_or_percentile_agg'; import { APMConfig } from '../../../..'; import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client'; +import { getGroupByTerms } from '../utils/get_groupby_terms'; +import { getAllGroupByFields } from '../utils/get_all_groupby_fields'; export type TransactionDurationChartPreviewResponse = Array<{ name: string; @@ -53,6 +56,7 @@ export async function getTransactionDurationChartPreview({ interval, start, end, + groupBy, } = alertParams; const searchAggregatedTransactions = await getSearchTransactionsEvents({ config, @@ -63,9 +67,15 @@ export async function getTransactionDurationChartPreview({ const query = { bool: { filter: [ - ...termQuery(SERVICE_NAME, serviceName), - ...termQuery(TRANSACTION_TYPE, transactionType), - ...termQuery(TRANSACTION_NAME, transactionName), + ...termQuery(SERVICE_NAME, serviceName, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_TYPE, transactionType, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_NAME, transactionName, { + queryEmptyString: false, + }), ...rangeQuery(start, end), ...environmentQuery(environment), ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), @@ -77,6 +87,11 @@ export async function getTransactionDurationChartPreview({ searchAggregatedTransactions ); + const allGroupByFields = getAllGroupByFields( + ApmRuleType.TransactionDuration, + groupBy + ); + const aggs = { timeseries: { date_histogram: { @@ -89,23 +104,18 @@ export async function getTransactionDurationChartPreview({ }, }, aggs: { - environment: { - terms: { - field: SERVICE_ENVIRONMENT, - missing: ENVIRONMENT_NOT_DEFINED.value, - size: 10, - order: { - [aggregationType === AggregationType.Avg - ? 'avgLatency' - : `pctLatency.${ - aggregationType === AggregationType.P95 ? 95 : 99 - }`]: 'desc', - } as Record, + series: { + multi_terms: { + terms: [...getGroupByTerms(allGroupByFields)], + size: 3, + ...getMultiTermsSortOrder(aggregationType), + }, + aggs: { + ...averageOrPercentileAgg({ + aggregationType, + transactionDurationField, + }), }, - aggs: averageOrPercentileAgg({ - aggregationType, - transactionDurationField, - }), }, }, }, @@ -125,19 +135,19 @@ export async function getTransactionDurationChartPreview({ return []; } - const environmentDataMap = resp.aggregations.timeseries.buckets.reduce( + const seriesDataMap = resp.aggregations.timeseries.buckets.reduce( (acc, bucket) => { const x = bucket.key; - bucket.environment.buckets.forEach((environmentBucket) => { - const env = environmentBucket.key as string; + bucket.series.buckets.forEach((seriesBucket) => { + const bucketKey = seriesBucket.key.join('_'); const y = - 'avgLatency' in environmentBucket - ? environmentBucket.avgLatency.value - : environmentBucket.pctLatency.values[0].value; - if (acc[env]) { - acc[env].push({ x, y }); + 'avgLatency' in seriesBucket + ? seriesBucket.avgLatency.value + : seriesBucket.pctLatency.values[0].value; + if (acc[bucketKey]) { + acc[bucketKey].push({ x, y }); } else { - acc[env] = [{ x, y }]; + acc[bucketKey] = [{ x, y }]; } }); @@ -146,8 +156,8 @@ export async function getTransactionDurationChartPreview({ {} as Record> ); - return Object.keys(environmentDataMap).map((env) => ({ - name: getEnvironmentLabel(env), - data: environmentDataMap[env], + return Object.keys(seriesDataMap).map((key) => ({ + name: key, + data: seriesDataMap[key], })); } diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts index 0ac465261c954..682457d4b19c4 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts @@ -64,6 +64,7 @@ import { getMultiTermsSortOrder, } from './average_or_percentile_agg'; import { getGroupByActionVariables } from '../utils/get_groupby_action_variables'; +import { getAllGroupByFields } from '../utils/get_all_groupby_fields'; const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionDuration]; @@ -103,14 +104,9 @@ export function registerTransactionDurationRuleType({ minimumLicenseRequired: 'basic', isExportable: true, executor: async ({ params: ruleParams, services, spaceId }) => { - const predefinedGroupby = [ - SERVICE_NAME, - SERVICE_ENVIRONMENT, - TRANSACTION_TYPE, - ]; - - const allGroupbyFields = Array.from( - new Set([...predefinedGroupby, ...(ruleParams.groupBy ?? [])]) + const allGroupByFields = getAllGroupByFields( + ApmRuleType.TransactionDuration, + ruleParams.groupBy ); const config = await firstValueFrom(config$); @@ -172,7 +168,7 @@ export function registerTransactionDurationRuleType({ aggs: { series: { multi_terms: { - terms: [...getGroupByTerms(allGroupbyFields)], + terms: [...getGroupByTerms(allGroupByFields)], size: 1000, ...getMultiTermsSortOrder(ruleParams.aggregationType), }, @@ -205,7 +201,7 @@ export function registerTransactionDurationRuleType({ for (const bucket of response.aggregations.series.buckets) { const groupByFields = bucket.key.reduce( (obj, bucketKey, bucketIndex) => { - obj[allGroupbyFields[bucketIndex]] = bucketKey; + obj[allGroupByFields[bucketIndex]] = bucketKey; return obj; }, {} as Record diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/utils/get_all_groupby_fields.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/utils/get_all_groupby_fields.ts new file mode 100644 index 0000000000000..6d4ccda93ee0a --- /dev/null +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/utils/get_all_groupby_fields.ts @@ -0,0 +1,29 @@ +/* + * 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 { union } from 'lodash'; +import { ApmRuleType } from '../../../../../common/rules/apm_rule_types'; +import { + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../../common/es_fields/apm'; + +export const getAllGroupByFields = ( + ruleType: string, + groupBy: string[] | undefined = [] +) => { + const predefinedGroupBy = + ruleType === ApmRuleType.TransactionDuration || + ruleType === ApmRuleType.TransactionErrorRate + ? [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE] + : ruleType === ApmRuleType.ErrorCount + ? [SERVICE_NAME, SERVICE_ENVIRONMENT] + : []; + + return union(predefinedGroupBy, groupBy); +}; diff --git a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts index f95bb8de59a89..bc96f7dda7d7d 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts @@ -5,6 +5,12 @@ * 2.0. */ +import { + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '@kbn/apm-plugin/common/es_fields/apm'; import expect from '@kbn/expect'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -246,5 +252,98 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expect(response.body.latencyChartPreview).to.eql([]); }); + + it('transaction_duration with no group by parameter', async () => { + const options = getOptions(); + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.length).to.equal(1); + expect( + response.body.latencyChartPreview.map( + (item: { name: string; data: Array<{ x: number; y: number | null }> }) => item.name + ) + ).to.eql(['opbeans-java_production_request']); + }); + + it('transaction_duration with default group by fields', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.length).to.equal(1); + expect( + response.body.latencyChartPreview.map( + (item: { name: string; data: Array<{ x: number; y: number | null }> }) => item.name + ) + ).to.eql(['opbeans-java_production_request']); + }); + + it('transaction_duration with group by on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.length).to.equal(3); + expect( + response.body.latencyChartPreview.map( + (item: { name: string; data: Array<{ x: number; y: number | null }> }) => item.name + ) + ).to.eql([ + 'opbeans-java_production_request_DispatcherServlet#doGet', + 'opbeans-java_production_request_APIRestController#stats', + 'opbeans-java_production_request_APIRestController#customers', + ]); + }); + + it('transaction_duration with group by on transaction name and filter on transaction name', async () => { + const options = { + params: { + query: { + ...getOptions().params.query, + transactionName: 'DispatcherServlet#doGet', + groupBy: [SERVICE_NAME, SERVICE_ENVIRONMENT, TRANSACTION_TYPE, TRANSACTION_NAME], + }, + }, + }; + + const response = await apmApiClient.readUser({ + ...options, + endpoint: 'GET /internal/apm/rule_types/transaction_duration/chart_preview', + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview.length).to.equal(1); + expect( + response.body.latencyChartPreview.map( + (item: { name: string; data: Array<{ x: number; y: number | null }> }) => item.name + ) + ).to.eql(['opbeans-java_production_request_DispatcherServlet#doGet']); + }); }); }