[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.


![image](22eb711d-1b72-42a4-b264-effaaacffdf7)

### 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:
Dominique Clarke 2024-05-13 10:25:24 -04:00 committed by GitHub
parent 218f5ae6f2
commit 66f1c4e5a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1837 additions and 87 deletions

View file

@ -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', {

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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}
/>
);
}

View file

@ -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,

View file

@ -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,

View file

@ -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,
},
});
}}

View file

@ -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)}
/>
);
}

View file

@ -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);

View file

@ -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();
});
});
});

View file

@ -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}
/>
);
}

View file

@ -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;
};

View file

@ -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();
});
});
});

View file

@ -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}
/>
);
}

View file

@ -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;
};

View file

@ -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();
});
});
});

View file

@ -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}
/>
);
}

View file

@ -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;
};

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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;
}

View file

@ -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,
};
}

View file

@ -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>
);
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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));

View file

@ -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/**/*"]
}

View file

@ -20,6 +20,7 @@
"data",
"dataViews",
"dataViewEditor",
"embeddable",
"fieldFormats",
"uiActions",
"presentationUtil",

View file

@ -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;

View file

@ -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 {

View file

@ -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>
);
}

View file

@ -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,
};
};

View file

@ -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 {

View file

@ -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';

View file

@ -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;
}

View file

@ -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];

View file

@ -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;

View file

@ -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;
}

View file

@ -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(),
};
}