[AO] Use annotations and utilities of @kbn/observability-alert-details in the APM Latency alert details page (#158207)

## Summary

It fixes #156566 by reusing the shared components and the utilities from
`@kbn/observability-alert-details `
This commit is contained in:
Faisal Kanout 2023-05-24 10:48:04 +02:00 committed by GitHub
parent 4efb235640
commit bdc4399e01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 166 additions and 232 deletions

View file

@ -7,5 +7,7 @@
export { AlertAnnotation } from './src/components/alert_annotation';
export { AlertActiveTimeRangeAnnotation } from './src/components/alert_active_time_range_annotation';
export { AlertThresholdTimeRangeRect } from './src/components/alert_threshold_time_range_rect';
export { AlertThresholdAnnotation } from './src/components/alert_threshold_annotation';
export { getPaddedAlertTimeRange } from './src/helpers/get_padded_alert_time_range';
export { useAlertsHistory } from './src/hooks/use_alerts_history';

View file

@ -6,31 +6,39 @@
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { AnnotationDomainType, LineAnnotation } from '@elastic/charts';
import { CHART_ANNOTATION_RED_COLOR } from '../constants';
export function AlertThresholdAnnotation({
threshold,
}: {
threshold?: number;
}) {
if (!threshold) return <></>;
interface Props {
id: string;
threshold: number;
color: string;
}
const ANNOTATION_TITLE = i18n.translate(
'observabilityAlertDetails.alertThresholdAnnotation.detailsTooltip',
{
defaultMessage: 'Alert started',
}
);
export function AlertThresholdAnnotation({ threshold, color, id }: Props) {
return (
<LineAnnotation
id="annotation_alert_threshold"
id={id}
domainType={AnnotationDomainType.YDomain}
dataValues={[
{
dataValue: threshold,
header: String(threshold),
details: ANNOTATION_TITLE,
},
]}
style={{
line: {
opacity: 0.5,
strokeWidth: 1,
stroke: CHART_ANNOTATION_RED_COLOR,
stroke: color,
},
}}
/>

View file

@ -0,0 +1,43 @@
/*
* 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 { RectAnnotation } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
interface Props {
alertStarted: number;
color: string;
id: string;
threshold: number;
}
const RECT_ANNOTATION_TITLE = i18n.translate(
'observabilityAlertDetails.alertThresholdTimeRangeRect.detailsTooltip',
{
defaultMessage: 'Threshold',
}
);
export function AlertThresholdTimeRangeRect({ alertStarted, color, id, threshold }: Props) {
return (
<RectAnnotation
id={id}
zIndex={2}
dataValues={[
{
coordinates: {
y0: threshold,
x1: alertStarted,
},
details: RECT_ANNOTATION_TITLE,
},
]}
style={{ fill: color, opacity: 0.1 }}
/>
);
}

View file

@ -9,26 +9,27 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { formatAlertEvaluationValue } from '@kbn/observability-plugin/public';
import {
ALERT_DURATION,
ALERT_END,
ALERT_EVALUATION_THRESHOLD,
ALERT_EVALUATION_VALUE,
ALERT_RULE_TYPE_ID,
ALERT_RULE_UUID,
ALERT_START,
} from '@kbn/rule-data-utils';
import moment from 'moment';
import React, { useEffect, useMemo } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { getPaddedAlertTimeRange } from '@kbn/observability-alert-details';
import { EuiCallOut } from '@elastic/eui';
import { toMicroseconds as toMicrosecondsUtil } from '../../../../../common/utils/formatters';
import { SERVICE_ENVIRONMENT } from '../../../../../common/es_fields/apm';
import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context';
import { TimeRangeMetadataContextProvider } from '../../../../context/time_range_metadata/time_range_metadata_context';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { getComparisonChartTheme } from '../../../shared/time_comparison/get_comparison_chart_theme';
import FailedTransactionChart from './failed_transaction_chart';
import { getAggsTypeFromRule } from './helpers';
import { LatencyAlertsHistoryChart } from './latency_alerts_history_chart';
import LatencyChart from './latency_chart/latency_chart';
import LatencyChart from './latency_chart';
import ThroughputChart from './throughput_chart';
import {
AlertDetailsAppSectionProps,
@ -93,53 +94,20 @@ export function AlertDetailsAppSection({
setAlertSummaryFields(alertSummaryFields);
}, [alert?.fields, setAlertSummaryFields]);
const params = rule.params;
const environment = alert.fields[SERVICE_ENVIRONMENT];
const latencyAggregationType = getAggsTypeFromRule(params.aggregationType);
// duration is us, convert it to MS
const alertDurationMS = alert.fields[ALERT_DURATION]! / 1000;
const serviceName = String(alert.fields[SERVICE_NAME]);
// Currently, we don't use comparisonEnabled nor offset.
// But providing them as they are required for the chart.
const comparisonEnabled = false;
const offset = '1d';
const ruleWindowSizeMS = moment
.duration(rule.params.windowSize, rule.params.windowUnit)
.asMilliseconds();
const TWENTY_TIMES_RULE_WINDOW_MS = 20 * ruleWindowSizeMS;
/**
* This is part or the requirements (RFC).
* If the alert is less than 20 units of `FOR THE LAST <x> <units>` then we should draw a time range of 20 units.
* IE. The user set "FOR THE LAST 5 minutes" at a minimum we should show 100 minutes.
*/
const rangeFrom =
alertDurationMS < TWENTY_TIMES_RULE_WINDOW_MS
? moment(alert.start)
.subtract(TWENTY_TIMES_RULE_WINDOW_MS, 'millisecond')
.toISOString()
: moment(alert.start)
.subtract(ruleWindowSizeMS, 'millisecond')
.toISOString();
const rangeTo = alert.active
? // Add one minute to chart range to ensure that the active alert annotation is shown when seconds are involved.
moment().add(1, 'minute').toISOString()
: moment(alert.fields[ALERT_END])
.add(ruleWindowSizeMS, 'millisecond')
.toISOString();
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const transactionType = alert.fields[TRANSACTION_TYPE];
const comparisonChartTheme = getComparisonChartTheme();
const {
services: { uiSettings },
} = useKibana();
const params = rule.params;
const environment = alert.fields[SERVICE_ENVIRONMENT];
const latencyAggregationType = getAggsTypeFromRule(params.aggregationType);
const serviceName = String(alert.fields[SERVICE_NAME]);
const timeRange = getPaddedAlertTimeRange(
alert.fields[ALERT_START]!,
alert.fields[ALERT_END]
);
const transactionType = alert.fields[TRANSACTION_TYPE];
const comparisonChartTheme = getComparisonChartTheme();
const historicalRange = useMemo(() => {
return {
start: moment().subtract(30, 'days').toISOString(),
@ -147,11 +115,34 @@ export function AlertDetailsAppSection({
};
}, []);
const { from, to } = timeRange;
if (!from || !to) {
return (
<EuiCallOut
title={
<FormattedMessage
id="xpack.apm.alertDetails.error.toastTitle"
defaultMessage="An error occurred when identifying the alert time range."
/>
}
color="danger"
iconType="error"
>
<p>
<FormattedMessage
id="xpack.apm.alertDetails.error.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>
);
}
return (
<EuiFlexGroup direction="column" gutterSize="s">
<TimeRangeMetadataContextProvider
start={start}
end={end}
start={from}
end={to}
kuery=""
useSpanName={false}
uiSettings={uiSettings!}
@ -163,13 +154,13 @@ export function AlertDetailsAppSection({
transactionType={transactionType}
serviceName={serviceName}
environment={environment}
start={start}
end={end}
start={from}
end={to}
comparisonChartTheme={comparisonChartTheme}
timeZone={timeZone}
latencyAggregationType={latencyAggregationType}
comparisonEnabled={comparisonEnabled}
offset={offset}
comparisonEnabled={false}
offset={''}
/>
<EuiSpacer size="s" />
<EuiFlexGroup direction="row" gutterSize="s">
@ -177,19 +168,19 @@ export function AlertDetailsAppSection({
transactionType={transactionType}
serviceName={serviceName}
environment={environment}
start={start}
end={end}
start={from}
end={to}
comparisonChartTheme={comparisonChartTheme}
comparisonEnabled={comparisonEnabled}
offset={offset}
comparisonEnabled={false}
offset={''}
timeZone={timeZone}
/>
<FailedTransactionChart
transactionType={transactionType}
serviceName={serviceName}
environment={environment}
start={start}
end={end}
start={from}
end={to}
comparisonChartTheme={comparisonChartTheme}
timeZone={timeZone}
/>

View file

@ -15,23 +15,29 @@ import {
ALERT_EVALUATION_THRESHOLD,
} from '@kbn/rule-data-utils';
import type { TopAlert } from '@kbn/observability-plugin/public';
import { filterNil } from '../../../../shared/charts/latency_chart';
import { TimeseriesChart } from '../../../../shared/charts/timeseries_chart';
import {
AlertActiveTimeRangeAnnotation,
AlertThresholdAnnotation,
AlertThresholdTimeRangeRect,
AlertAnnotation,
} from '@kbn/observability-alert-details';
import { useEuiTheme } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import { filterNil } from '../../../shared/charts/latency_chart';
import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
import {
getMaxY,
getResponseTimeTickFormatter,
} from '../../../../shared/charts/transaction_charts/helper';
import { isTimeComparison } from '../../../../shared/time_comparison/get_comparison_options';
import { useFetcher } from '../../../../../hooks/use_fetcher';
import { getLatencyChartSelector } from '../../../../../selectors/latency_chart_selectors';
import { LatencyAggregationType } from '../../../../../../common/latency_aggregation_types';
import { isLatencyThresholdRuleType } from '../helpers';
import { AlertActiveRect } from './alert_active_rect';
import { AlertAnnotation } from './alert_annotation';
import { AlertThresholdAnnotation } from './alert_threshold_annotation';
import { AlertThresholdRect } from './alert_threshold_rect';
import { ApmDocumentType } from '../../../../../../common/document_type';
import { usePreferredDataSourceAndBucketSize } from '../../../../../hooks/use_preferred_data_source_and_bucket_size';
} from '../../../shared/charts/transaction_charts/helper';
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { getLatencyChartSelector } from '../../../../selectors/latency_chart_selectors';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
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';
function LatencyChart({
alert,
@ -65,7 +71,10 @@ function LatencyChart({
numBuckets: 100,
type: ApmDocumentType.ServiceTransactionMetric,
});
const { euiTheme } = useEuiTheme();
const {
services: { uiSettings },
} = useKibana();
const { data, status } = useFetcher(
(callApmApi) => {
if (
@ -108,27 +117,46 @@ function LatencyChart({
preferred,
]
);
const alertEvalThreshold = alert.fields[ALERT_EVALUATION_THRESHOLD];
const alertEvalThresholdChartData = alertEvalThreshold
? [
<AlertThresholdTimeRangeRect
key={'alertThresholdRect'}
id={'alertThresholdRect'}
threshold={alertEvalThreshold}
alertStarted={alert.start}
color={euiTheme.colors.danger}
/>,
<AlertThresholdAnnotation
id={'alertThresholdAnnotation'}
key={'alertThresholdAnnotation'}
color={euiTheme.colors.danger}
threshold={alertEvalThreshold}
/>,
]
: [];
const getLatencyChartAdditionalData = () => {
if (isLatencyThresholdRuleType(alert.fields[ALERT_RULE_TYPE_ID])) {
return [
<AlertThresholdRect
<AlertActiveTimeRangeAnnotation
alertStart={alert.start}
color={euiTheme.colors.danger}
id={'alertActiveRect'}
key={'alertThresholdRect'}
threshold={alert.fields[ALERT_EVALUATION_THRESHOLD]}
alertStarted={alert.start}
/>,
<AlertAnnotation
key={'alertAnnotationStart'}
alertStarted={alert.start}
/>,
<AlertActiveRect
key={'alertAnnotationActiveRect'}
alertStarted={alert.start}
/>,
<AlertThresholdAnnotation
key={'alertThresholdAnnotation'}
threshold={alert.fields[ALERT_EVALUATION_THRESHOLD]}
id={'alertAnnotationStart'}
alertStart={alert.start}
color={euiTheme.colors.danger}
dateFormat={
(uiSettings && uiSettings.get(UI_SETTINGS.DATE_FORMAT)) ||
DEFAULT_DATE_FORMAT
}
/>,
...alertEvalThresholdChartData,
];
}
};

View file

@ -1,32 +0,0 @@
/*
* 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 { RectAnnotation } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import React from 'react';
export function AlertActiveRect({ alertStarted }: { alertStarted: number }) {
return (
<RectAnnotation
id="rect_alert_active"
dataValues={[
{
coordinates: {
y0: 0,
x0: alertStarted,
},
details: i18n.translate(
'xpack.apm.latency.chart.alertDetails.active',
{
defaultMessage: 'Active',
}
),
},
]}
style={{ fill: 'red', opacity: 0.2 }}
/>
);
}

View file

@ -1,47 +0,0 @@
/*
* 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 {
AnnotationDomainType,
LineAnnotation,
Position,
} from '@elastic/charts';
import moment from 'moment';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { CHART_ANNOTATION_RED_COLOR, DEFAULT_DATE_FORMAT } from '../constants';
export function AlertAnnotation({ alertStarted }: { alertStarted: number }) {
return (
<LineAnnotation
id="annotation_alert_started"
domainType={AnnotationDomainType.XDomain}
dataValues={[
{
dataValue: alertStarted,
header: moment(alertStarted).format(DEFAULT_DATE_FORMAT),
details: i18n.translate(
'xpack.apm.latency.chart.alertDetails.alertStarted',
{
defaultMessage: 'Alert started',
}
),
},
]}
style={{
line: {
strokeWidth: 3,
stroke: CHART_ANNOTATION_RED_COLOR,
opacity: 1,
},
}}
marker={<EuiIcon type="warning" color={CHART_ANNOTATION_RED_COLOR} />}
markerPosition={Position.Top}
/>
);
}

View file

@ -1,39 +0,0 @@
/*
* 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 { RectAnnotation } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import React from 'react';
export function AlertThresholdRect({
threshold,
alertStarted,
}: {
threshold?: number;
alertStarted: number;
}) {
if (!threshold) return <></>;
return (
<RectAnnotation
id="rect_alert_threshold"
zIndex={2}
dataValues={[
{
coordinates: { y0: threshold, x1: alertStarted },
details: i18n.translate(
'xpack.apm.latency.chart.alertDetails.threshold',
{
defaultMessage: 'Threshold',
}
),
},
]}
style={{ fill: 'red', opacity: 0.05 }}
/>
);
}

View file

@ -1,11 +0,0 @@
/*
* 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.
*/
export { AlertAnnotation } from './alert_annotation';
export { AlertThresholdRect } from './alert_threshold_rect';
export { AlertActiveRect } from './alert_active_rect';
export { AlertThresholdAnnotation } from './alert_threshold_annotation';

View file

@ -8207,9 +8207,6 @@
"xpack.apm.labs.cancel": "Annuler",
"xpack.apm.labs.description": "Essayez les fonctionnalités APM qui sont en version d'évaluation technique et en cours de progression.",
"xpack.apm.labs.reload": "Recharger pour appliquer les modifications",
"xpack.apm.latency.chart.alertDetails.active": "Actif",
"xpack.apm.latency.chart.alertDetails.alertStarted": "Alerte démarrée",
"xpack.apm.latency.chart.alertDetails.threshold": "Seuil",
"xpack.apm.latencyChartHistory.alertsTriggered": "Alertes déclenchées",
"xpack.apm.latencyChartHistory.avgTimeToRecover": "Temps moyen de récupération",
"xpack.apm.latencyChartHistory.chartTitle": " historique des alertes de latence",

View file

@ -8208,9 +8208,6 @@
"xpack.apm.labs.cancel": "キャンセル",
"xpack.apm.labs.description": "現在テクニカルプレビュー中のAPM機能をお試しください。",
"xpack.apm.labs.reload": "変更を適用するには、再読み込みしてください",
"xpack.apm.latency.chart.alertDetails.active": "アクティブ",
"xpack.apm.latency.chart.alertDetails.alertStarted": "アラートが開始しました",
"xpack.apm.latency.chart.alertDetails.threshold": "しきい値",
"xpack.apm.latencyChartHistory.alertsTriggered": "アラートがトリガーされました",
"xpack.apm.latencyChartHistory.avgTimeToRecover": "回復までの平均時間",
"xpack.apm.latencyChartHistory.chartTitle": " レイテンシアラート履歴",

View file

@ -8207,9 +8207,6 @@
"xpack.apm.labs.cancel": "取消",
"xpack.apm.labs.description": "试用正处于技术预览状态和开发中的 APM 功能。",
"xpack.apm.labs.reload": "重新加载以应用更改",
"xpack.apm.latency.chart.alertDetails.active": "活动",
"xpack.apm.latency.chart.alertDetails.alertStarted": "已启动告警",
"xpack.apm.latency.chart.alertDetails.threshold": "阈值",
"xpack.apm.latencyChartHistory.alertsTriggered": "已触发告警",
"xpack.apm.latencyChartHistory.avgTimeToRecover": "恢复的平均时间",
"xpack.apm.latencyChartHistory.chartTitle": " 延迟告警历史记录",