Skip to content

Commit

Permalink
[Actionable Observability] - Add latency alert history chart on the A…
Browse files Browse the repository at this point in the history
…lert details page for APM (#148011)

## Summary

Closes #147932 by adding a new latency chart that covers the last 30
days of alerts for a given rule.
And it adds annotations with the number of alerts for a given day
besides the time to recover.

<img width="1196" alt="Screenshot 2023-01-18 at 16 22 08"
src="https://user-images.githubusercontent.com/6838659/213211368-1416e620-e96c-4a98-9552-9397fc37ee1e.png">
  • Loading branch information
fkanout authored Jan 19, 2023
1 parent ecc048c commit ff3d413
Show file tree
Hide file tree
Showing 11 changed files with 496 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ import { EuiIconTip } from '@elastic/eui';
import {
ALERT_DURATION,
ALERT_END,
ALERT_RULE_UUID,
ALERT_EVALUATION_THRESHOLD,
ALERT_RULE_TYPE_ID,
} from '@kbn/rule-data-utils';
import moment from 'moment';
import { getTransactionType } from '../../../../context/apm_service/apm_service_context';
import { useServiceAgentFetcher } from '../../../../context/apm_service/use_service_agent_fetcher';
import { useServiceTransactionTypesFetcher } from '../../../../context/apm_service/use_service_transaction_types_fetcher';
import { asPercent } from '../../../../../common/utils/formatters';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { getDurationFormatter } from '../../../../../common/utils/formatters/duration';
Expand All @@ -48,6 +46,7 @@ import {
import { getAggsTypeFromRule, isLatencyThresholdRuleType } from './helpers';
import { filterNil } from '../../../shared/charts/latency_chart';
import { errorRateI18n } from '../../../shared/charts/failed_transaction_rate_chart';
import { LatencyAlertsHistoryChart } from './latency_alerts_history_chart';
import {
AlertActiveRect,
AlertAnnotation,
Expand Down Expand Up @@ -100,23 +99,7 @@ export function AlertDetailsAppSection({
.toISOString();

const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { agentName } = useServiceAgentFetcher({
serviceName,
start,
end,
});
const transactionTypes = useServiceTransactionTypesFetcher({
serviceName,
start,
end,
});

const transactionType = getTransactionType({
transactionType: alert.fields[TRANSACTION_TYPE],
transactionTypes,
agentName,
});

const transactionType = alert.fields[TRANSACTION_TYPE];
const comparisonChartTheme = getComparisonChartTheme();
const INITIAL_STATE = {
currentPeriod: [],
Expand Down Expand Up @@ -443,6 +426,18 @@ export function AlertDetailsAppSection({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LatencyAlertsHistoryChart
ruleId={alert.fields[ALERT_RULE_UUID]}
serviceName={serviceName}
start={start}
end={end}
transactionType={transactionType}
latencyAggregationType={latencyAggregationType}
environment={environment}
timeZone={timeZone}
/>
</EuiFlexItem>
</ChartPointerEventContextProvider>
</EuiFlexGroup>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
* 2.0.
*/
export const DEFAULT_DATE_FORMAT = 'HH:mm:ss';
export const CHART_ANNOTATION_RED_COLOR = '#BD271E';
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/*
* 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, { useMemo } from 'react';
import moment from 'moment';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { EuiFlexGroup } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiText } from '@elastic/eui';
import {
AnnotationDomainType,
LineAnnotation,
Position,
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { EuiBadge } from '@elastic/eui';
import { convertTo } from '@kbn/observability-plugin/public';
import { useFetchTriggeredAlertsHistory } from '../../../../hooks/use_fetch_triggered_alert_history';
import { getDurationFormatter } from '../../../../../common/utils/formatters';
import { getLatencyChartSelector } from '../../../../selectors/latency_chart_selectors';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
import { filterNil } from '../../../shared/charts/latency_chart';
import {
getMaxY,
getResponseTimeTickFormatter,
} from '../../../shared/charts/transaction_charts/helper';
import { CHART_ANNOTATION_RED_COLOR } from './constants';

interface LatencyAlertsHistoryChartProps {
serviceName: string;
start: string;
end: string;
transactionType?: string;
latencyAggregationType: LatencyAggregationType;
environment: string;
timeZone: string;
ruleId: string;
}
export function LatencyAlertsHistoryChart({
serviceName,
start,
end,
transactionType,
latencyAggregationType,
environment,
timeZone,
ruleId,
}: LatencyAlertsHistoryChartProps) {
const { data, status } = useFetcher(
(callApmApi) => {
if (
serviceName &&
start &&
end &&
transactionType &&
latencyAggregationType
) {
return callApmApi(
`GET /internal/apm/services/{serviceName}/transactions/charts/latency`,
{
params: {
path: { serviceName },
query: {
environment,
kuery: '',
start: moment().subtract(30, 'days').toISOString(),
end: moment().toISOString(),
transactionType,
transactionName: undefined,
latencyAggregationType,
},
},
}
);
}
},
[
end,
environment,
latencyAggregationType,
serviceName,
start,
transactionType,
]
);
const memoizedData = useMemo(
() =>
getLatencyChartSelector({
latencyChart: data,
latencyAggregationType,
previousPeriodLabel: '',
}),
// It should only update when the data has changed
// eslint-disable-next-line react-hooks/exhaustive-deps
[data]
);

const { currentPeriod, previousPeriod } = memoizedData;
const timeseriesLatency = [currentPeriod, previousPeriod].filter(filterNil);
const latencyMaxY = getMaxY(timeseriesLatency);
const latencyFormatter = getDurationFormatter(latencyMaxY);
const { triggeredAlertsData } = useFetchTriggeredAlertsHistory({
features: 'apm',
ruleId,
});

return (
<EuiPanel hasBorder={true}>
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{serviceName}
{i18n.translate('xpack.apm.latencyChartHistory.chartTitle', {
defaultMessage: ' latency alerts history',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
{i18n.translate('xpack.apm.latencyChartHistory.last30days', {
defaultMessage: 'Last 30 days',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />

<EuiFlexGroup gutterSize="l">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" direction="column">
<EuiFlexItem grow={false}>
<EuiText color="danger">
<EuiTitle size="s">
<h3>{triggeredAlertsData?.totalTriggeredAlerts || '-'}</h3>
</EuiTitle>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
{i18n.translate(
'xpack.apm.latencyChartHistory.alertsTriggered',
{
defaultMessage: 'Alerts triggered',
}
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexGroup gutterSize="xs" direction="column">
<EuiFlexItem grow={false}>
<EuiText>
<EuiTitle size="s">
<h3>
{triggeredAlertsData?.avgTimeToRecoverUS
? convertTo({
unit: 'minutes',
microseconds: triggeredAlertsData?.avgTimeToRecoverUS,
extended: true,
}).formatted
: '-'}
</h3>
</EuiTitle>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
{i18n.translate(
'xpack.apm.latencyChartHistory.avgTimeToRecover',
{
defaultMessage: 'Avg time to recover',
}
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
<EuiSpacer size="s" />

<TimeseriesChart
id="latencyChart"
annotations={[
<LineAnnotation
id="annotations"
key={'annotationsAlertHistory'}
domainType={AnnotationDomainType.XDomain}
dataValues={
triggeredAlertsData?.histogramTriggeredAlerts
.filter((annotation) => annotation.doc_count > 0)
.map((annotation) => {
return {
dataValue: annotation.key,
header: String(annotation.doc_count),
details: moment(annotation.key_as_string).format(
'yyyy-MM-DD'
),
};
}) || []
}
style={{
line: {
strokeWidth: 3,
stroke: CHART_ANNOTATION_RED_COLOR,
opacity: 1,
},
}}
marker={<EuiIcon type="alert" color={CHART_ANNOTATION_RED_COLOR} />}
markerBody={(annotationData) => (
<>
<EuiBadge color={CHART_ANNOTATION_RED_COLOR}>
<EuiText size="xs" color="white">
{annotationData.header}
</EuiText>
</EuiBadge>
<EuiSpacer size="xs" />
</>
)}
markerPosition={Position.Top}
/>,
]}
height={200}
comparisonEnabled={false}
offset={''}
fetchStatus={status}
timeseries={timeseriesLatency}
yLabelFormat={getResponseTimeTickFormatter(latencyFormatter)}
timeZone={timeZone}
/>
</EuiPanel>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import moment from 'moment';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DEFAULT_DATE_FORMAT } from '../constants';
import { CHART_ANNOTATION_RED_COLOR, DEFAULT_DATE_FORMAT } from '../constants';

export function AlertAnnotation({ alertStarted }: { alertStarted: number }) {
return (
Expand All @@ -36,11 +36,11 @@ export function AlertAnnotation({ alertStarted }: { alertStarted: number }) {
style={{
line: {
strokeWidth: 3,
stroke: '#f00',
stroke: CHART_ANNOTATION_RED_COLOR,
opacity: 1,
},
}}
marker={<EuiIcon type="alert" color="red" />}
marker={<EuiIcon type="alert" color={CHART_ANNOTATION_RED_COLOR} />}
markerPosition={Position.Top}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import React from 'react';
import { AnnotationDomainType, LineAnnotation } from '@elastic/charts';
import { CHART_ANNOTATION_RED_COLOR } from '../constants';

export function AlertThresholdAnnotation({
threshold,
Expand All @@ -29,7 +30,7 @@ export function AlertThresholdAnnotation({
line: {
opacity: 0.5,
strokeWidth: 1,
stroke: 'red',
stroke: CHART_ANNOTATION_RED_COLOR,
},
}}
/>
Expand Down
Loading

0 comments on commit ff3d413

Please sign in to comment.