Skip to content

Commit

Permalink
feat(slo): burn rate alert details page (elastic#174548)
Browse files Browse the repository at this point in the history
  • Loading branch information
kdelemme authored and CoenWarmer committed Feb 15, 2024
1 parent e6279d5 commit 8551799
Show file tree
Hide file tree
Showing 20 changed files with 788 additions and 289 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import { AggregationsDateHistogramBucketKeys } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { type HttpSetup } from '@kbn/core/public';
import {
ALERT_DURATION,
ALERT_RULE_UUID,
Expand All @@ -13,10 +15,8 @@ import {
ALERT_TIME_RANGE,
ValidFeatureId,
} from '@kbn/rule-data-utils';
import { type HttpSetup } from '@kbn/core/public';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
import { useQuery } from '@tanstack/react-query';
import { AggregationsDateHistogramBucketKeys } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';

export interface Props {
http: HttpSetup | undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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, EuiLink } from '@elastic/eui';
import { Rule } from '@kbn/alerting-plugin/common';
import { i18n } from '@kbn/i18n';
import React, { useEffect } from 'react';
import { useFetchSloDetails } from '../../../../hooks/slo/use_fetch_slo_details';
import { AlertSummaryField } from '../../../../pages/alert_details/components/alert_summary';
import { TopAlert } from '../../../../typings/alerts';
import { BurnRateRuleParams } from '../../../../typings/slo';
import { useKibana } from '../../../../utils/kibana_react';
import { AlertsHistoryPanel } from './components/alerts_history/alerts_history_panel';
import { ErrorRatePanel } from './components/error_rate/error_rate_panel';

export type BurnRateRule = Rule<BurnRateRuleParams>;
export type BurnRateAlert = TopAlert;

interface AppSectionProps {
alert: BurnRateAlert;
rule: BurnRateRule;
ruleLink: string;
setAlertSummaryFields: React.Dispatch<React.SetStateAction<AlertSummaryField[] | undefined>>;
}

// eslint-disable-next-line import/no-default-export
export default function AlertDetailsAppSection({
alert,
rule,
ruleLink,
setAlertSummaryFields,
}: AppSectionProps) {
const {
services: {
http: { basePath },
},
} = useKibana();

const sloId = alert.fields['kibana.alert.rule.parameters']!.sloId as string;
const instanceId = alert.fields['kibana.alert.instance.id']!;
const { isLoading, data: slo } = useFetchSloDetails({ sloId, instanceId });

useEffect(() => {
setAlertSummaryFields([
{
label: i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.summaryField.slo',
{
defaultMessage: 'Source SLO',
}
),
value: (
<EuiLink data-test-subj="sloLink" href={basePath.prepend(alert.link!)}>
{slo?.name ?? '-'}
</EuiLink>
),
},
{
label: i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.summaryField.rule',
{
defaultMessage: 'Rule',
}
),
value: (
<EuiLink data-test-subj="ruleLink" href={ruleLink}>
{rule.name}
</EuiLink>
),
},
]);
}, [alert, rule, ruleLink, setAlertSummaryFields, basePath, slo]);

return (
<EuiFlexGroup direction="column" data-test-subj="overviewSection">
<ErrorRatePanel alert={alert} slo={slo} isLoading={isLoading} />
<AlertsHistoryPanel alert={alert} rule={rule} slo={slo} isLoading={isLoading} />
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
* 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,
EuiIcon,
EuiLink,
EuiLoadingChart,
EuiLoadingSpinner,
EuiPanel,
EuiStat,
EuiText,
EuiTextColor,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useAlertsHistory } from '@kbn/observability-alert-details';
import rison from '@kbn/rison';
import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
import { GetSLOResponse } from '@kbn/slo-schema';
import moment from 'moment';
import React from 'react';
import { convertTo } from '../../../../../../../common/utils/formatters';
import { WindowSchema } from '../../../../../../typings';
import { useKibana } from '../../../../../../utils/kibana_react';
import { ErrorRateChart } from '../../../../error_rate_chart';
import { BurnRateAlert, BurnRateRule } from '../../alert_details_app_section';
import { getActionGroupFromReason } from '../../utils/alert';

interface Props {
slo?: GetSLOResponse;
alert: BurnRateAlert;
rule: BurnRateRule;
isLoading: boolean;
}

export function AlertsHistoryPanel({ rule, slo, alert, isLoading }: Props) {
const {
services: { http },
} = useKibana();
const { isLoading: isAlertsHistoryLoading, data } = useAlertsHistory({
featureIds: ['slo'],
ruleId: rule.id,
dateRange: {
from: 'now-30d',
to: 'now',
},
http,
});

const actionGroup = getActionGroupFromReason(alert.reason);
const actionGroupWindow = (
(alert.fields[ALERT_RULE_PARAMETERS]?.windows ?? []) as WindowSchema[]
).find((window: WindowSchema) => window.actionGroup === actionGroup);
const dataTimeRange = {
from: moment().subtract(30, 'day').toDate(),
to: new Date(),
};

function getAlertsLink() {
const kuery = `kibana.alert.rule.uuid:"${rule.id}"`;
return http.basePath.prepend(`/app/observability/alerts?_a=${rison.encode({ kuery })}`);
}

if (isLoading) {
return <EuiLoadingChart size="m" mono data-test-subj="loading" />;
}

if (!slo) {
return null;
}

return (
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="alertsHistoryPanel">
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexGroup direction="row" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.title',
{ defaultMessage: '{sloName} alerts history', values: { sloName: slo.name } }
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink color="text" href={getAlertsLink()} data-test-subj="alertsLink">
<EuiIcon type="sortRight" style={{ marginRight: '4px' }} />
<FormattedMessage
id="xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.alertsLink"
defaultMessage="View alerts"
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>

<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<span>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.subtitle',
{
defaultMessage: 'Last 30 days',
}
)}
</span>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>

<EuiFlexGroup direction="row" gutterSize="m" justifyContent="flexStart">
<EuiFlexItem grow={false}>
<EuiStat
title={
isAlertsHistoryLoading ? (
<EuiLoadingSpinner size="s" />
) : data.totalTriggeredAlerts ? (
data.totalTriggeredAlerts
) : (
'-'
)
}
titleColor="danger"
titleSize="m"
textAlign="left"
isLoading={isLoading}
data-test-subj="alertsTriggeredStats"
reverse
description={
<EuiTextColor color="default">
<span>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.triggeredAlertsStatsTitle',
{ defaultMessage: 'Alerts triggered' }
)}
</span>
</EuiTextColor>
}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiStat
title={
isAlertsHistoryLoading ? (
<EuiLoadingSpinner size="s" />
) : data.avgTimeToRecoverUS ? (
convertTo({
unit: 'minutes',
microseconds: data.avgTimeToRecoverUS,
extended: true,
}).formatted
) : (
'-'
)
}
titleColor="default"
titleSize="m"
textAlign="left"
isLoading={isLoading}
data-test-subj="avgTimeToRecoverStat"
reverse
description={
<EuiTextColor color="default">
<span>
{i18n.translate(
'xpack.observability.slo.burnRateRule.alertDetailsAppSection.alertsHistory.avgTimeToRecoverStatsTitle',
{ defaultMessage: 'Avg time to recover' }
)}
</span>
</EuiTextColor>
}
/>
</EuiFlexItem>
</EuiFlexGroup>

<EuiFlexGroup direction="row" gutterSize="m" justifyContent="flexStart">
<EuiFlexItem>
{isAlertsHistoryLoading ? (
<EuiLoadingSpinner size="s" />
) : (
<ErrorRateChart
slo={slo}
dataTimeRange={dataTimeRange}
threshold={actionGroupWindow!.burnRateThreshold}
annotations={data.histogramTriggeredAlerts
.filter((a) => a.doc_count > 0)
.map((a) => ({
date: new Date(a.key_as_string!),
total: a.doc_count,
}))}
showErrorRateAsLine
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
</EuiPanel>
);
}
Loading

0 comments on commit 8551799

Please sign in to comment.