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']);
+ });
});
}