[APM] Add transaction name filter in latency threshold rule (#154241)

Part of the #152329

1. Adds a synthrace scenario that generates many transactions per
service
2. Fixes the duration chart preview when selecting All option -
https://github.com/elastic/kibana/issues/152195
3. Introduces the `Transaction name` filter in the rule type. 


### Pages loading the rule type
1. APM
2. Alert
3. Management rule 


### Consider 
- [ ] Update/Adding documentation example
https://www.elastic.co/guide/en/kibana/master/apm-alerts.html#apm-create-transaction-alert
## Creating a rule

https://user-images.githubusercontent.com/3369346/231740745-425c8eb8-6798-4ce4-b375-4ef96afdb334.mov

## Updating a rule


https://user-images.githubusercontent.com/3369346/231742035-28f50dfd-64bb-475d-b037-331eea4188d7.mov


### Before



https://user-images.githubusercontent.com/3369346/232799038-2edaa199-b970-48f2-b3ca-728273e4bf44.mov



### Notes

#### Feedback
The default action messages don't include any links. I will create a
follow-up ticket to improve the messages with actionable links.

Related bugs but out of the scope of the PR

- https://github.com/elastic/kibana/issues/154818
- https://github.com/elastic/kibana/issues/154704

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Katerina Patticha 2023-04-20 13:01:16 +02:00 committed by GitHub
parent 79969fbf2c
commit 7c70508b7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 283 additions and 33 deletions

View file

@ -0,0 +1,84 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ApmFields, apm, Instance } from '@kbn/apm-synthtrace-client';
import { Scenario } from '../cli/scenario';
import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment';
const ENVIRONMENT = getSynthtraceEnvironment(__filename);
const scenario: Scenario<ApmFields> = async (runOptions) => {
const { logger } = runOptions;
const { numServices = 3 } = runOptions.scenarioOpts || {};
const numTransactions = 100;
return {
generate: ({ range }) => {
const urls = ['GET /order', 'POST /basket', 'DELETE /basket', 'GET /products'];
const successfulTimestamps = range.ratePerMinute(180);
const failedTimestamps = range.interval('1m').rate(180);
const instances = [...Array(numServices).keys()].map((index) =>
apm
.service({ name: `synth-go-${index}`, environment: ENVIRONMENT, agentName: 'go' })
.instance(`instance-${index}`)
);
const transactionNames = [...Array(numTransactions).keys()].map(
(index) => `${urls[index % urls.length]}/${index}`
);
const instanceSpans = (instance: Instance, transactionName: string) => {
const successfulTraceEvents = successfulTimestamps.generator((timestamp) =>
instance.transaction({ transactionName }).timestamp(timestamp).duration(1000).success()
);
const failedTraceEvents = failedTimestamps.generator((timestamp) =>
instance
.transaction({ transactionName })
.timestamp(timestamp)
.duration(1000)
.failure()
.errors(
instance
.error({ message: '[ResponseError] index_not_found_exception' })
.timestamp(timestamp + 50)
)
);
const metricsets = range
.interval('30s')
.rate(1)
.generator((timestamp) =>
instance
.appMetrics({
'system.memory.actual.free': 800,
'system.memory.total': 1000,
'system.cpu.total.norm.pct': 0.6,
'system.process.cpu.total.norm.pct': 0.7,
})
.timestamp(timestamp)
);
return [successfulTraceEvents, failedTraceEvents, metricsets];
};
return logger.perf('generating_apm_events', () =>
instances
.flatMap((instance) =>
transactionNames.map((transactionName) => ({ instance, transactionName }))
)
.flatMap(({ instance, transactionName }, index) =>
instanceSpans(instance, transactionName)
)
);
},
};
};
export default scenario;

View file

@ -25,7 +25,8 @@ export const transactionDurationMessage = i18n.translate(
defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions:
- Service name: \\{\\{context.serviceName\\}\\}
- Type: \\{\\{context.transactionType\\}\\}
- Transaction type: \\{\\{context.transactionType\\}\\}
- Transaction name: \\{\\{context.transactionName\\}\\}
- Environment: \\{\\{context.environment\\}\\}
- Latency threshold: \\{\\{context.threshold\\}\\}ms
- Latency observed: \\{\\{context.triggerValue\\}\\} over the last \\{\\{context.interval\\}\\}`,

View file

@ -20,6 +20,7 @@ export const errorCountParamsSchema = schema.object({
export const transactionDurationParamsSchema = schema.object({
serviceName: schema.maybe(schema.string()),
transactionType: schema.maybe(schema.string()),
transactionName: schema.maybe(schema.string()),
windowSize: schema.number(),
windowUnit: schema.string(),
threshold: schema.number(),

View file

@ -11,11 +11,18 @@ import { CoreStart } from '@kbn/core/public';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { RuleParams, TransactionDurationRuleType } from '.';
import { AggregationType } from '../../../../../common/rules/apm_rule_types';
import { AlertMetadata } from '../../utils/helper';
import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
const KibanaReactContext = createKibanaReactContext({
notifications: { toasts: { add: () => {} } },
} as unknown as Partial<CoreStart>);
interface Args {
ruleParams: RuleParams;
metadata?: AlertMetadata;
}
export default {
title: 'alerting/TransactionDurationRuleType',
component: TransactionDurationRuleType,
@ -32,16 +39,11 @@ export default {
],
};
export const Example: Story = () => {
const [params, setParams] = useState<RuleParams>({
aggregationType: AggregationType.Avg,
environment: 'testEnvironment',
serviceName: 'testServiceName',
threshold: 1500,
transactionType: 'testTransactionType',
windowSize: 5,
windowUnit: 'm',
});
export const CreatingInApmServiceOverview: Story<Args> = ({
ruleParams,
metadata,
}) => {
const [params, setParams] = useState<RuleParams>(ruleParams);
function setRuleParams(property: string, value: any) {
setParams({ ...params, [property]: value });
@ -50,8 +52,57 @@ export const Example: Story = () => {
return (
<TransactionDurationRuleType
ruleParams={params}
metadata={metadata}
setRuleParams={setRuleParams}
setRuleProperty={() => {}}
/>
);
};
CreatingInApmServiceOverview.args = {
ruleParams: {
aggregationType: AggregationType.Avg,
environment: 'testEnvironment',
serviceName: 'testServiceName',
threshold: 1500,
transactionType: 'testTransactionType',
transactionName: 'GET /api/customer/:id',
windowSize: 5,
windowUnit: 'm',
},
metadata: {
environment: ENVIRONMENT_ALL.value,
serviceName: undefined,
},
};
export const CreatingInStackManagement: Story<Args> = ({
ruleParams,
metadata,
}) => {
const [params, setParams] = useState<RuleParams>(ruleParams);
function setRuleParams(property: string, value: any) {
setParams({ ...params, [property]: value });
}
return (
<TransactionDurationRuleType
ruleParams={params}
metadata={metadata}
setRuleParams={setRuleParams}
setRuleProperty={() => {}}
/>
);
};
CreatingInStackManagement.args = {
ruleParams: {
aggregationType: AggregationType.Avg,
environment: 'testEnvironment',
threshold: 1500,
windowSize: 5,
windowUnit: 'm',
},
metadata: undefined,
};

View file

@ -30,6 +30,7 @@ import {
IsAboveField,
ServiceField,
TransactionTypeField,
TransactionNameField,
} from '../../utils/fields';
import { AlertMetadata, getIntervalAndTimeRange } from '../../utils/helper';
import { ApmRuleParamsContainer } from '../../ui_components/apm_rule_params_container';
@ -38,9 +39,10 @@ import { PopoverExpression } from '../../ui_components/popover_expression';
export interface RuleParams {
aggregationType: AggregationType;
environment: string;
serviceName: string;
threshold: number;
transactionType: string;
transactionType?: string;
transactionName?: string;
serviceName?: string;
windowSize: number;
windowUnit: string;
}
@ -105,6 +107,7 @@ export function TransactionDurationRuleType(props: Props) {
environment: params.environment,
serviceName: params.serviceName,
transactionType: params.transactionType,
transactionName: params.transactionName,
interval,
start,
end,
@ -119,6 +122,7 @@ export function TransactionDurationRuleType(props: Props) {
params.environment,
params.serviceName,
params.transactionType,
params.transactionName,
params.windowSize,
params.windowUnit,
]
@ -149,7 +153,8 @@ export function TransactionDurationRuleType(props: Props) {
onChange={(value) => {
if (value !== params.serviceName) {
setRuleParams('serviceName', value);
setRuleParams('transactionType', '');
setRuleParams('transactionType', undefined);
setRuleParams('transactionName', undefined);
setRuleParams('environment', ENVIRONMENT_ALL.value);
}
}}
@ -164,6 +169,11 @@ export function TransactionDurationRuleType(props: Props) {
onChange={(value) => setRuleParams('environment', value)}
serviceName={params.serviceName}
/>,
<TransactionNameField
currentValue={params.transactionName}
onChange={(value) => setRuleParams('transactionName', value)}
serviceName={params.serviceName}
/>,
<PopoverExpression
value={params.aggregationType}
title={i18n.translate('xpack.apm.transactionDurationRuleType.when', {

View file

@ -12,6 +12,7 @@ import {
SERVICE_ENVIRONMENT,
SERVICE_NAME,
TRANSACTION_TYPE,
TRANSACTION_NAME,
} from '../../../../common/es_fields/apm';
import {
ENVIRONMENT_ALL,
@ -39,7 +40,7 @@ export function ServiceField({
>
<SuggestionsSelect
customOptions={
allowAll ? [{ label: allOptionText, value: '' }] : undefined
allowAll ? [{ label: allOptionText, value: undefined }] : undefined
}
customOptionText={i18n.translate(
'xpack.apm.serviceNamesSelectCustomOptionText',
@ -98,6 +99,46 @@ export function EnvironmentField({
);
}
export function TransactionNameField({
currentValue,
onChange,
serviceName,
}: {
currentValue?: string;
onChange: (value?: string) => void;
serviceName?: string;
}) {
const label = i18n.translate('xpack.apm.alerting.fields.transaction.name', {
defaultMessage: 'Name',
});
return (
<PopoverExpression value={currentValue || allOptionText} title={label}>
<SuggestionsSelect
customOptions={[{ label: allOptionText, value: undefined }]}
customOptionText={i18n.translate(
'xpack.apm.alerting.transaction.name.custom.text',
{
defaultMessage: 'Add \\{searchValue\\} as a new transaction name',
}
)}
defaultValue={currentValue}
fieldName={TRANSACTION_NAME}
onChange={onChange}
placeholder={i18n.translate(
'xpack.apm.transactionNamesSelectPlaceholder',
{
defaultMessage: 'Select transaction name',
}
)}
start={moment().subtract(24, 'h').toISOString()}
end={moment().toISOString()}
serviceName={serviceName}
/>
</PopoverExpression>
);
}
export function TransactionTypeField({
currentValue,
onChange,
@ -113,7 +154,7 @@ export function TransactionTypeField({
return (
<PopoverExpression value={currentValue || allOptionText} title={label}>
<SuggestionsSelect
customOptions={[{ label: allOptionText, value: '' }]}
customOptions={[{ label: allOptionText, value: undefined }]}
customOptionText={i18n.translate(
'xpack.apm.transactionTypesSelectCustomOptionText',
{

View file

@ -66,6 +66,13 @@ export const apmActionVariables = {
),
name: 'transactionType' as const,
},
transactionName: {
description: i18n.translate(
'xpack.apm.alerts.action_variables.transactionName',
{ defaultMessage: 'The transaction name the alert is created for' }
),
name: 'transactionName' as const,
},
triggerValue: {
description: i18n.translate(
'xpack.apm.alerts.action_variables.triggerValue',

View file

@ -21,6 +21,7 @@ import {
SERVICE_ENVIRONMENT,
SERVICE_LANGUAGE_NAME,
SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_TYPE,
} from '../../../common/es_fields/apm';
import { registerTransactionDurationRuleType } from './rule_types/transaction_duration/register_transaction_duration_rule_type';
@ -45,6 +46,10 @@ export const apmRuleTypeAlertFieldMap = {
type: 'keyword',
required: false,
},
[TRANSACTION_NAME]: {
type: 'keyword',
required: false,
},
[PROCESSOR_EVENT]: {
type: 'keyword',
required: false,

View file

@ -29,6 +29,7 @@ const alertParamsRt = t.intersection([
]),
serviceName: t.string,
transactionType: t.string,
transactionName: t.string,
}),
environmentRt,
rangeRt,

View file

@ -111,9 +111,7 @@ export function registerErrorCountRuleType({
},
},
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.error } },
...termQuery(SERVICE_NAME, ruleParams.serviceName, {
queryEmptyString: false,
}),
...termQuery(SERVICE_NAME, ruleParams.serviceName),
...environmentQuery(ruleParams.environment),
],
},

View file

@ -12,6 +12,7 @@ import {
SERVICE_NAME,
SERVICE_ENVIRONMENT,
TRANSACTION_TYPE,
TRANSACTION_NAME,
} from '../../../../../common/es_fields/apm';
import { environmentQuery } from '../../../../../common/utils/environment_query';
import { AlertParams } from '../../route';
@ -48,6 +49,7 @@ export async function getTransactionDurationChartPreview({
environment,
serviceName,
transactionType,
transactionName,
interval,
start,
end,
@ -63,6 +65,7 @@ export async function getTransactionDurationChartPreview({
filter: [
...termQuery(SERVICE_NAME, serviceName),
...termQuery(TRANSACTION_TYPE, transactionType),
...termQuery(TRANSACTION_NAME, transactionName),
...rangeQuery(start, end),
...environmentQuery(environment),
...getDocumentTypeFilterForTransactions(searchAggregatedTransactions),

View file

@ -52,6 +52,7 @@ describe('registerTransactionDurationRuleType', () => {
transactionType: 'request',
serviceName: 'opbeans-java',
aggregationType: 'avg',
transactionName: 'GET /orders',
};
await executor({ params });
expect(scheduleActions).toHaveBeenCalledTimes(1);
@ -59,6 +60,7 @@ describe('registerTransactionDurationRuleType', () => {
alertDetailsUrl: expect.stringContaining(
'http://localhost:5601/eyr/app/observability/alerts/'
),
transactionName: 'GET /orders',
environment: 'Not defined',
interval: `5 mins`,
reason:

View file

@ -32,6 +32,7 @@ import {
PROCESSOR_EVENT,
SERVICE_ENVIRONMENT,
SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_TYPE,
} from '../../../../../common/es_fields/apm';
import {
@ -94,6 +95,7 @@ export function registerTransactionDurationRuleType({
apmActionVariables.reason,
apmActionVariables.serviceName,
apmActionVariables.transactionType,
apmActionVariables.transactionName,
apmActionVariables.threshold,
apmActionVariables.triggerValue,
apmActionVariables.viewInAppUrl,
@ -146,12 +148,9 @@ export function registerTransactionDurationRuleType({
...getDocumentTypeFilterForTransactions(
searchAggregatedTransactions
),
...termQuery(SERVICE_NAME, ruleParams.serviceName, {
queryEmptyString: false,
}),
...termQuery(TRANSACTION_TYPE, ruleParams.transactionType, {
queryEmptyString: false,
}),
...termQuery(SERVICE_NAME, ruleParams.serviceName),
...termQuery(TRANSACTION_TYPE, ruleParams.transactionType),
...termQuery(TRANSACTION_NAME, ruleParams.transactionName),
...environmentQuery(ruleParams.environment),
] as QueryDslQueryContainer[],
},
@ -268,6 +267,7 @@ export function registerTransactionDurationRuleType({
[SERVICE_NAME]: serviceName,
...getEnvironmentEsField(environment),
[TRANSACTION_TYPE]: transactionType,
[TRANSACTION_NAME]: ruleParams.transactionName,
[PROCESSOR_EVENT]: ProcessorEvent.transaction,
[ALERT_EVALUATION_VALUE]: transactionDuration,
[ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold,
@ -284,6 +284,7 @@ export function registerTransactionDurationRuleType({
),
reason,
serviceName,
transactionName: ruleParams.transactionName, // #Note once we group by transactionName, use the transactionName key from the bucket
threshold: ruleParams.threshold,
transactionType,
triggerValue: transactionDurationFormatted,

View file

@ -142,12 +142,8 @@ export function registerTransactionErrorRateRuleType({
],
},
},
...termQuery(SERVICE_NAME, ruleParams.serviceName, {
queryEmptyString: false,
}),
...termQuery(TRANSACTION_TYPE, ruleParams.transactionType, {
queryEmptyString: false,
}),
...termQuery(SERVICE_NAME, ruleParams.serviceName),
...termQuery(TRANSACTION_TYPE, ruleParams.transactionType),
...environmentQuery(ruleParams.environment),
],
},

View file

@ -115,5 +115,54 @@ export default function ApiTest({ getService }: FtrProviderContext) {
)
).to.equal(true);
});
it('transaction_duration with transaction name', async () => {
const options = {
params: {
query: {
start,
end,
serviceName: 'opbeans-java',
transactionName: 'DispatcherServlet#doGet',
transactionType: 'request',
environment: 'ENVIRONMENT_ALL',
interval: '5m',
},
},
};
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[0].data[0]).to.eql({
x: 1627974600000,
y: 18485.85714285714,
});
});
it('transaction_duration with nonexistent transaction name', async () => {
const options = {
params: {
query: {
start,
end,
serviceName: 'opbeans-java',
transactionType: 'request',
transactionName: 'foo',
environment: 'ENVIRONMENT_ALL',
interval: '5m',
},
},
};
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).to.eql([]);
});
});
}

View file

@ -32,7 +32,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
name: 'Latency threshold | synth-go',
params: {
serviceName: 'synth-go',
transactionType: '',
transactionType: undefined,
windowSize: 99,
windowUnit: 'y',
threshold: 100,

View file

@ -48,7 +48,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) {
name: `Latency threshold | ${goService}`,
params: {
serviceName: goService,
transactionType: '',
transactionType: undefined,
windowSize: 99,
windowUnit: 'y',
threshold: 100,