Skip to content

Commit

Permalink
[AO] - Add Logs history chart to the Logs Alert Details page (#153930)
Browse files Browse the repository at this point in the history
## Summary

It closes #150854 by

- Add optional annotations to the prereview chart
- Add the Logs history chart 

<img width="941" alt="Screenshot 2023-03-29 at 17 09 36"
src="https://user-images.githubusercontent.com/6838659/228584016-f73efef0-03e6-4777-b2df-17f13166c77b.png">

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
  • Loading branch information
fkanout authored Apr 3, 2023
1 parent 662507b commit 64d53bd
Show file tree
Hide file tree
Showing 6 changed files with 416 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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 { AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts';
import moment from 'moment';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-theme';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana';
export function AlertAnnotation({ alertStarted }: { alertStarted: number }) {
const { uiSettings } = useKibanaContextForPlugin().services;

return (
<LineAnnotation
id="annotation_alert_started"
domainType={AnnotationDomainType.XDomain}
dataValues={[
{
dataValue: alertStarted,
header: moment(alertStarted).format(uiSettings.get(UI_SETTINGS.DATE_FORMAT)),
details: i18n.translate('xpack.infra.logs.alertDetails.chartAnnotation.alertStarted', {
defaultMessage: 'Alert started',
}),
},
]}
style={{
line: {
strokeWidth: 3,
stroke: euiThemeVars.euiColorDangerText,
opacity: 1,
},
}}
marker={<EuiIcon type="warning" color="danger" />}
markerPosition={Position.Top}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* 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 moment from 'moment';
import React from 'react';
import { Rule } from '@kbn/alerting-plugin/common';
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { convertTo, TopAlert } from '@kbn/observability-plugin/public';
import { AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts';
import { EuiIcon, EuiBadge } from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import { AlertConsumers } from '@kbn/rule-data-utils';
import DateMath from '@kbn/datemath';
import { useAlertsHistory } from '../../../../../hooks/use_alerts_history';
import { type PartialCriterion } from '../../../../../../common/alerting/logs/log_threshold';
import { CriterionPreview } from '../../expression_editor/criterion_preview_chart';
import { PartialRuleParams } from '../../../../../../common/alerting/logs/log_threshold';

const LogsHistoryChart = ({
rule,
alert,
}: {
rule: Rule<PartialRuleParams>;
alert: TopAlert<Record<string, any>>;
}) => {
// Show the Logs History Chart ONLY if we have one criteria
// So always pull the first criteria
const criteria = rule.params.criteria[0];

const dateRange = {
from: 'now-30d',
to: 'now',
};
const executionTimeRange = {
gte: DateMath.parse(dateRange.from)!.valueOf(),
lte: DateMath.parse(dateRange.to, { roundUp: true })!.valueOf(),
};

const { alertsHistory } = useAlertsHistory({
featureIds: [AlertConsumers.LOGS],
ruleId: rule.id,
dateRange,
});
const alertHistoryAnnotations =
alertsHistory?.histogramTriggeredAlerts
.filter((annotation) => annotation.doc_count > 0)
.map((annotation) => {
return {
dataValue: annotation.key,
header: String(annotation.doc_count),
// Only the date(without time) is needed here, uiSettings don't provide that
details: moment(annotation.key_as_string).format('yyyy-MM-DD'),
};
}) || [];

return (
<EuiPanel hasBorder={true} data-test-subj="logsHistoryChartAlertDetails">
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.infra.logs.alertDetails.chartHistory.chartTitle', {
defaultMessage: 'Logs threshold alerts history',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
{i18n.translate('xpack.infra.logs.alertDetails.chartHistory.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>{alertsHistory?.totalTriggeredAlerts || '-'}</h3>
</EuiTitle>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
{i18n.translate('xpack.infra.logs.alertDetails.chartHistory.alertsTriggered', {
defaultMessage: 'Alerts triggered',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexGroup gutterSize="xs" direction="column">
<EuiFlexItem grow={false}>
<EuiText>
<EuiTitle size="s">
<h3>
{alertsHistory?.avgTimeToRecoverUS
? convertTo({
unit: 'minutes',
microseconds: alertsHistory?.avgTimeToRecoverUS,
extended: true,
}).formatted
: '-'}
</h3>
</EuiTitle>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
{i18n.translate('xpack.infra.logs.alertDetails.chartHistory.avgTimeToRecover', {
defaultMessage: 'Avg time to recover',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
<EuiSpacer size="s" />
<CriterionPreview
annotations={[
<LineAnnotation
id="annotations"
key={'annotationsAlertHistory'}
domainType={AnnotationDomainType.XDomain}
dataValues={alertHistoryAnnotations}
style={{
line: {
strokeWidth: 3,
stroke: euiThemeVars.euiColorDangerText,
opacity: 1,
},
}}
marker={<EuiIcon type="warning" color="danger" />}
markerBody={(annotationData) => (
<>
<EuiBadge color="danger">
<EuiText size="xs" color="white">
{annotationData.header}
</EuiText>
</EuiBadge>
<EuiSpacer size="xs" />
</>
)}
markerPosition={Position.Top}
/>,
]}
ruleParams={rule.params}
logViewReference={rule.params.logView}
chartCriterion={criteria as PartialCriterion}
showThreshold={true}
executionTimeRange={executionTimeRange}
/>
</EuiPanel>
);
};
// eslint-disable-next-line import/no-default-export
export default LogsHistoryChart;
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import moment from 'moment';
import React from 'react';
import { type PartialCriterion } from '../../../../../common/alerting/logs/log_threshold';
import { CriterionPreview } from '../expression_editor/criterion_preview_chart';
import { AlertAnnotation } from './components/alert_annotation';
import { AlertDetailsAppSectionProps } from './types';

const LogsHistoryChart = React.lazy(() => import('./components/logs_history_chart'));

const AlertDetailsAppSection = ({ rule, alert }: AlertDetailsAppSectionProps) => {
const ruleWindowSizeMS = moment
.duration(rule.params.timeSize, rule.params.timeUnit)
Expand All @@ -34,13 +37,12 @@ const AlertDetailsAppSection = ({ rule, alert }: AlertDetailsAppSectionProps) =>

return (
// Create a chart per-criteria
<EuiFlexGroup>
{rule.params.criteria.map((criteria) => {
<EuiFlexGroup direction="column">
{rule.params.criteria.map((criteria, idx) => {
const chartCriterion = criteria as PartialCriterion;
return (
<EuiFlexItem>
<EuiFlexItem key={`${chartCriterion.field}${idx}`}>
<CriterionPreview
key={chartCriterion.field}
ruleParams={rule.params}
logViewReference={{
type: 'log-view-reference',
Expand All @@ -49,10 +51,17 @@ const AlertDetailsAppSection = ({ rule, alert }: AlertDetailsAppSectionProps) =>
chartCriterion={chartCriterion}
showThreshold={true}
executionTimeRange={{ gte: rangeFrom, lte: rangeTo }}
annotations={[<AlertAnnotation alertStarted={alert.start} />]}
/>
</EuiFlexItem>
);
})}
{/* For now we show the history chart only if we have one criteria */}
{rule.params.criteria.length === 1 && (
<EuiFlexItem>
<LogsHistoryChart alert={alert} rule={rule} />
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useMemo } from 'react';
import React, { ReactElement, useMemo } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import {
ScaleType,
Expand Down Expand Up @@ -59,6 +59,7 @@ interface Props {
logViewReference: PersistedLogViewReference;
showThreshold: boolean;
executionTimeRange?: ExecutionTimeRange;
annotations?: Array<ReactElement<typeof RectAnnotation | typeof LineAnnotation>>;
}

export const CriterionPreview: React.FC<Props> = ({
Expand All @@ -67,6 +68,7 @@ export const CriterionPreview: React.FC<Props> = ({
logViewReference,
showThreshold,
executionTimeRange,
annotations,
}) => {
const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => {
const { field, comparator, value } = chartCriterion;
Expand Down Expand Up @@ -111,6 +113,7 @@ export const CriterionPreview: React.FC<Props> = ({
chartAlertParams={chartAlertParams}
showThreshold={showThreshold}
executionTimeRange={executionTimeRange}
annotations={annotations}
/>
);
};
Expand All @@ -122,6 +125,7 @@ interface ChartProps {
chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset;
showThreshold: boolean;
executionTimeRange?: ExecutionTimeRange;
annotations?: Array<ReactElement<typeof RectAnnotation | typeof LineAnnotation>>;
}

const CriterionPreviewChart: React.FC<ChartProps> = ({
Expand All @@ -131,6 +135,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
chartAlertParams,
showThreshold,
executionTimeRange,
annotations,
}) => {
const { uiSettings } = useKibana().services;
const isDarkMode = uiSettings?.get('theme:darkMode') || false;
Expand Down Expand Up @@ -287,6 +292,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
]}
/>
) : null}
{annotations}
{showThreshold && threshold && isAbove ? (
<RectAnnotation
id="above-threshold"
Expand Down
Loading

0 comments on commit 64d53bd

Please sign in to comment.