mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SLO] APM latency SLI - add custom panel to alert details (#179078)
## Summary
Adds the APM latency, throughput, and error rate chart to the APM
latency SLI's alert details page.

### Testing
1. Generate some APM data. The easiest way to do so is to via
`synthtrace`, for example `node scripts/synthtrace many_transactions.ts
--live`
2. Navigate to the SLO page. Create an APM latency SLI with a threshold
of `2500` ms.
3. Wait for an alert to fire
4. Navigate to the alert details page for the alert to view the charts.
5. Navigate to the APM page for the service selected. Compare the charts
to confirm the accuracy.
6. Ideally, you'd repeat this test with many different configurations of
the SLI, for example an SLI with a specific environment, transaction
type, or transaction name, and compare the charts from the APM page for
accuracy.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
218f5ae6f2
commit
66f1c4e5a5
41 changed files with 1837 additions and 87 deletions
|
@ -10,7 +10,7 @@ import { escapeKuery } from '@kbn/es-query';
|
|||
import { SERVICE_ENVIRONMENT } from './es_fields/apm';
|
||||
import { Environment } from './environment_rt';
|
||||
|
||||
const ENVIRONMENT_ALL_VALUE = 'ENVIRONMENT_ALL' as const;
|
||||
export const ENVIRONMENT_ALL_VALUE = 'ENVIRONMENT_ALL' as const;
|
||||
const ENVIRONMENT_NOT_DEFINED_VALUE = 'ENVIRONMENT_NOT_DEFINED' as const;
|
||||
|
||||
export const allOptionText = i18n.translate('xpack.apm.filter.environment.allLabel', {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { EuiFlexItem, EuiPanel, EuiFlexGroup, EuiTitle, EuiIconTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { BoolQuery } from '@kbn/es-query';
|
||||
import React from 'react';
|
||||
import { RecursivePartial } from '@elastic/eui';
|
||||
import { Theme } from '@elastic/charts';
|
||||
|
@ -20,6 +21,7 @@ import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
|
|||
import { yLabelFormat } from './helpers';
|
||||
import { usePreferredDataSourceAndBucketSize } from '../../../../hooks/use_preferred_data_source_and_bucket_size';
|
||||
import { ApmDocumentType } from '../../../../../common/document_type';
|
||||
import { TransactionTypeSelect } from './transaction_type_select';
|
||||
|
||||
type ErrorRate =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/charts/error_rate'>;
|
||||
|
@ -37,6 +39,8 @@ const INITIAL_STATE_ERROR_RATE: ErrorRate = {
|
|||
|
||||
function FailedTransactionChart({
|
||||
transactionType,
|
||||
transactionTypes,
|
||||
setTransactionType,
|
||||
transactionName,
|
||||
serviceName,
|
||||
environment,
|
||||
|
@ -44,8 +48,12 @@ function FailedTransactionChart({
|
|||
end,
|
||||
comparisonChartTheme,
|
||||
timeZone,
|
||||
kuery = '',
|
||||
filters,
|
||||
}: {
|
||||
transactionType: string;
|
||||
transactionTypes?: string[];
|
||||
setTransactionType?: (transactionType: string) => void;
|
||||
transactionName?: string;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
|
@ -53,6 +61,8 @@ function FailedTransactionChart({
|
|||
end: string;
|
||||
comparisonChartTheme: RecursivePartial<Theme>;
|
||||
timeZone: string;
|
||||
kuery?: string;
|
||||
filters?: BoolQuery;
|
||||
}) {
|
||||
const { currentPeriodColor: currentPeriodColorErrorRate } =
|
||||
get_timeseries_color.getTimeSeriesColor(ChartType.FAILED_TRANSACTION_RATE);
|
||||
|
@ -60,7 +70,7 @@ function FailedTransactionChart({
|
|||
const preferred = usePreferredDataSourceAndBucketSize({
|
||||
start,
|
||||
end,
|
||||
kuery: '',
|
||||
kuery,
|
||||
numBuckets: 100,
|
||||
type: transactionName
|
||||
? ApmDocumentType.TransactionMetric
|
||||
|
@ -79,7 +89,8 @@ function FailedTransactionChart({
|
|||
},
|
||||
query: {
|
||||
environment,
|
||||
kuery: '',
|
||||
kuery,
|
||||
filters: filters ? JSON.stringify(filters) : undefined,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
|
@ -93,7 +104,17 @@ function FailedTransactionChart({
|
|||
);
|
||||
}
|
||||
},
|
||||
[environment, serviceName, start, end, transactionType, transactionName, preferred]
|
||||
[
|
||||
environment,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
transactionName,
|
||||
preferred,
|
||||
kuery,
|
||||
filters,
|
||||
]
|
||||
);
|
||||
const timeseriesErrorRate = [
|
||||
{
|
||||
|
@ -105,6 +126,7 @@ function FailedTransactionChart({
|
|||
}),
|
||||
},
|
||||
];
|
||||
const showTransactionTypeSelect = setTransactionType && transactionTypes;
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder={true}>
|
||||
|
@ -122,6 +144,15 @@ function FailedTransactionChart({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip content={errorRateI18n} position="right" />
|
||||
</EuiFlexItem>
|
||||
{showTransactionTypeSelect && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionTypeSelect
|
||||
transactionType={transactionType}
|
||||
transactionTypes={transactionTypes}
|
||||
onChange={setTransactionType}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<TimeseriesChart
|
||||
|
|
|
@ -9,6 +9,7 @@ import { RecursivePartial, transparentize } from '@elastic/eui';
|
|||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexItem, EuiPanel, EuiFlexGroup, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { BoolQuery } from '@kbn/es-query';
|
||||
import { getDurationFormatter } from '@kbn/observability-plugin/common';
|
||||
import { ALERT_RULE_TYPE_ID, ALERT_EVALUATION_THRESHOLD, ALERT_END } from '@kbn/rule-data-utils';
|
||||
import type { TopAlert } from '@kbn/observability-plugin/public';
|
||||
|
@ -24,6 +25,7 @@ import { UI_SETTINGS } from '@kbn/data-plugin/public';
|
|||
import moment from 'moment';
|
||||
import chroma from 'chroma-js';
|
||||
import { filterNil } from '../../../shared/charts/latency_chart';
|
||||
import { LatencyAggregationTypeSelect } from '../../../shared/charts/latency_chart/latency_aggregation_type_select';
|
||||
import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
|
||||
import {
|
||||
getMaxY,
|
||||
|
@ -37,23 +39,31 @@ import { isLatencyThresholdRuleType } from './helpers';
|
|||
import { ApmDocumentType } from '../../../../../common/document_type';
|
||||
import { usePreferredDataSourceAndBucketSize } from '../../../../hooks/use_preferred_data_source_and_bucket_size';
|
||||
import { DEFAULT_DATE_FORMAT } from './constants';
|
||||
import { TransactionTypeSelect } from './transaction_type_select';
|
||||
|
||||
function LatencyChart({
|
||||
alert,
|
||||
transactionType,
|
||||
transactionTypes,
|
||||
transactionName,
|
||||
serviceName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
latencyAggregationType,
|
||||
setLatencyAggregationType,
|
||||
setTransactionType,
|
||||
comparisonChartTheme,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
timeZone,
|
||||
customAlertEvaluationThreshold,
|
||||
kuery = '',
|
||||
filters,
|
||||
}: {
|
||||
alert: TopAlert;
|
||||
transactionType: string;
|
||||
transactionTypes?: string[];
|
||||
transactionName?: string;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
|
@ -61,9 +71,14 @@ function LatencyChart({
|
|||
end: string;
|
||||
comparisonChartTheme: RecursivePartial<Theme>;
|
||||
latencyAggregationType: LatencyAggregationType;
|
||||
setLatencyAggregationType?: (value: LatencyAggregationType) => void;
|
||||
setTransactionType?: (value: string) => void;
|
||||
comparisonEnabled: boolean;
|
||||
offset: string;
|
||||
timeZone: string;
|
||||
customAlertEvaluationThreshold?: number;
|
||||
kuery?: string;
|
||||
filters?: BoolQuery;
|
||||
}) {
|
||||
const preferred = usePreferredDataSourceAndBucketSize({
|
||||
start,
|
||||
|
@ -86,7 +101,8 @@ function LatencyChart({
|
|||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
kuery: '',
|
||||
kuery,
|
||||
filters: filters ? JSON.stringify(filters) : undefined,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
|
@ -112,13 +128,15 @@ function LatencyChart({
|
|||
transactionType,
|
||||
transactionName,
|
||||
preferred,
|
||||
kuery,
|
||||
filters,
|
||||
]
|
||||
);
|
||||
const alertEvalThreshold =
|
||||
customAlertEvaluationThreshold || alert.fields[ALERT_EVALUATION_THRESHOLD];
|
||||
|
||||
const alertEnd = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]).valueOf() : undefined;
|
||||
|
||||
const alertEvalThreshold = alert.fields[ALERT_EVALUATION_THRESHOLD];
|
||||
|
||||
const alertEvalThresholdChartData = alertEvalThreshold
|
||||
? [
|
||||
<AlertThresholdTimeRangeRect
|
||||
|
@ -137,7 +155,10 @@ function LatencyChart({
|
|||
: [];
|
||||
|
||||
const getLatencyChartAdditionalData = () => {
|
||||
if (isLatencyThresholdRuleType(alert.fields[ALERT_RULE_TYPE_ID])) {
|
||||
if (
|
||||
isLatencyThresholdRuleType(alert.fields[ALERT_RULE_TYPE_ID]) ||
|
||||
customAlertEvaluationThreshold
|
||||
) {
|
||||
return [
|
||||
<AlertActiveTimeRangeAnnotation
|
||||
alertStart={alert.start}
|
||||
|
@ -176,6 +197,7 @@ function LatencyChart({
|
|||
].filter(filterNil);
|
||||
const latencyMaxY = getMaxY(timeseriesLatency);
|
||||
const latencyFormatter = getDurationFormatter(latencyMaxY);
|
||||
const showTransactionTypeSelect = transactionTypes && setTransactionType;
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder={true}>
|
||||
|
@ -189,6 +211,23 @@ function LatencyChart({
|
|||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{setLatencyAggregationType && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<LatencyAggregationTypeSelect
|
||||
latencyAggregationType={latencyAggregationType}
|
||||
onChange={setLatencyAggregationType}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{showTransactionTypeSelect && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionTypeSelect
|
||||
transactionType={transactionType}
|
||||
transactionTypes={transactionTypes}
|
||||
onChange={setTransactionType}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<TimeseriesChart
|
||||
id="latencyChart"
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Theme } from '@elastic/charts';
|
||||
import { BoolQuery } from '@kbn/es-query';
|
||||
import {
|
||||
RecursivePartial,
|
||||
EuiFlexItem,
|
||||
|
@ -23,6 +24,7 @@ import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
|
|||
import { usePreferredDataSourceAndBucketSize } from '../../../../hooks/use_preferred_data_source_and_bucket_size';
|
||||
import { ApmDocumentType } from '../../../../../common/document_type';
|
||||
import { asExactTransactionRate } from '../../../../../common/utils/formatters';
|
||||
import { TransactionTypeSelect } from './transaction_type_select';
|
||||
|
||||
const INITIAL_STATE = {
|
||||
currentPeriod: [],
|
||||
|
@ -30,6 +32,8 @@ const INITIAL_STATE = {
|
|||
};
|
||||
function ThroughputChart({
|
||||
transactionType,
|
||||
transactionTypes,
|
||||
setTransactionType,
|
||||
transactionName,
|
||||
serviceName,
|
||||
environment,
|
||||
|
@ -39,8 +43,12 @@ function ThroughputChart({
|
|||
comparisonEnabled,
|
||||
offset,
|
||||
timeZone,
|
||||
kuery = '',
|
||||
filters,
|
||||
}: {
|
||||
transactionType: string;
|
||||
transactionTypes?: string[];
|
||||
setTransactionType?: (transactionType: string) => void;
|
||||
transactionName?: string;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
|
@ -50,14 +58,14 @@ function ThroughputChart({
|
|||
comparisonEnabled: boolean;
|
||||
offset: string;
|
||||
timeZone: string;
|
||||
kuery?: string;
|
||||
filters?: BoolQuery;
|
||||
}) {
|
||||
/* Throughput Chart */
|
||||
|
||||
const preferred = usePreferredDataSourceAndBucketSize({
|
||||
start,
|
||||
end,
|
||||
numBuckets: 100,
|
||||
kuery: '',
|
||||
kuery,
|
||||
type: transactionName
|
||||
? ApmDocumentType.TransactionMetric
|
||||
: ApmDocumentType.ServiceTransactionMetric,
|
||||
|
@ -73,7 +81,8 @@ function ThroughputChart({
|
|||
},
|
||||
query: {
|
||||
environment,
|
||||
kuery: '',
|
||||
kuery,
|
||||
filters: filters ? JSON.stringify(filters) : undefined,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
|
@ -86,7 +95,17 @@ function ThroughputChart({
|
|||
});
|
||||
}
|
||||
},
|
||||
[environment, serviceName, start, end, transactionType, transactionName, preferred]
|
||||
[
|
||||
environment,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
transactionName,
|
||||
preferred,
|
||||
kuery,
|
||||
filters,
|
||||
]
|
||||
);
|
||||
const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor(ChartType.THROUGHPUT);
|
||||
const timeseriesThroughput = [
|
||||
|
@ -110,6 +129,8 @@ function ThroughputChart({
|
|||
: []),
|
||||
];
|
||||
|
||||
const showTransactionTypeSelect = setTransactionType && transactionTypes;
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder={true}>
|
||||
|
@ -132,6 +153,15 @@ function ThroughputChart({
|
|||
position="right"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{showTransactionTypeSelect && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionTypeSelect
|
||||
transactionType={transactionType}
|
||||
transactionTypes={transactionTypes}
|
||||
onChange={setTransactionType}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<TimeseriesChart
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiSelect } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
export function TransactionTypeSelect({
|
||||
transactionType,
|
||||
transactionTypes,
|
||||
onChange,
|
||||
}: {
|
||||
transactionType: string;
|
||||
transactionTypes: string[];
|
||||
onChange: (transactionType: string) => void;
|
||||
}) {
|
||||
const options = transactionTypes.map((t) => ({ text: t, value: t }));
|
||||
|
||||
return (
|
||||
<EuiSelect
|
||||
style={{ minWidth: 160 }}
|
||||
compressed
|
||||
data-test-subj="alertingFilterTransactionType"
|
||||
prepend={i18n.translate('xpack.apm.alertingVisualizations.transactionType.prepend', {
|
||||
defaultMessage: 'Transaction Type',
|
||||
})}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
options={options}
|
||||
value={transactionType}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -13,7 +13,10 @@ import React from 'react';
|
|||
import { offsetRt } from '../../../../common/comparison_rt';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { environmentRt } from '../../../../common/environment_rt';
|
||||
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
|
||||
import {
|
||||
LatencyAggregationType,
|
||||
latencyAggregationTypeRt,
|
||||
} from '../../../../common/latency_aggregation_types';
|
||||
import { AlertsOverview } from '../../app/alerts_overview';
|
||||
import { ServiceMapServiceDetail } from '../../app/service_map';
|
||||
import { MobileServiceTemplate } from '../templates/mobile_service_template';
|
||||
|
@ -83,7 +86,7 @@ export const mobileServiceDetailRoute = {
|
|||
comparisonEnabled: toBooleanRt,
|
||||
}),
|
||||
t.partial({
|
||||
latencyAggregationType: t.string,
|
||||
latencyAggregationType: latencyAggregationTypeRt,
|
||||
transactionType: t.string,
|
||||
refreshPaused: t.union([t.literal('true'), t.literal('false')]),
|
||||
refreshInterval: t.string,
|
||||
|
|
|
@ -15,7 +15,10 @@ import { Redirect } from 'react-router-dom';
|
|||
import { offsetRt } from '../../../../common/comparison_rt';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { environmentRt } from '../../../../common/environment_rt';
|
||||
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
|
||||
import {
|
||||
LatencyAggregationType,
|
||||
latencyAggregationTypeRt,
|
||||
} from '../../../../common/latency_aggregation_types';
|
||||
import { ApmTimeRangeMetadataContextProvider } from '../../../context/time_range_metadata/time_range_metadata_context';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { AlertsOverview, ALERT_STATUS_ALL } from '../../app/alerts_overview';
|
||||
|
@ -103,7 +106,7 @@ export const serviceDetailRoute = {
|
|||
comparisonEnabled: toBooleanRt,
|
||||
}),
|
||||
t.partial({
|
||||
latencyAggregationType: t.string,
|
||||
latencyAggregationType: latencyAggregationTypeRt,
|
||||
transactionType: t.string,
|
||||
refreshPaused: t.union([t.literal('true'), t.literal('false')]),
|
||||
refreshInterval: t.string,
|
||||
|
|
|
@ -5,15 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiTitle } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isTimeComparison } from '../../time_comparison/get_comparison_options';
|
||||
import {
|
||||
getLatencyAggregationType,
|
||||
LatencyAggregationType,
|
||||
} from '../../../../../common/latency_aggregation_types';
|
||||
import { getLatencyAggregationType } from '../../../../../common/latency_aggregation_types';
|
||||
import { getDurationFormatter } from '../../../../../common/utils/formatters';
|
||||
import { useLicenseContext } from '../../../../context/license/use_license_context';
|
||||
import { useTransactionLatencyChartsFetcher } from '../../../../hooks/use_transaction_latency_chart_fetcher';
|
||||
|
@ -29,18 +26,12 @@ import { useAnyOfApmParams } from '../../../../hooks/use_apm_params';
|
|||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import { getLatencyChartScreenContext } from './get_latency_chart_screen_context';
|
||||
|
||||
import { LatencyAggregationTypeSelect } from './latency_aggregation_type_select';
|
||||
interface Props {
|
||||
height?: number;
|
||||
kuery: string;
|
||||
}
|
||||
|
||||
const options: Array<{ value: LatencyAggregationType; text: string }> = [
|
||||
{ value: LatencyAggregationType.avg, text: 'Average' },
|
||||
{ value: LatencyAggregationType.p95, text: '95th percentile' },
|
||||
{ value: LatencyAggregationType.p99, text: '99th percentile' },
|
||||
];
|
||||
|
||||
export function filterNil<T>(value: T | null | undefined): value is T {
|
||||
return value != null;
|
||||
}
|
||||
|
@ -131,18 +122,12 @@ export function LatencyChart({ height, kuery }: Props) {
|
|||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
data-test-subj="apmLatencyChartSelect"
|
||||
compressed
|
||||
prepend={i18n.translate('xpack.apm.serviceOverview.latencyChartTitle.prepend', {
|
||||
defaultMessage: 'Metric',
|
||||
})}
|
||||
options={options}
|
||||
value={latencyAggregationType}
|
||||
onChange={(nextOption) => {
|
||||
<LatencyAggregationTypeSelect
|
||||
latencyAggregationType={latencyAggregationType}
|
||||
onChange={(type) => {
|
||||
urlHelpers.push(history, {
|
||||
query: {
|
||||
latencyAggregationType: nextOption.target.value,
|
||||
latencyAggregationType: type,
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { EuiSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
|
||||
|
||||
const options: Array<{ value: LatencyAggregationType; text: string }> = [
|
||||
{ value: LatencyAggregationType.avg, text: 'Average' },
|
||||
{ value: LatencyAggregationType.p95, text: '95th percentile' },
|
||||
{ value: LatencyAggregationType.p99, text: '99th percentile' },
|
||||
];
|
||||
|
||||
export function LatencyAggregationTypeSelect({
|
||||
latencyAggregationType,
|
||||
onChange,
|
||||
}: {
|
||||
latencyAggregationType?: LatencyAggregationType;
|
||||
onChange: (value: LatencyAggregationType) => void;
|
||||
}) {
|
||||
return (
|
||||
<EuiSelect
|
||||
data-test-subj="apmLatencyChartSelect"
|
||||
compressed
|
||||
prepend={i18n.translate('xpack.apm.serviceOverview.latencyChartTitle.prepend', {
|
||||
defaultMessage: 'Metric',
|
||||
})}
|
||||
options={options}
|
||||
value={latencyAggregationType}
|
||||
onChange={(nextOption) => onChange(nextOption.target.value as LatencyAggregationType)}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -9,6 +9,8 @@ import type { AppMountParameters, CoreStart } from '@kbn/core/public';
|
|||
import { createContext } from 'react';
|
||||
import type { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public';
|
||||
import type { MapsStartApi } from '@kbn/maps-plugin/public';
|
||||
import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||
import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public';
|
||||
import type { ObservabilityPublicStart } from '@kbn/observability-plugin/public';
|
||||
import type { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
|
@ -29,6 +31,7 @@ export interface ApmPluginContextValue {
|
|||
plugins: ApmPluginSetupDeps & { maps?: MapsStartApi };
|
||||
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
|
||||
observability: ObservabilityPublicStart;
|
||||
observabilityShared: ObservabilitySharedPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
|
@ -36,6 +39,7 @@ export interface ApmPluginContextValue {
|
|||
observabilityAIAssistant?: ObservabilityAIAssistantPublicStart;
|
||||
share: SharePluginSetup;
|
||||
kibanaEnvironment: KibanaEnvContext;
|
||||
lens: LensPublicStart;
|
||||
}
|
||||
|
||||
export const ApmPluginContext = createContext({} as ApmPluginContextValue);
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { APMAlertingFailedTransactionsChart } from './chart';
|
||||
import { ApmEmbeddableContext } from '../../embeddable_context';
|
||||
import { MOCK_ALERT, MOCK_RULE, MOCK_DEPS } from '../testing/fixtures';
|
||||
import * as transactionFetcher from '../../../context/apm_service/use_service_transaction_types_fetcher';
|
||||
|
||||
jest.mock('../../../context/apm_service/use_service_agent_fetcher', () => ({
|
||||
useServiceAgentFetcher: jest.fn(() => ({
|
||||
agentName: 'mockAgent',
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('renders chart', () => {
|
||||
const serviceName = 'ops-bean';
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(transactionFetcher, 'useServiceTransactionTypesFetcher')
|
||||
.mockReturnValue({ transactionTypes: ['request'], status: FETCH_STATUS.SUCCESS });
|
||||
});
|
||||
it('renders error when serviceName is not defined', async () => {
|
||||
const { getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingFailedTransactionsChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
// @ts-ignore
|
||||
serviceName={undefined}
|
||||
alert={MOCK_ALERT}
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Unable to load the APM visualizations.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders when serviceName is defined', async () => {
|
||||
const { getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingFailedTransactionsChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
serviceName={serviceName}
|
||||
alert={MOCK_ALERT}
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Failed transaction rate')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('supports custom transactionType when transactionType is included in transaction types list', async () => {
|
||||
jest
|
||||
.spyOn(transactionFetcher, 'useServiceTransactionTypesFetcher')
|
||||
.mockReturnValue({ transactionTypes: ['request', 'custom'], status: FETCH_STATUS.SUCCESS });
|
||||
const { getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingFailedTransactionsChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
serviceName={serviceName}
|
||||
alert={MOCK_ALERT}
|
||||
transactionType="custom"
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('custom')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not support custom transactionType when transactionType is not included in transaction types list', async () => {
|
||||
jest
|
||||
.spyOn(transactionFetcher, 'useServiceTransactionTypesFetcher')
|
||||
.mockReturnValue({ transactionTypes: ['request'], status: FETCH_STATUS.SUCCESS });
|
||||
const { queryByText, getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingFailedTransactionsChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
serviceName={serviceName}
|
||||
alert={MOCK_ALERT}
|
||||
transactionType="custom"
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(queryByText('custom')).not.toBeInTheDocument();
|
||||
expect(getByText('request')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import FailedTransactionChart from '../../../components/alerting/ui_components/alert_details_app_section/failed_transaction_chart';
|
||||
import { useAlertingProps } from '../use_alerting_props';
|
||||
import { TimeRangeCallout } from '../time_range_callout';
|
||||
import { ServiceNameCallout } from '../service_name_callout';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import type { EmbeddableApmAlertingVizProps } from '../types';
|
||||
|
||||
export function APMAlertingFailedTransactionsChart({
|
||||
rule,
|
||||
serviceName,
|
||||
environment = ENVIRONMENT_ALL.value,
|
||||
rangeFrom = 'now-15m',
|
||||
rangeTo = 'now',
|
||||
transactionType,
|
||||
transactionName,
|
||||
kuery = '',
|
||||
filters,
|
||||
}: EmbeddableApmAlertingVizProps) {
|
||||
const {
|
||||
transactionType: currentTransactionType,
|
||||
transactionTypes,
|
||||
setTransactionType,
|
||||
comparisonChartTheme,
|
||||
timeZone,
|
||||
} = useAlertingProps({
|
||||
rule,
|
||||
serviceName,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
kuery,
|
||||
defaultTransactionType: transactionType,
|
||||
});
|
||||
|
||||
if (!rangeFrom || !rangeTo) {
|
||||
return <TimeRangeCallout />;
|
||||
}
|
||||
|
||||
if (!serviceName || !currentTransactionType) {
|
||||
return <ServiceNameCallout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FailedTransactionChart
|
||||
transactionType={currentTransactionType}
|
||||
transactionTypes={transactionTypes}
|
||||
setTransactionType={setTransactionType}
|
||||
transactionName={transactionName}
|
||||
serviceName={serviceName}
|
||||
environment={environment}
|
||||
start={rangeFrom}
|
||||
end={rangeTo}
|
||||
comparisonChartTheme={comparisonChartTheme}
|
||||
timeZone={timeZone}
|
||||
kuery={kuery}
|
||||
filters={filters}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { initializeTitles, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { EmbeddableApmAlertingVizProps } from '../types';
|
||||
import type { EmbeddableDeps } from '../../types';
|
||||
import { ApmEmbeddableContext } from '../../embeddable_context';
|
||||
import { APMAlertingFailedTransactionsChart } from './chart';
|
||||
|
||||
export const APM_ALERTING_FAILED_TRANSACTIONS_CHART_EMBEDDABLE =
|
||||
'APM_ALERTING_FAILED_TRANSACTIONS_CHART_EMBEDDABLE';
|
||||
|
||||
export const getApmAlertingFailedTransactionsChartEmbeddableFactory = (deps: EmbeddableDeps) => {
|
||||
const factory: ReactEmbeddableFactory<
|
||||
EmbeddableApmAlertingVizProps,
|
||||
DefaultEmbeddableApi<EmbeddableApmAlertingVizProps>
|
||||
> = {
|
||||
type: APM_ALERTING_FAILED_TRANSACTIONS_CHART_EMBEDDABLE,
|
||||
deserializeState: (state) => {
|
||||
return state.rawState as EmbeddableApmAlertingVizProps;
|
||||
},
|
||||
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
|
||||
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
|
||||
const serviceName$ = new BehaviorSubject(state.serviceName);
|
||||
const transactionType$ = new BehaviorSubject(state.transactionType);
|
||||
const transactionName$ = new BehaviorSubject(state.transactionName);
|
||||
const environment$ = new BehaviorSubject(state.environment);
|
||||
const rangeFrom$ = new BehaviorSubject(state.rangeFrom);
|
||||
const rangeTo$ = new BehaviorSubject(state.rangeTo);
|
||||
const rule$ = new BehaviorSubject(state.rule);
|
||||
const alert$ = new BehaviorSubject(state.alert);
|
||||
const kuery$ = new BehaviorSubject(state.kuery);
|
||||
const filters$ = new BehaviorSubject(state.filters);
|
||||
|
||||
const api = buildApi(
|
||||
{
|
||||
...titlesApi,
|
||||
serializeState: () => {
|
||||
return {
|
||||
rawState: {
|
||||
...serializeTitles(),
|
||||
serviceName: serviceName$.getValue(),
|
||||
transactionType: transactionType$.getValue(),
|
||||
transactionName: transactionName$.getValue(),
|
||||
environment: environment$.getValue(),
|
||||
rangeFrom: rangeFrom$.getValue(),
|
||||
rangeTo: rangeTo$.getValue(),
|
||||
rule: rule$.getValue(),
|
||||
alert: alert$.getValue(),
|
||||
kuery: kuery$.getValue(),
|
||||
filters: filters$.getValue(),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: [serviceName$, (value) => serviceName$.next(value)],
|
||||
transactionType: [transactionType$, (value) => transactionType$.next(value)],
|
||||
transactionName: [transactionName$, (value) => transactionName$.next(value)],
|
||||
environment: [environment$, (value) => environment$.next(value)],
|
||||
rangeFrom: [rangeFrom$, (value) => rangeFrom$.next(value)],
|
||||
rangeTo: [rangeTo$, (value) => rangeTo$.next(value)],
|
||||
rule: [rule$, (value) => rule$.next(value)],
|
||||
alert: [alert$, (value) => alert$.next(value)],
|
||||
kuery: [kuery$, (value) => kuery$.next(value)],
|
||||
filters: [filters$, (value) => filters$.next(value)],
|
||||
...titleComparators,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
const [
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
environment,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
rule,
|
||||
alert,
|
||||
kuery,
|
||||
filters,
|
||||
] = useBatchedPublishingSubjects(
|
||||
serviceName$,
|
||||
transactionType$,
|
||||
transactionName$,
|
||||
environment$,
|
||||
rangeFrom$,
|
||||
rangeTo$,
|
||||
rule$,
|
||||
alert$,
|
||||
kuery$,
|
||||
filters$
|
||||
);
|
||||
|
||||
return (
|
||||
<ApmEmbeddableContext deps={deps} rangeFrom={rangeFrom} rangeTo={rangeTo}>
|
||||
<APMAlertingFailedTransactionsChart
|
||||
rule={rule}
|
||||
alert={alert}
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
environment={environment}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
transactionName={transactionName}
|
||||
kuery={kuery}
|
||||
filters={filters}
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
return factory;
|
||||
};
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { APMAlertingLatencyChart } from './chart';
|
||||
import { ApmEmbeddableContext } from '../../embeddable_context';
|
||||
import { MOCK_ALERT, MOCK_RULE, MOCK_DEPS } from '../testing/fixtures';
|
||||
import * as transactionFetcher from '../../../context/apm_service/use_service_transaction_types_fetcher';
|
||||
|
||||
jest.mock('../../../context/apm_service/use_service_agent_fetcher', () => ({
|
||||
useServiceAgentFetcher: jest.fn(() => ({
|
||||
agentName: 'mockAgent',
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('renders chart', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(transactionFetcher, 'useServiceTransactionTypesFetcher')
|
||||
.mockReturnValue({ transactionTypes: ['request'], status: FETCH_STATUS.SUCCESS });
|
||||
});
|
||||
const serviceName = 'ops-bean';
|
||||
it('renders error when serviceName is not defined', async () => {
|
||||
const { getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingLatencyChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
// @ts-ignore
|
||||
serviceName={undefined}
|
||||
alert={MOCK_ALERT}
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Unable to load the APM visualizations.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders when serviceName is defined', async () => {
|
||||
const { getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingLatencyChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
serviceName={serviceName}
|
||||
alert={MOCK_ALERT}
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Latency')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('supports custom transactionType when transactionType is included in transaction types list', async () => {
|
||||
jest
|
||||
.spyOn(transactionFetcher, 'useServiceTransactionTypesFetcher')
|
||||
.mockReturnValue({ transactionTypes: ['request', 'custom'], status: FETCH_STATUS.SUCCESS });
|
||||
const { getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingLatencyChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
serviceName={serviceName}
|
||||
alert={MOCK_ALERT}
|
||||
transactionType="custom"
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('custom')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not support custom transactionType when transactionType is not included in transaction types list', async () => {
|
||||
jest
|
||||
.spyOn(transactionFetcher, 'useServiceTransactionTypesFetcher')
|
||||
.mockReturnValue({ transactionTypes: ['request'], status: FETCH_STATUS.SUCCESS });
|
||||
const { queryByText, getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingLatencyChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
serviceName={serviceName}
|
||||
alert={MOCK_ALERT}
|
||||
transactionType="custom"
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(queryByText('custom')).not.toBeInTheDocument();
|
||||
expect(getByText('request')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows latency aggregation type select', async () => {
|
||||
jest
|
||||
.spyOn(transactionFetcher, 'useServiceTransactionTypesFetcher')
|
||||
.mockReturnValue({ transactionTypes: ['request'], status: FETCH_STATUS.SUCCESS });
|
||||
const { getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingLatencyChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
serviceName={serviceName}
|
||||
alert={MOCK_ALERT}
|
||||
transactionType="custom"
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Metric')).toBeInTheDocument();
|
||||
expect(getByText('Average')).toBeInTheDocument();
|
||||
expect(getByText('95th percentile')).toBeInTheDocument();
|
||||
expect(getByText('99th percentile')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import LatencyChart from '../../../components/alerting/ui_components/alert_details_app_section/latency_chart';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { useAlertingProps } from '../use_alerting_props';
|
||||
import { TimeRangeCallout } from '../time_range_callout';
|
||||
import type { EmbeddableApmAlertingLatencyVizProps } from '../types';
|
||||
import { ServiceNameCallout } from '../service_name_callout';
|
||||
|
||||
export function APMAlertingLatencyChart({
|
||||
rule,
|
||||
alert,
|
||||
serviceName,
|
||||
environment = ENVIRONMENT_ALL.value,
|
||||
transactionType,
|
||||
transactionName,
|
||||
rangeFrom = 'now-15m',
|
||||
rangeTo = 'now',
|
||||
latencyThresholdInMicroseconds,
|
||||
kuery = '',
|
||||
filters,
|
||||
}: EmbeddableApmAlertingLatencyVizProps) {
|
||||
const {
|
||||
transactionType: currentTransactionType,
|
||||
transactionTypes,
|
||||
setTransactionType,
|
||||
comparisonChartTheme,
|
||||
latencyAggregationType,
|
||||
setLatencyAggregationType,
|
||||
timeZone,
|
||||
} = useAlertingProps({
|
||||
rule,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
kuery,
|
||||
serviceName,
|
||||
defaultTransactionType: transactionType,
|
||||
});
|
||||
|
||||
if (!rangeFrom || !rangeTo) {
|
||||
return <TimeRangeCallout />;
|
||||
}
|
||||
|
||||
if (!serviceName || !currentTransactionType) {
|
||||
return <ServiceNameCallout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<LatencyChart
|
||||
alert={alert}
|
||||
transactionType={currentTransactionType}
|
||||
transactionTypes={transactionTypes}
|
||||
transactionName={transactionName}
|
||||
serviceName={serviceName}
|
||||
environment={environment}
|
||||
start={rangeFrom}
|
||||
end={rangeTo}
|
||||
comparisonChartTheme={comparisonChartTheme}
|
||||
timeZone={timeZone}
|
||||
latencyAggregationType={latencyAggregationType}
|
||||
setLatencyAggregationType={setLatencyAggregationType}
|
||||
setTransactionType={setTransactionType}
|
||||
comparisonEnabled={false}
|
||||
offset={''}
|
||||
customAlertEvaluationThreshold={latencyThresholdInMicroseconds}
|
||||
kuery={kuery}
|
||||
filters={filters}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { initializeTitles, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { EmbeddableApmAlertingLatencyVizProps } from '../types';
|
||||
import type { EmbeddableDeps } from '../../types';
|
||||
import { ApmEmbeddableContext } from '../../embeddable_context';
|
||||
import { APMAlertingLatencyChart } from './chart';
|
||||
|
||||
export const APM_ALERTING_LATENCY_CHART_EMBEDDABLE = 'APM_ALERTING_LATENCY_CHART_EMBEDDABLE';
|
||||
|
||||
export const getApmAlertingLatencyChartEmbeddableFactory = (deps: EmbeddableDeps) => {
|
||||
const factory: ReactEmbeddableFactory<
|
||||
EmbeddableApmAlertingLatencyVizProps,
|
||||
DefaultEmbeddableApi<EmbeddableApmAlertingLatencyVizProps>
|
||||
> = {
|
||||
type: APM_ALERTING_LATENCY_CHART_EMBEDDABLE,
|
||||
deserializeState: (state) => {
|
||||
return state.rawState as EmbeddableApmAlertingLatencyVizProps;
|
||||
},
|
||||
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
|
||||
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
|
||||
const serviceName$ = new BehaviorSubject(state.serviceName);
|
||||
const transactionType$ = new BehaviorSubject(state.transactionType);
|
||||
const transactionName$ = new BehaviorSubject(state.transactionName);
|
||||
const environment$ = new BehaviorSubject(state.environment);
|
||||
const latencyThresholdInMicroseconds$ = new BehaviorSubject(
|
||||
state.latencyThresholdInMicroseconds
|
||||
);
|
||||
const rangeFrom$ = new BehaviorSubject(state.rangeFrom);
|
||||
const rangeTo$ = new BehaviorSubject(state.rangeTo);
|
||||
const rule$ = new BehaviorSubject(state.rule);
|
||||
const alert$ = new BehaviorSubject(state.alert);
|
||||
const kuery$ = new BehaviorSubject(state.kuery);
|
||||
const filters$ = new BehaviorSubject(state.filters);
|
||||
|
||||
const api = buildApi(
|
||||
{
|
||||
...titlesApi,
|
||||
serializeState: () => {
|
||||
return {
|
||||
rawState: {
|
||||
...serializeTitles(),
|
||||
serviceName: serviceName$.getValue(),
|
||||
transactionType: transactionType$.getValue(),
|
||||
transactionName: transactionName$.getValue(),
|
||||
environment: environment$.getValue(),
|
||||
latencyThresholdInMicroseconds: latencyThresholdInMicroseconds$.getValue(),
|
||||
rangeFrom: rangeFrom$.getValue(),
|
||||
rangeTo: rangeTo$.getValue(),
|
||||
rule: rule$.getValue(),
|
||||
alert: alert$.getValue(),
|
||||
kuery: kuery$.getValue(),
|
||||
filters: filters$.getValue(),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: [serviceName$, (value) => serviceName$.next(value)],
|
||||
transactionType: [transactionType$, (value) => transactionType$.next(value)],
|
||||
transactionName: [transactionName$, (value) => transactionName$.next(value)],
|
||||
environment: [environment$, (value) => environment$.next(value)],
|
||||
latencyThresholdInMicroseconds: [
|
||||
latencyThresholdInMicroseconds$,
|
||||
(value) => latencyThresholdInMicroseconds$.next(value),
|
||||
],
|
||||
rangeFrom: [rangeFrom$, (value) => rangeFrom$.next(value)],
|
||||
rangeTo: [rangeTo$, (value) => rangeTo$.next(value)],
|
||||
rule: [rule$, (value) => rule$.next(value)],
|
||||
alert: [alert$, (value) => alert$.next(value)],
|
||||
kuery: [kuery$, (value) => kuery$.next(value)],
|
||||
filters: [filters$, (value) => filters$.next(value)],
|
||||
...titleComparators,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
const [
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
environment,
|
||||
latencyThresholdInMicroseconds,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
rule,
|
||||
alert,
|
||||
kuery,
|
||||
filters,
|
||||
] = useBatchedPublishingSubjects(
|
||||
serviceName$,
|
||||
transactionType$,
|
||||
transactionName$,
|
||||
environment$,
|
||||
latencyThresholdInMicroseconds$,
|
||||
rangeFrom$,
|
||||
rangeTo$,
|
||||
rule$,
|
||||
alert$,
|
||||
kuery$,
|
||||
filters$
|
||||
);
|
||||
|
||||
return (
|
||||
<ApmEmbeddableContext deps={deps} rangeFrom={rangeFrom} rangeTo={rangeTo}>
|
||||
<APMAlertingLatencyChart
|
||||
rule={rule}
|
||||
alert={alert}
|
||||
latencyThresholdInMicroseconds={latencyThresholdInMicroseconds}
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
environment={environment}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
transactionName={transactionName}
|
||||
kuery={kuery}
|
||||
filters={filters}
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
return factory;
|
||||
};
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { APMAlertingThroughputChart } from './chart';
|
||||
import { ApmEmbeddableContext } from '../../embeddable_context';
|
||||
import { MOCK_ALERT, MOCK_RULE, MOCK_DEPS } from '../testing/fixtures';
|
||||
import * as transactionFetcher from '../../../context/apm_service/use_service_transaction_types_fetcher';
|
||||
|
||||
jest.mock('../../../context/apm_service/use_service_agent_fetcher', () => ({
|
||||
useServiceAgentFetcher: jest.fn(() => ({
|
||||
agentName: 'mockAgent',
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('renders chart', () => {
|
||||
const serviceName = 'ops-bean';
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(transactionFetcher, 'useServiceTransactionTypesFetcher')
|
||||
.mockReturnValue({ transactionTypes: ['request'], status: FETCH_STATUS.SUCCESS });
|
||||
});
|
||||
|
||||
it('renders error when serviceName is not defined', async () => {
|
||||
const { getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingThroughputChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
// @ts-ignore
|
||||
serviceName={undefined}
|
||||
alert={MOCK_ALERT}
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Unable to load the APM visualizations.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders when serviceName is defined', async () => {
|
||||
const { getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingThroughputChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
serviceName={serviceName}
|
||||
alert={MOCK_ALERT}
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Throughput')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('supports custom transactionType when transactionType is included in transaction types list', async () => {
|
||||
jest
|
||||
.spyOn(transactionFetcher, 'useServiceTransactionTypesFetcher')
|
||||
.mockReturnValue({ transactionTypes: ['request', 'custom'], status: FETCH_STATUS.SUCCESS });
|
||||
const { getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingThroughputChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
serviceName={serviceName}
|
||||
alert={MOCK_ALERT}
|
||||
transactionType="custom"
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('custom')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not support custom transactionType when transactionType is not included in transaction types list', async () => {
|
||||
jest
|
||||
.spyOn(transactionFetcher, 'useServiceTransactionTypesFetcher')
|
||||
.mockReturnValue({ transactionTypes: ['request'], status: FETCH_STATUS.SUCCESS });
|
||||
const { queryByText, getByText } = render(
|
||||
<ApmEmbeddableContext deps={MOCK_DEPS}>
|
||||
<APMAlertingThroughputChart
|
||||
rule={MOCK_RULE}
|
||||
rangeFrom="now-15m"
|
||||
rangeTo="now"
|
||||
serviceName={serviceName}
|
||||
alert={MOCK_ALERT}
|
||||
transactionType="custom"
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(queryByText('custom')).not.toBeInTheDocument();
|
||||
expect(getByText('request')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import ThroughputChart from '../../../components/alerting/ui_components/alert_details_app_section/throughput_chart';
|
||||
import { EmbeddableApmAlertingVizProps } from '../types';
|
||||
import { useAlertingProps } from '../use_alerting_props';
|
||||
import { TimeRangeCallout } from '../time_range_callout';
|
||||
import { ServiceNameCallout } from '../service_name_callout';
|
||||
|
||||
export function APMAlertingThroughputChart({
|
||||
rule,
|
||||
rangeFrom = 'now-15m',
|
||||
rangeTo = 'now',
|
||||
transactionName,
|
||||
kuery,
|
||||
filters,
|
||||
serviceName,
|
||||
transactionType,
|
||||
environment = ENVIRONMENT_ALL.value,
|
||||
}: EmbeddableApmAlertingVizProps) {
|
||||
const {
|
||||
comparisonChartTheme,
|
||||
setTransactionType,
|
||||
transactionType: currentTransactionType,
|
||||
transactionTypes,
|
||||
timeZone,
|
||||
} = useAlertingProps({
|
||||
rule,
|
||||
rangeTo,
|
||||
rangeFrom,
|
||||
defaultTransactionType: transactionType,
|
||||
serviceName,
|
||||
});
|
||||
|
||||
if (!rangeFrom || !rangeTo) {
|
||||
return <TimeRangeCallout />;
|
||||
}
|
||||
|
||||
if (!serviceName || !currentTransactionType) {
|
||||
return <ServiceNameCallout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThroughputChart
|
||||
transactionType={currentTransactionType}
|
||||
transactionTypes={transactionTypes}
|
||||
setTransactionType={setTransactionType}
|
||||
transactionName={transactionName}
|
||||
serviceName={serviceName}
|
||||
environment={environment}
|
||||
start={rangeFrom}
|
||||
end={rangeTo}
|
||||
comparisonChartTheme={comparisonChartTheme}
|
||||
timeZone={timeZone}
|
||||
comparisonEnabled={false}
|
||||
offset={''}
|
||||
kuery={kuery}
|
||||
filters={filters}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { initializeTitles, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { EmbeddableApmAlertingVizProps } from '../types';
|
||||
import type { EmbeddableDeps } from '../../types';
|
||||
import { ApmEmbeddableContext } from '../../embeddable_context';
|
||||
import { APMAlertingThroughputChart } from './chart';
|
||||
|
||||
export const APM_ALERTING_THROUGHPUT_CHART_EMBEDDABLE = 'APM_ALERTING_THROUGHPUT_CHART_EMBEDDABLE';
|
||||
|
||||
export const getApmAlertingThroughputChartEmbeddableFactory = (deps: EmbeddableDeps) => {
|
||||
const factory: ReactEmbeddableFactory<
|
||||
EmbeddableApmAlertingVizProps,
|
||||
DefaultEmbeddableApi<EmbeddableApmAlertingVizProps>
|
||||
> = {
|
||||
type: APM_ALERTING_THROUGHPUT_CHART_EMBEDDABLE,
|
||||
deserializeState: (state) => {
|
||||
return state.rawState as EmbeddableApmAlertingVizProps;
|
||||
},
|
||||
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
|
||||
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
|
||||
const serviceName$ = new BehaviorSubject(state.serviceName);
|
||||
const transactionType$ = new BehaviorSubject(state.transactionType);
|
||||
const transactionName$ = new BehaviorSubject(state.transactionName);
|
||||
const environment$ = new BehaviorSubject(state.environment);
|
||||
const rangeFrom$ = new BehaviorSubject(state.rangeFrom);
|
||||
const rangeTo$ = new BehaviorSubject(state.rangeTo);
|
||||
const rule$ = new BehaviorSubject(state.rule);
|
||||
const alert$ = new BehaviorSubject(state.alert);
|
||||
const kuery$ = new BehaviorSubject(state.kuery);
|
||||
const filters$ = new BehaviorSubject(state.filters);
|
||||
|
||||
const api = buildApi(
|
||||
{
|
||||
...titlesApi,
|
||||
serializeState: () => {
|
||||
return {
|
||||
rawState: {
|
||||
...serializeTitles(),
|
||||
serviceName: serviceName$.getValue(),
|
||||
transactionType: transactionType$.getValue(),
|
||||
transactionName: transactionName$.getValue(),
|
||||
environment: environment$.getValue(),
|
||||
rangeFrom: rangeFrom$.getValue(),
|
||||
rangeTo: rangeTo$.getValue(),
|
||||
rule: rule$.getValue(),
|
||||
alert: alert$.getValue(),
|
||||
kuery: kuery$.getValue(),
|
||||
filters: filters$.getValue(),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: [serviceName$, (value) => serviceName$.next(value)],
|
||||
transactionType: [transactionType$, (value) => transactionType$.next(value)],
|
||||
transactionName: [transactionName$, (value) => transactionName$.next(value)],
|
||||
environment: [environment$, (value) => environment$.next(value)],
|
||||
rangeFrom: [rangeFrom$, (value) => rangeFrom$.next(value)],
|
||||
rangeTo: [rangeTo$, (value) => rangeTo$.next(value)],
|
||||
rule: [rule$, (value) => rule$.next(value)],
|
||||
alert: [alert$, (value) => alert$.next(value)],
|
||||
kuery: [kuery$, (value) => kuery$.next(value)],
|
||||
filters: [filters$, (value) => filters$.next(value)],
|
||||
...titleComparators,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
const [
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
environment,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
rule,
|
||||
alert,
|
||||
kuery,
|
||||
filters,
|
||||
] = useBatchedPublishingSubjects(
|
||||
serviceName$,
|
||||
transactionType$,
|
||||
transactionName$,
|
||||
environment$,
|
||||
rangeFrom$,
|
||||
rangeTo$,
|
||||
rule$,
|
||||
alert$,
|
||||
kuery$,
|
||||
filters$
|
||||
);
|
||||
|
||||
return (
|
||||
<ApmEmbeddableContext deps={deps} rangeFrom={rangeFrom} rangeTo={rangeTo}>
|
||||
<APMAlertingThroughputChart
|
||||
rule={rule}
|
||||
alert={alert}
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
environment={environment}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
transactionName={transactionName}
|
||||
kuery={kuery}
|
||||
filters={filters}
|
||||
/>
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
return factory;
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
export function ServiceNameCallout() {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.apm.alertingEmbeddables.serviceName.error.toastTitle"
|
||||
defaultMessage="An error occurred when identifying the APM service name or transaction type."
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.alertingEmbeddables.serviceName.error.toastDescription"
|
||||
defaultMessage="Unable to load the APM visualizations."
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { TopAlert } from '@kbn/observability-plugin/public';
|
||||
import { Rule } from '@kbn/alerting-plugin/common';
|
||||
import { ApmEmbeddableContextProps } from '../../embeddable_context';
|
||||
import { mockApmPluginContextValue } from '../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
|
||||
export const MOCK_DEPS: ApmEmbeddableContextProps['deps'] = {
|
||||
pluginsSetup: mockApmPluginContextValue.plugins,
|
||||
pluginsStart: mockApmPluginContextValue.corePlugins,
|
||||
coreSetup: mockApmPluginContextValue.core,
|
||||
coreStart: mockApmPluginContextValue.core,
|
||||
} as unknown as ApmEmbeddableContextProps['deps'];
|
||||
export const MOCK_RULE = {
|
||||
ruleTypeId: 'slo.rules.burnRate',
|
||||
params: {
|
||||
sloId: '84ef850b-ea68-4bff-a3c8-dd522ca80f1c',
|
||||
windows: [
|
||||
{
|
||||
id: '481d7b88-bbc9-49c8-ae19-bf5ab9da0cf8',
|
||||
burnRateThreshold: 14.4,
|
||||
maxBurnRateThreshold: 720,
|
||||
longWindow: {
|
||||
value: 1,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 5,
|
||||
unit: 'm',
|
||||
},
|
||||
actionGroup: 'slo.burnRate.alert',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as Rule<never>;
|
||||
export const MOCK_ALERT = {
|
||||
link: '/app/slos/84ef850b-ea68-4bff-a3c8-dd522ca80f1c?instanceId=*',
|
||||
reason:
|
||||
'LOW: The burn rate for the past 72h is 70.54 and for the past 360m is 83.33. Alert when above 1 for both windows',
|
||||
fields: {
|
||||
'kibana.alert.reason':
|
||||
'LOW: The burn rate for the past 72h is 70.54 and for the past 360m is 83.33. Alert when above 1 for both windows',
|
||||
'kibana.alert.rule.category': 'SLO burn rate',
|
||||
'kibana.alert.rule.consumer': 'slo',
|
||||
'kibana.alert.rule.execution.uuid': '3560f989-f065-49af-9f1f-51b4d6cefc11',
|
||||
'kibana.alert.rule.name': 'APM SLO with a specific transaction name Burn Rate rule',
|
||||
'kibana.alert.rule.parameters': {},
|
||||
'kibana.alert.rule.producer': 'slo',
|
||||
'kibana.alert.rule.revision': 0,
|
||||
'kibana.alert.rule.rule_type_id': 'slo.rules.burnRate',
|
||||
'kibana.alert.duration.us': 23272406000,
|
||||
'kibana.alert.start': '2024-04-30T01:49:41.050Z',
|
||||
'kibana.alert.time_range': {
|
||||
gte: '2024-04-30T01:49:41.050Z',
|
||||
lte: '2024-04-30T08:17:33.456Z',
|
||||
},
|
||||
'kibana.version': '8.15.0',
|
||||
tags: [],
|
||||
'kibana.alert.end': '2024-04-30T08:17:33.456Z',
|
||||
'kibana.alert.evaluation.threshold': 1,
|
||||
'kibana.alert.evaluation.value': 70.53571428571422,
|
||||
'slo.id': '84ef850b-ea68-4bff-a3c8-dd522ca80f1c',
|
||||
'slo.revision': 1,
|
||||
'slo.instanceId': '*',
|
||||
},
|
||||
active: false,
|
||||
start: 1714441781050,
|
||||
lastUpdated: 1714483111432,
|
||||
} as unknown as TopAlert;
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
export function TimeRangeCallout() {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.apm.alertingEmbeddables.timeRange.error.toastTitle"
|
||||
defaultMessage="An error occurred when identifying the alert time range."
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.alertingEmbeddables.timeRange.toastDescription"
|
||||
defaultMessage="Unable to load the alert details page's charts. Please try to refresh the page if the alert is newly created"
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { Rule } from '@kbn/alerting-plugin/common';
|
||||
import type { TopAlert } from '@kbn/observability-plugin/public';
|
||||
import { SerializedTitles } from '@kbn/presentation-publishing';
|
||||
import type { BoolQuery } from '@kbn/es-query';
|
||||
|
||||
export interface EmbeddableApmAlertingVizProps extends SerializedTitles {
|
||||
rule: Rule;
|
||||
alert: TopAlert;
|
||||
transactionName?: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
kuery?: string;
|
||||
filters?: BoolQuery;
|
||||
serviceName: string;
|
||||
environment?: string;
|
||||
transactionType?: string;
|
||||
}
|
||||
|
||||
export interface EmbeddableApmAlertingLatencyVizProps extends EmbeddableApmAlertingVizProps {
|
||||
latencyThresholdInMicroseconds?: number;
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { useState, useEffect } from 'react';
|
||||
import { Rule } from '@kbn/alerting-plugin/common';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { getTransactionType } from '../../context/apm_service/apm_service_context';
|
||||
import { useServiceTransactionTypesFetcher } from '../../context/apm_service/use_service_transaction_types_fetcher';
|
||||
import { useServiceAgentFetcher } from '../../context/apm_service/use_service_agent_fetcher';
|
||||
import { usePreferredDataSourceAndBucketSize } from '../../hooks/use_preferred_data_source_and_bucket_size';
|
||||
import { useTimeRange } from '../../hooks/use_time_range';
|
||||
import { getComparisonChartTheme } from '../../components/shared/time_comparison/get_comparison_chart_theme';
|
||||
import { getAggsTypeFromRule } from '../../components/alerting/ui_components/alert_details_app_section/helpers';
|
||||
import { getTimeZone } from '../../components/shared/charts/helper/timezone';
|
||||
import { ApmDocumentType } from '../../../common/document_type';
|
||||
import type { LatencyAggregationType } from '../../../common/latency_aggregation_types';
|
||||
|
||||
export function useAlertingProps({
|
||||
rule,
|
||||
serviceName,
|
||||
kuery = '',
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
defaultTransactionType,
|
||||
}: {
|
||||
rule: Rule<{ aggregationType: LatencyAggregationType }>;
|
||||
serviceName: string;
|
||||
kuery?: string;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
defaultTransactionType?: string;
|
||||
}) {
|
||||
const {
|
||||
services: { uiSettings },
|
||||
} = useKibana();
|
||||
|
||||
const timeZone = getTimeZone(uiSettings);
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const preferred = usePreferredDataSourceAndBucketSize({
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
type: ApmDocumentType.TransactionMetric,
|
||||
numBuckets: 100,
|
||||
});
|
||||
const { transactionTypes } = useServiceTransactionTypesFetcher({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
documentType: preferred?.source.documentType,
|
||||
rollupInterval: preferred?.source.rollupInterval,
|
||||
});
|
||||
const { agentName } = useServiceAgentFetcher({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
const currentTransactionType = getTransactionType({
|
||||
transactionTypes,
|
||||
transactionType: defaultTransactionType,
|
||||
agentName,
|
||||
});
|
||||
|
||||
const params = rule.params;
|
||||
const comparisonChartTheme = getComparisonChartTheme();
|
||||
|
||||
const [latencyAggregationType, setLatencyAggregationType] = useState(
|
||||
getAggsTypeFromRule(params.aggregationType)
|
||||
);
|
||||
const [transactionType, setTransactionType] = useState(currentTransactionType);
|
||||
|
||||
useEffect(() => {
|
||||
setTransactionType(currentTransactionType);
|
||||
}, [currentTransactionType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultTransactionType) {
|
||||
setTransactionType(defaultTransactionType);
|
||||
}
|
||||
}, [defaultTransactionType]);
|
||||
|
||||
return {
|
||||
transactionType,
|
||||
transactionTypes,
|
||||
setTransactionType,
|
||||
latencyAggregationType,
|
||||
setLatencyAggregationType,
|
||||
comparisonChartTheme,
|
||||
timeZone,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||
import { ApmPluginContext, ApmPluginContextValue } from '../context/apm_plugin/apm_plugin_context';
|
||||
import { createCallApmApi } from '../services/rest/create_call_apm_api';
|
||||
import { ApmThemeProvider } from '../components/routing/app_root';
|
||||
import { ChartPointerEventContextProvider } from '../context/chart_pointer_event/chart_pointer_event_context';
|
||||
import { EmbeddableDeps } from './types';
|
||||
import { TimeRangeMetadataContextProvider } from '../context/time_range_metadata/time_range_metadata_context';
|
||||
|
||||
export interface ApmEmbeddableContextProps {
|
||||
deps: EmbeddableDeps;
|
||||
children: React.ReactNode;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
kuery?: string;
|
||||
}
|
||||
|
||||
export function ApmEmbeddableContext({
|
||||
rangeFrom = 'now-15m',
|
||||
rangeTo = 'now',
|
||||
kuery = '',
|
||||
deps,
|
||||
children,
|
||||
}: ApmEmbeddableContextProps) {
|
||||
const services = {
|
||||
config: deps.config,
|
||||
core: deps.coreStart,
|
||||
plugins: deps.pluginsSetup,
|
||||
data: deps.pluginsStart.data,
|
||||
inspector: deps.pluginsStart.inspector,
|
||||
observability: deps.pluginsStart.observability,
|
||||
observabilityShared: deps.pluginsStart.observabilityShared,
|
||||
dataViews: deps.pluginsStart.dataViews,
|
||||
unifiedSearch: deps.pluginsStart.unifiedSearch,
|
||||
lens: deps.pluginsStart.lens,
|
||||
uiActions: deps.pluginsStart.uiActions,
|
||||
observabilityAIAssistant: deps.pluginsStart.observabilityAIAssistant,
|
||||
share: deps.pluginsSetup.share,
|
||||
kibanaEnvironment: deps.kibanaEnvironment,
|
||||
observabilityRuleTypeRegistry: deps.observabilityRuleTypeRegistry,
|
||||
} as ApmPluginContextValue;
|
||||
|
||||
createCallApmApi(deps.coreStart);
|
||||
|
||||
const I18nContext = deps.coreStart.i18n.Context;
|
||||
return (
|
||||
<I18nContext>
|
||||
<ApmPluginContext.Provider value={services}>
|
||||
<KibanaThemeProvider theme={deps.coreStart.theme}>
|
||||
<ApmThemeProvider>
|
||||
<KibanaContextProvider services={deps.coreStart}>
|
||||
<TimeRangeMetadataContextProvider
|
||||
uiSettings={deps.coreStart.uiSettings}
|
||||
start={rangeFrom}
|
||||
end={rangeTo}
|
||||
kuery={kuery}
|
||||
useSpanName={false}
|
||||
>
|
||||
<ChartPointerEventContextProvider>{children}</ChartPointerEventContextProvider>
|
||||
</TimeRangeMetadataContextProvider>
|
||||
</KibanaContextProvider>
|
||||
</ApmThemeProvider>
|
||||
</KibanaThemeProvider>
|
||||
</ApmPluginContext.Provider>
|
||||
</I18nContext>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { CoreSetup } from '@kbn/core/public';
|
||||
|
||||
import { ApmPluginStartDeps, ApmPluginStart } from '../plugin';
|
||||
import { EmbeddableDeps } from './types';
|
||||
|
||||
export async function registerEmbeddables(
|
||||
deps: Omit<EmbeddableDeps, 'coreStart' | 'pluginsStart'>
|
||||
) {
|
||||
const coreSetup = deps.coreSetup as CoreSetup<ApmPluginStartDeps, ApmPluginStart>;
|
||||
const pluginsSetup = deps.pluginsSetup;
|
||||
const [coreStart, pluginsStart] = await coreSetup.getStartServices();
|
||||
const registerReactEmbeddableFactory = pluginsSetup.embeddable.registerReactEmbeddableFactory;
|
||||
const registerApmAlertingLatencyChartEmbeddable = async () => {
|
||||
const { getApmAlertingLatencyChartEmbeddableFactory, APM_ALERTING_LATENCY_CHART_EMBEDDABLE } =
|
||||
await import('./alerting/alerting_latency_chart/react_embeddable_factory');
|
||||
registerReactEmbeddableFactory(APM_ALERTING_LATENCY_CHART_EMBEDDABLE, async () => {
|
||||
return getApmAlertingLatencyChartEmbeddableFactory({ ...deps, coreStart, pluginsStart });
|
||||
});
|
||||
};
|
||||
const registerApmAlertingThroughputChartEmbeddable = async () => {
|
||||
const {
|
||||
getApmAlertingThroughputChartEmbeddableFactory,
|
||||
APM_ALERTING_THROUGHPUT_CHART_EMBEDDABLE,
|
||||
} = await import('./alerting/alerting_throughput_chart/react_embeddable_factory');
|
||||
registerReactEmbeddableFactory(APM_ALERTING_THROUGHPUT_CHART_EMBEDDABLE, async () => {
|
||||
return getApmAlertingThroughputChartEmbeddableFactory({ ...deps, coreStart, pluginsStart });
|
||||
});
|
||||
};
|
||||
const registerApmAlertingFailedTransactionsChartEmbeddable = async () => {
|
||||
const {
|
||||
getApmAlertingFailedTransactionsChartEmbeddableFactory,
|
||||
APM_ALERTING_FAILED_TRANSACTIONS_CHART_EMBEDDABLE,
|
||||
} = await import('./alerting/alerting_failed_transactions_chart/react_embeddable_factory');
|
||||
registerReactEmbeddableFactory(APM_ALERTING_FAILED_TRANSACTIONS_CHART_EMBEDDABLE, async () => {
|
||||
return getApmAlertingFailedTransactionsChartEmbeddableFactory({
|
||||
...deps,
|
||||
coreStart,
|
||||
pluginsStart,
|
||||
});
|
||||
});
|
||||
};
|
||||
registerApmAlertingLatencyChartEmbeddable();
|
||||
registerApmAlertingThroughputChartEmbeddable();
|
||||
registerApmAlertingFailedTransactionsChartEmbeddable();
|
||||
}
|
|
@ -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 type { EmbeddableInput } from '@kbn/embeddable-plugin/public';
|
||||
import type { CoreStart, CoreSetup } from '@kbn/core/public';
|
||||
import type { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public';
|
||||
import type { ApmPluginStartDeps, ApmPluginSetupDeps } from '../plugin';
|
||||
import type { ConfigSchema } from '..';
|
||||
import type { KibanaEnvContext } from '../context/kibana_environment_context/kibana_environment_context';
|
||||
export interface EmbeddableDeps {
|
||||
coreStart: CoreStart;
|
||||
pluginsStart: ApmPluginStartDeps;
|
||||
coreSetup: CoreSetup;
|
||||
pluginsSetup: ApmPluginSetupDeps;
|
||||
config: ConfigSchema;
|
||||
kibanaEnvironment: KibanaEnvContext;
|
||||
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
|
||||
}
|
||||
export interface APMEmbeddableProps {
|
||||
transactionName?: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
kuery?: string;
|
||||
}
|
||||
|
||||
export type APMEmbeddableInput = EmbeddableInput & APMEmbeddableProps;
|
|
@ -21,7 +21,7 @@ import {
|
|||
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public/plugin';
|
||||
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import type { ExploratoryViewPublicSetup } from '@kbn/exploratory-view-plugin/public';
|
||||
import type { FeaturesPluginSetup } from '@kbn/features-plugin/public';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
|
@ -69,6 +69,7 @@ import { map } from 'rxjs';
|
|||
import type { CloudSetup } from '@kbn/cloud-plugin/public';
|
||||
import type { ConfigSchema } from '.';
|
||||
import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types';
|
||||
import { registerEmbeddables } from './embeddable/register_embeddables';
|
||||
import {
|
||||
getApmEnrollmentFlyoutData,
|
||||
LazyApmCustomAssetsExtension,
|
||||
|
@ -87,6 +88,7 @@ export interface ApmPluginSetupDeps {
|
|||
alerting?: AlertingPluginPublicSetup;
|
||||
data: DataPublicPluginSetup;
|
||||
discover?: DiscoverSetup;
|
||||
embeddable: EmbeddableSetup;
|
||||
exploratoryView: ExploratoryViewPublicSetup;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
features: FeaturesPluginSetup;
|
||||
|
@ -325,6 +327,14 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
|
|||
// Register APM telemetry based events
|
||||
const telemetry = this.telemetry.start();
|
||||
|
||||
const isCloudEnv = !!pluginSetupDeps.cloud?.isCloudEnabled;
|
||||
const isServerlessEnv = pluginSetupDeps.cloud?.isServerlessEnabled || this.isServerlessEnv;
|
||||
const kibanaEnvironment = {
|
||||
isCloudEnv,
|
||||
isServerlessEnv,
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
};
|
||||
|
||||
core.application.register({
|
||||
id: 'apm',
|
||||
title: 'APM',
|
||||
|
@ -371,18 +381,12 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
|
|||
import('./application'),
|
||||
core.getStartServices(),
|
||||
]);
|
||||
const isCloudEnv = !!pluginSetupDeps.cloud?.isCloudEnabled;
|
||||
const isServerlessEnv = pluginSetupDeps.cloud?.isServerlessEnabled || this.isServerlessEnv;
|
||||
return renderApp({
|
||||
coreStart,
|
||||
pluginsSetup: pluginSetupDeps as ApmPluginSetupDeps,
|
||||
appMountParameters,
|
||||
config,
|
||||
kibanaEnvironment: {
|
||||
isCloudEnv,
|
||||
isServerlessEnv,
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
},
|
||||
kibanaEnvironment,
|
||||
pluginsStart: pluginsStart as ApmPluginStartDeps,
|
||||
observabilityRuleTypeRegistry,
|
||||
apmServices: {
|
||||
|
@ -393,6 +397,13 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
|
|||
});
|
||||
|
||||
registerApmRuleTypes(observabilityRuleTypeRegistry);
|
||||
registerEmbeddables({
|
||||
coreSetup: core,
|
||||
pluginsSetup: plugins,
|
||||
config,
|
||||
kibanaEnvironment,
|
||||
observabilityRuleTypeRegistry,
|
||||
});
|
||||
|
||||
const locator = plugins.share.url.locators.create(new APMServiceDetailLocator(core.uiSettings));
|
||||
|
||||
|
|
|
@ -113,8 +113,10 @@
|
|||
"@kbn/shared-ux-utility",
|
||||
"@kbn/management-settings-components-field-row",
|
||||
"@kbn/shared-ux-markdown",
|
||||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/core-http-request-handler-context-server",
|
||||
"@kbn/search-types",
|
||||
"@kbn/presentation-publishing",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"data",
|
||||
"dataViews",
|
||||
"dataViewEditor",
|
||||
"embeddable",
|
||||
"fieldFormats",
|
||||
"uiActions",
|
||||
"presentationUtil",
|
||||
|
|
|
@ -4,21 +4,16 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiLink } from '@elastic/eui';
|
||||
import { Rule } from '@kbn/alerting-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect } from 'react';
|
||||
import { AlertSummaryField, TopAlert } from '@kbn/observability-plugin/public';
|
||||
import { AlertSummaryField } from '@kbn/observability-plugin/public';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { useFetchSloDetails } from '../../../../hooks/use_fetch_slo_details';
|
||||
import { BurnRateRuleParams } from '../../../../typings/slo';
|
||||
import { AlertsHistoryPanel } from './components/alerts_history/alerts_history_panel';
|
||||
import { ErrorRatePanel } from './components/error_rate/error_rate_panel';
|
||||
import { CustomAlertDetailsPanel } from './components/custom_panels/custom_panels';
|
||||
|
||||
export type BurnRateRule = Rule<BurnRateRuleParams>;
|
||||
export type BurnRateAlert = TopAlert;
|
||||
import { BurnRateAlert, BurnRateRule } from './types';
|
||||
|
||||
interface AppSectionProps {
|
||||
alert: BurnRateAlert;
|
||||
|
|
|
@ -30,7 +30,7 @@ import { convertTo } from '@kbn/observability-plugin/public';
|
|||
import { useKibana } from '../../../../../../utils/kibana_react';
|
||||
import { WindowSchema } from '../../../../../../typings';
|
||||
import { ErrorRateChart } from '../../../../error_rate_chart';
|
||||
import { BurnRateAlert, BurnRateRule } from '../../alert_details_app_section';
|
||||
import { BurnRateAlert, BurnRateRule } from '../../types';
|
||||
import { getActionGroupFromReason } from '../../utils/alert';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { GetSLOResponse, APMTransactionDurationIndicator } from '@kbn/slo-schema';
|
||||
import { APMEmbeddableRoot } from './embeddable_root';
|
||||
import type { BurnRateRule, BurnRateAlert, TimeRange } from '../../../types';
|
||||
|
||||
interface APMAlertDetailsProps {
|
||||
slo: APMTransactionDurationSLOResponse;
|
||||
alert: BurnRateAlert;
|
||||
rule: BurnRateRule;
|
||||
dataTimeRange: TimeRange;
|
||||
}
|
||||
|
||||
export type APMTransactionDurationSLOResponse = GetSLOResponse & {
|
||||
indicator: APMTransactionDurationIndicator;
|
||||
};
|
||||
|
||||
export function APMAlertDetails({ slo, dataTimeRange, alert, rule }: APMAlertDetailsProps) {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" data-test-subj="overviewSection">
|
||||
<APMEmbeddableRoot
|
||||
slo={slo}
|
||||
dataTimeRange={dataTimeRange}
|
||||
embeddableId={'APM_ALERTING_LATENCY_CHART_EMBEDDABLE'}
|
||||
alert={alert}
|
||||
rule={rule}
|
||||
/>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<APMEmbeddableRoot
|
||||
slo={slo}
|
||||
dataTimeRange={dataTimeRange}
|
||||
embeddableId={'APM_ALERTING_THROUGHPUT_CHART_EMBEDDABLE'}
|
||||
alert={alert}
|
||||
rule={rule}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<APMEmbeddableRoot
|
||||
slo={slo}
|
||||
dataTimeRange={dataTimeRange}
|
||||
embeddableId={'APM_ALERTING_FAILED_TRANSACTIONS_CHART_EMBEDDABLE'}
|
||||
alert={alert}
|
||||
rule={rule}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { buildQueryFromFilters, Filter } from '@kbn/es-query';
|
||||
import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
|
||||
import { GetSLOResponse, APMTransactionDurationIndicator } from '@kbn/slo-schema';
|
||||
import type { BurnRateAlert, BurnRateRule, TimeRange } from '../../../types';
|
||||
|
||||
type EmbeddableId =
|
||||
| 'APM_THROUGHPUT_CHART_EMBEDDABLE'
|
||||
| 'APM_LATENCY_CHART_EMBEDDABLE'
|
||||
| 'APM_ALERTING_FAILED_TRANSACTIONS_CHART_EMBEDDABLE'
|
||||
| 'APM_ALERTING_LATENCY_CHART_EMBEDDABLE'
|
||||
| 'APM_ALERTING_THROUGHPUT_CHART_EMBEDDABLE';
|
||||
|
||||
interface APMEmbeddableRootProps {
|
||||
slo: APMTransactionDurationSLOResponse;
|
||||
dataTimeRange: TimeRange;
|
||||
embeddableId: EmbeddableId;
|
||||
alert: BurnRateAlert;
|
||||
rule: BurnRateRule;
|
||||
}
|
||||
|
||||
export type APMTransactionDurationSLOResponse = GetSLOResponse & {
|
||||
indicator: APMTransactionDurationIndicator;
|
||||
};
|
||||
|
||||
export function APMEmbeddableRoot({
|
||||
slo,
|
||||
dataTimeRange,
|
||||
embeddableId,
|
||||
alert,
|
||||
rule,
|
||||
}: APMEmbeddableRootProps) {
|
||||
const filter = slo.indicator.params.filter;
|
||||
const isKueryFilter = typeof filter === 'string';
|
||||
const groupingInput = getInputFromGroupings(slo);
|
||||
|
||||
const kuery = isKueryFilter ? filter : undefined;
|
||||
const allFilters =
|
||||
!isKueryFilter && filter?.filters
|
||||
? [...filter?.filters, ...groupingInput.filters]
|
||||
: groupingInput.filters;
|
||||
const filters = buildQueryFromFilters(allFilters, undefined, undefined);
|
||||
const groupingsInput = getInputFromGroupings(slo);
|
||||
const { transactionName, transactionType, environment, service } = slo.indicator.params;
|
||||
const input = {
|
||||
id: uuidv4(),
|
||||
serviceName: service,
|
||||
transactionName: transactionName !== '*' ? transactionName : undefined,
|
||||
transactionType: transactionType !== '*' ? transactionType : undefined,
|
||||
environment: environment !== '*' ? environment : undefined,
|
||||
rangeFrom: dataTimeRange.from.toISOString(),
|
||||
rangeTo: dataTimeRange.to.toISOString(),
|
||||
latencyThresholdInMicroseconds: slo.indicator.params.threshold * 1000,
|
||||
kuery,
|
||||
filters,
|
||||
alert,
|
||||
rule,
|
||||
comparisonEnabled: true,
|
||||
offset: '1d',
|
||||
...groupingsInput.input,
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactEmbeddableRenderer
|
||||
type={embeddableId}
|
||||
state={{ rawState: input }}
|
||||
hidePanelChrome={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getInputFromGroupings = (slo: APMTransactionDurationSLOResponse) => {
|
||||
const groupings = Object.entries(slo.groupings) as Array<[string, string]>;
|
||||
const input: {
|
||||
transactionName?: string;
|
||||
transactionType?: string;
|
||||
serviceName?: string;
|
||||
environment?: string;
|
||||
} = {};
|
||||
const filters: Filter[] = [];
|
||||
groupings.forEach(([key, value]) => {
|
||||
switch (key) {
|
||||
case 'transaction.name':
|
||||
input.transactionName = value;
|
||||
break;
|
||||
case 'transaction.type':
|
||||
input.transactionType = value;
|
||||
break;
|
||||
case 'service.name':
|
||||
input.serviceName = value;
|
||||
break;
|
||||
case 'service.environment':
|
||||
input.environment = value;
|
||||
break;
|
||||
default:
|
||||
filters.push({
|
||||
meta: {
|
||||
type: 'custom',
|
||||
alias: null,
|
||||
key,
|
||||
params: {
|
||||
query: value,
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
input,
|
||||
filters,
|
||||
};
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
import { GetSLOResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { LogRateAnalysisPanel } from './log_rate_analysis_panel';
|
||||
import { BurnRateAlert, BurnRateRule } from '../../../alert_details_app_section';
|
||||
import { BurnRateAlert, BurnRateRule } from '../../../types';
|
||||
import { useLicense } from '../../../../../../../hooks/use_license';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -25,7 +25,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { Message } from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import type { WindowSchema } from '../../../../../../../typings';
|
||||
import { TimeRange } from '../../../../../error_rate_chart/use_lens_definition';
|
||||
import { BurnRateAlert, BurnRateRule } from '../../../alert_details_app_section';
|
||||
import { BurnRateAlert, BurnRateRule } from '../../../types';
|
||||
import { getActionGroupFromReason } from '../../../utils/alert';
|
||||
import { useKibana } from '../../../../../../../utils/kibana_react';
|
||||
import { getESQueryForLogRateAnalysis } from './helpers/log_rate_analysis_query';
|
||||
|
|
|
@ -6,9 +6,12 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { GetSLOResponse } from '@kbn/slo-schema';
|
||||
import type { GetSLOResponse } from '@kbn/slo-schema';
|
||||
import { APMAlertDetails } from './apm/apm_alert_details';
|
||||
import { CustomKqlPanels } from './custom_kql/custom_kql_panels';
|
||||
import { BurnRateAlert, BurnRateRule } from '../../alert_details_app_section';
|
||||
import { getDataTimeRange } from '../../utils/time_range';
|
||||
import type { BurnRateAlert, BurnRateRule } from '../../types';
|
||||
import type { APMTransactionDurationSLOResponse } from './apm/apm_alert_details';
|
||||
|
||||
interface Props {
|
||||
alert: BurnRateAlert;
|
||||
|
@ -17,9 +20,19 @@ interface Props {
|
|||
}
|
||||
|
||||
export function CustomAlertDetailsPanel({ slo, alert, rule }: Props) {
|
||||
const dataTimeRange = getDataTimeRange(alert);
|
||||
switch (slo?.indicator.type) {
|
||||
case 'sli.kql.custom':
|
||||
return <CustomKqlPanels slo={slo} alert={alert} rule={rule} />;
|
||||
case 'sli.apm.transactionDuration':
|
||||
return (
|
||||
<APMAlertDetails
|
||||
slo={slo as APMTransactionDurationSLOResponse}
|
||||
dataTimeRange={dataTimeRange}
|
||||
alert={alert}
|
||||
rule={rule}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -20,31 +20,16 @@ import {
|
|||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
ALERT_EVALUATION_VALUE,
|
||||
ALERT_RULE_PARAMETERS,
|
||||
ALERT_TIME_RANGE,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { ALERT_EVALUATION_VALUE, ALERT_TIME_RANGE } from '@kbn/rule-data-utils';
|
||||
import { GetSLOResponse } from '@kbn/slo-schema';
|
||||
import React from 'react';
|
||||
import { WindowSchema } from '../../../../../../typings';
|
||||
import { useKibana } from '../../../../../../utils/kibana_react';
|
||||
import { ErrorRateChart } from '../../../../error_rate_chart';
|
||||
import { TimeRange } from '../../../../error_rate_chart/use_lens_definition';
|
||||
import { BurnRateAlert } from '../../alert_details_app_section';
|
||||
import { getActionGroupFromReason } from '../../utils/alert';
|
||||
import { BurnRateAlert } from '../../types';
|
||||
import { getActionGroupWindow } from '../../utils/alert';
|
||||
import { getLastDurationInUnit } from '../../utils/last_duration_i18n';
|
||||
|
||||
function getDataTimeRange(
|
||||
timeRange: { gte: string; lte?: string },
|
||||
window: WindowSchema
|
||||
): TimeRange {
|
||||
const windowDurationInMs = window.longWindow.value * 60 * 60 * 1000;
|
||||
return {
|
||||
from: new Date(new Date(timeRange.gte).getTime() - windowDurationInMs),
|
||||
to: timeRange.lte ? new Date(timeRange.lte) : new Date(),
|
||||
};
|
||||
}
|
||||
import { getDataTimeRange } from '../../utils/time_range';
|
||||
|
||||
function getAlertTimeRange(timeRange: { gte: string; lte?: string }): TimeRange {
|
||||
return {
|
||||
|
@ -63,14 +48,8 @@ export function ErrorRatePanel({ alert, slo, isLoading }: Props) {
|
|||
const {
|
||||
services: { http },
|
||||
} = useKibana();
|
||||
|
||||
const actionGroup = getActionGroupFromReason(alert.reason);
|
||||
const actionGroupWindow = (
|
||||
(alert.fields[ALERT_RULE_PARAMETERS]?.windows ?? []) as WindowSchema[]
|
||||
).find((window: WindowSchema) => window.actionGroup === actionGroup);
|
||||
|
||||
// @ts-ignore
|
||||
const dataTimeRange = getDataTimeRange(alert.fields[ALERT_TIME_RANGE], actionGroupWindow);
|
||||
const dataTimeRange = getDataTimeRange(alert);
|
||||
const actionGroupWindow = getActionGroupWindow(alert);
|
||||
// @ts-ignore
|
||||
const alertTimeRange = getAlertTimeRange(alert.fields[ALERT_TIME_RANGE]);
|
||||
const burnRate = alert.fields[ALERT_EVALUATION_VALUE];
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { Rule } from '@kbn/alerting-plugin/common';
|
||||
import type { TopAlert } from '@kbn/observability-plugin/public';
|
||||
import type { BurnRateRuleParams } from '../../../../typings/slo';
|
||||
export type { TimeRange } from '../../error_rate_chart/use_lens_definition';
|
||||
|
||||
export type BurnRateRule = Rule<BurnRateRuleParams>;
|
||||
export type BurnRateAlert = TopAlert;
|
|
@ -4,13 +4,15 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
ALERT_ACTION_ID,
|
||||
HIGH_PRIORITY_ACTION_ID,
|
||||
LOW_PRIORITY_ACTION_ID,
|
||||
MEDIUM_PRIORITY_ACTION_ID,
|
||||
} from '../../../../../../common/constants';
|
||||
import { BurnRateAlert } from '../types';
|
||||
import { WindowSchema } from '../../../../../typings';
|
||||
|
||||
export function getActionGroupFromReason(reason: string): string {
|
||||
const prefix = reason.split(':')[0]?.toLowerCase() ?? undefined;
|
||||
|
@ -26,3 +28,11 @@ export function getActionGroupFromReason(reason: string): string {
|
|||
return LOW_PRIORITY_ACTION_ID;
|
||||
}
|
||||
}
|
||||
|
||||
export function getActionGroupWindow(alert: BurnRateAlert) {
|
||||
const actionGroup = getActionGroupFromReason(alert.reason);
|
||||
const actionGroupWindow = (
|
||||
(alert.fields[ALERT_RULE_PARAMETERS]?.windows ?? []) as WindowSchema[]
|
||||
).find((window: WindowSchema) => window.actionGroup === actionGroup)!;
|
||||
return actionGroupWindow;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { DateRange } from '@kbn/alerting-plugin/common';
|
||||
import { ALERT_TIME_RANGE } from '@kbn/rule-data-utils';
|
||||
import { TimeRange } from '../../../error_rate_chart/use_lens_definition';
|
||||
import { BurnRateAlert } from '../types';
|
||||
import { getActionGroupWindow } from './alert';
|
||||
|
||||
export function getDataTimeRange(alert: BurnRateAlert): TimeRange {
|
||||
const timeRange = alert.fields[ALERT_TIME_RANGE] as DateRange;
|
||||
const actionGroupWindow = getActionGroupWindow(alert);
|
||||
const windowDurationInMs = actionGroupWindow.longWindow.value * 60 * 60 * 1000;
|
||||
return {
|
||||
from: new Date(new Date(timeRange.gte).getTime() - windowDurationInMs),
|
||||
to: timeRange.lte ? new Date(timeRange.lte) : new Date(),
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue