Skip to content

Commit

Permalink
[Actionable Observability] - Add alert annotation and threshold shade…
Browse files Browse the repository at this point in the history
… on the APM latency chart on the Alert Details page (#147848)

## Summary

Closes #147779 by adding the following elements to the latency chart for
the Latency threshold alert:

1. Alert annotation that indicates when the alert is triggered 
2. Alert threshold rect (shade) 
3. Alert threshold annotation (red line on Y axe)
4. Alert active rect (dark red shade where the alert is active)


![image](https://user-images.githubusercontent.com/6838659/208887316-b1848d92-4ee5-4b3e-97e0-1342fb25d3fa.png)
  • Loading branch information
fkanout authored Jan 9, 2023
1 parent ee0856d commit 12ae75f
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import { i18n } from '@kbn/i18n';
import { EuiPanel } from '@elastic/eui';
import { EuiTitle } from '@elastic/eui';
import { EuiIconTip } from '@elastic/eui';
import { ALERT_DURATION, ALERT_END } from '@kbn/rule-data-utils';
import {
ALERT_DURATION,
ALERT_END,
ALERT_EVALUATION_THRESHOLD,
ALERT_RULE_TYPE_ID,
} from '@kbn/rule-data-utils';
import moment from 'moment';
import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
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';
Expand All @@ -41,20 +45,25 @@ import {
SERVICE_NAME,
TRANSACTION_TYPE,
} from './types';
import { getAggsTypeFromRule } from './helpers';
import { getAggsTypeFromRule, isLatencyThresholdRuleType } from './helpers';
import { filterNil } from '../../../shared/charts/latency_chart';
import { errorRateI18n } from '../../../shared/charts/failed_transaction_rate_chart';
import {
AlertActiveRect,
AlertAnnotation,
AlertThresholdRect,
AlertThresholdAnnotation,
} from './latency_chart_components';
import { SERVICE_ENVIRONMENT } from '../../../../../common/es_fields/apm';

export function AlertDetailsAppSection({
rule,
alert,
timeZone,
}: AlertDetailsAppSectionProps) {
const params = rule.params;
const environment = String(params.environment) || ENVIRONMENT_ALL.value;
const latencyAggregationType = getAggsTypeFromRule(
params.aggregationType as string
);
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;
Expand Down Expand Up @@ -103,7 +112,7 @@ export function AlertDetailsAppSection({
});

const transactionType = getTransactionType({
transactionType: String(alert.fields[TRANSACTION_TYPE]),
transactionType: alert.fields[TRANSACTION_TYPE],
transactionTypes,
agentName,
});
Expand Down Expand Up @@ -289,9 +298,32 @@ export function AlertDetailsAppSection({
}),
},
];

/* Error Rate */

const getLatencyChartAdditionalData = () => {
if (isLatencyThresholdRuleType(alert.fields[ALERT_RULE_TYPE_ID])) {
return [
<AlertThresholdRect
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]}
/>,
];
}
};

return (
<EuiFlexGroup direction="column" gutterSize="s">
<ChartPointerEventContextProvider>
Expand All @@ -313,6 +345,7 @@ export function AlertDetailsAppSection({
</EuiFlexGroup>
<TimeseriesChart
id="latencyChart"
annotations={getLatencyChartAdditionalData()}
height={200}
comparisonEnabled={comparisonEnabled}
offset={offset}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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 const DEFAULT_DATE_FORMAT = 'HH:mm:ss';
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ export const getAggsTypeFromRule = (
if (ruleAggType === '99th') return LatencyAggregationType.p99;
return LatencyAggregationType.avg;
};

export const isLatencyThresholdRuleType = (ruleTypeId: string) =>
ruleTypeId === 'apm.transaction_duration';
Original file line number Diff line number Diff line change
@@ -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 { 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 }}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 { 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: '#f00',
opacity: 1,
},
}}
marker={<EuiIcon type="alert" color="red" />}
markerPosition={Position.Top}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 } from '@elastic/charts';

export function AlertThresholdAnnotation({
threshold,
}: {
threshold?: number;
}) {
if (!threshold) return <></>;

return (
<LineAnnotation
id="annotation_alert_threshold"
domainType={AnnotationDomainType.YDomain}
dataValues={[
{
dataValue: threshold,
header: String(threshold),
},
]}
style={{
line: {
opacity: 0.5,
strokeWidth: 1,
stroke: 'red',
},
}}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 }}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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';
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@
import { Rule } from '@kbn/alerting-plugin/common';
import { TopAlert } from '@kbn/observability-plugin/public/pages/alerts';
import { TIME_UNITS } from '@kbn/triggers-actions-ui-plugin/public';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import { SERVICE_ENVIRONMENT } from '../../../../../common/es_fields/apm';

export const SERVICE_NAME = 'service.name' as const;
export const TRANSACTION_TYPE = 'transaction.type' as const;
export interface AlertDetailsAppSectionProps {
rule: Rule<{
environment: string;
aggregationType: LatencyAggregationType;
aggregationType: string;
windowSize: number;
windowUnit: TIME_UNITS;
}>;
alert: TopAlert<{ [SERVICE_NAME]: string; [TRANSACTION_TYPE]: string }>;
alert: TopAlert<{
[SERVICE_NAME]: string;
[TRANSACTION_TYPE]: string;
[SERVICE_ENVIRONMENT]: string;
}>;
timeZone: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

import {
AnnotationDomainType,
AreaSeries,
Axis,
BarSeries,
Expand All @@ -26,12 +25,11 @@ import {
} from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import React, { ReactElement } from 'react';
import { useHistory } from 'react-router-dom';
import { useChartTheme } from '@kbn/observability-plugin/public';
import { isExpectedBoundsComparison } from '../time_comparison/get_comparison_options';
import { asAbsoluteDateTime } from '../../../../common/utils/formatters';
import { useAnnotationsContext } from '../../../context/annotations/use_annotations_context';

import { useChartPointerEventContext } from '../../../context/chart_pointer_event/use_chart_pointer_event_context';
import { useTheme } from '../../../hooks/use_theme';
import { unit } from '../../../utils/style';
Expand All @@ -51,6 +49,9 @@ interface TimeseriesChartProps extends TimeseriesChartWithContextProps {
comparisonEnabled: boolean;
offset?: string;
timeZone: string;
annotations?: Array<
ReactElement<typeof RectAnnotation | typeof LineAnnotation>
>;
}
export function TimeseriesChart({
id,
Expand All @@ -67,9 +68,9 @@ export function TimeseriesChart({
comparisonEnabled,
offset,
timeZone,
annotations,
}: TimeseriesChartProps) {
const history = useHistory();
const { annotations } = useAnnotationsContext();
const { chartRef, updatePointerEvent } = useChartPointerEventContext();
const theme = useTheme();
const chartTheme = useChartTheme();
Expand All @@ -79,7 +80,6 @@ export function TimeseriesChart({
anomalyTimeseriesColor: anomalyTimeseries?.color,
});
const isEmpty = isTimeseriesEmpty(timeseries);
const annotationColor = theme.eui.euiColorSuccess;
const isComparingExpectedBounds =
comparisonEnabled && isExpectedBoundsComparison(offset);
const allSeries = [
Expand Down Expand Up @@ -219,26 +219,7 @@ export function TimeseriesChart({
tickFormat={yTickFormat ? yTickFormat : yLabelFormat}
labelFormat={yLabelFormat}
/>

{showAnnotations && (
<LineAnnotation
id="annotations"
domainType={AnnotationDomainType.XDomain}
dataValues={annotations.map((annotation) => ({
dataValue: annotation['@timestamp'],
header: asAbsoluteDateTime(annotation['@timestamp']),
details: `${i18n.translate('xpack.apm.chart.annotation.version', {
defaultMessage: 'Version',
})} ${annotation.text}`,
}))}
style={{
line: { strokeWidth: 1, stroke: annotationColor, opacity: 1 },
}}
marker={<EuiIcon type="dot" color={annotationColor} />}
markerPosition={Position.Top}
/>
)}

{showAnnotations && annotations}
<RectAnnotation
id="__endzones__"
zIndex={2}
Expand All @@ -250,7 +231,6 @@ export function TimeseriesChart({
]}
style={endZoneRectAnnotationStyle}
/>

{allSeries.map((serie) => {
const Series = getChartType(serie.type);

Expand Down
Loading

0 comments on commit 12ae75f

Please sign in to comment.