Skip to content

Commit

Permalink
Add anomalies section and overall chart
Browse files Browse the repository at this point in the history
  • Loading branch information
Kerry350 committed Oct 7, 2019
1 parent 9793f4c commit 0cefd23
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { useTrackPageview } from '../../../hooks/use_track_metric';
import { FirstUseCallout } from './first_use';
import { LogRateResults } from './sections/log_rate';
import { AnomaliesResults } from './sections/anomalies';

export const AnalysisResultsContent = ({
sourceId,
Expand Down Expand Up @@ -150,7 +151,14 @@ export const AnalysisResultsContent = ({
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="l">Anomalies</EuiPanel>
<EuiPanel paddingSize="l">
<AnomaliesResults
isLoading={isLoading}
results={logEntryRate}
setTimeRange={handleChartTimeRangeChange}
timeRange={queryTimeRange}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPage>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { RectAnnotationDatum } from '@elastic/charts';
import {
Axis,
BarSeries,
Chart,
getAxisId,
getSpecId,
niceTimeFormatter,
Settings,
TooltipValue,
LIGHT_THEME,
DARK_THEME,
getAnnotationId,
RectAnnotation,
} from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import React, { useCallback, useMemo } from 'react';

import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting';

export const AnomaliesChart: React.FunctionComponent<{
chartId: string;
setTimeRange: (timeRange: TimeRange) => void;
timeRange: TimeRange;
series: Array<{ time: number; value: number }>;
annotations: RectAnnotationDatum[];
}> = ({ chartId, series, annotations, setTimeRange, timeRange }) => {
const [dateFormat] = useKibanaUiSetting('dateFormat');
const [isDarkMode] = useKibanaUiSetting('theme:darkMode');

const chartDateFormatter = useMemo(
() => niceTimeFormatter([timeRange.startTime, timeRange.endTime]),
[timeRange]
);

const logEntryRateSpecId = getSpecId('averageValues');

const tooltipProps = useMemo(
() => ({
headerFormatter: (tooltipData: TooltipValue) =>
moment(tooltipData.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'),
}),
[dateFormat]
);

const handleBrushEnd = useCallback(
(startTime: number, endTime: number) => {
setTimeRange({
endTime,
startTime,
});
},
[setTimeRange]
);
const chartAnnotationsId = getAnnotationId(`anomalies-${chartId}`);

return (
<div style={{ height: 400, width: '100%' }}>
<Chart className="log-entry-rate-chart">
<Axis
id={getAxisId('timestamp')}
title={i18n.translate('xpack.infra.logs.analysis.anomaliesSectionXaxisTitle', {
defaultMessage: 'Time',
})}
position="bottom"
showOverlappingTicks
tickFormat={chartDateFormatter}
/>
<Axis
id={getAxisId('values')}
title={i18n.translate('xpack.infra.logs.analysis.anomaliesSectionYaxisTitle', {
defaultMessage: 'Log entries per 15 minutes',
})}
position="left"
tickFormat={value => Number(value).toFixed(0)}
/>
<BarSeries
id={logEntryRateSpecId}
name={i18n.translate('xpack.infra.logs.analysis.anomaliesSectionLineSeriesName', {
defaultMessage: 'Log entries per 15 minutes (avg)',
})}
xScaleType="time"
yScaleType="linear"
xAccessor={'time'}
yAccessors={['value']}
data={series}
/>
<RectAnnotation dataValues={annotations} annotationId={chartAnnotationsId} />
<Settings
onBrushEnd={handleBrushEnd}
tooltip={tooltipProps}
theme={isDarkMode ? DARK_THEME : LIGHT_THEME}
xDomain={{ min: timeRange.startTime, max: timeRange.endTime }}
/>
</Chart>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { RectAnnotationDatum } from '@elastic/charts';
import {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingChart,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';

import { GetLogEntryRateSuccessResponsePayload } from '../../../../../../common/http_api/log_analysis/results/log_entry_rate';
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
import { AnomaliesChart } from './chart';

export const AnomaliesResults = ({
isLoading,
results,
setTimeRange,
timeRange,
}: {
isLoading: boolean;
results: GetLogEntryRateSuccessResponsePayload['data'] | null;
setTimeRange: (timeRange: TimeRange) => void;
timeRange: TimeRange;
}) => {
const title = i18n.translate('xpack.infra.logs.analysis.anomaliesSectionTitle', {
defaultMessage: 'Anomalies',
});

const loadingAriaLabel = i18n.translate(
'xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel',
{ defaultMessage: 'Loading anomalies' }
);

const logEntryRateSeries = useMemo(
() =>
results && results.histogramBuckets
? results.histogramBuckets.reduce<Array<{ time: number; value: number }>>(
(buckets, bucket) => {
return [
...buckets,
{
time: bucket.startTime,
value: bucket.dataSets.reduce((accumulatedValue, dataSet) => {
return accumulatedValue + dataSet.averageActualLogEntryRate;
}, 0),
},
];
},
[]
)
: [],
[results]
);

const logEntryRateAnomalyAnnotations = useMemo(
() =>
results && results.histogramBuckets
? results.histogramBuckets.reduce<RectAnnotationDatum[]>((annotatedBuckets, bucket) => {
const anomalies = bucket.dataSets.reduce<typeof bucket['dataSets'][0]['anomalies']>(
(accumulatedAnomalies, dataSet) => [...accumulatedAnomalies, ...dataSet.anomalies],
[]
);
if (anomalies.length <= 0) {
return annotatedBuckets;
}
return [
...annotatedBuckets,
{
coordinates: {
x0: bucket.startTime,
x1: bucket.startTime + results.bucketDuration,
},
details: i18n.translate(
'xpack.infra.logs.analysis.anomalySectionAnomalyCountTooltipLabel',
{
defaultMessage: `{anomalyCount, plural, one {# anomaly} other {# anomalies}}`,
values: {
anomalyCount: anomalies.length,
},
}
),
},
];
}, [])
: [],
[results]
);

return (
<>
<EuiTitle size="s" aria-label={title}>
<h2>{title}</h2>
</EuiTitle>
<EuiSpacer size="l" />
{isLoading ? (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingChart size="xl" aria-label={loadingAriaLabel} />
</EuiFlexItem>
</EuiFlexGroup>
) : !results || (results && results.histogramBuckets && !results.histogramBuckets.length) ? (
<EuiEmptyPrompt
title={
<h2>
{i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataTitle', {
defaultMessage: 'There is no data to display.',
})}
</h2>
}
titleSize="m"
body={
<p>
{i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataBody', {
defaultMessage: 'You may want to adjust your time range.',
})}
</p>
}
/>
) : (
<AnomaliesChart
chartId="overall"
setTimeRange={setTimeRange}
timeRange={timeRange}
series={logEntryRateSeries}
annotations={logEntryRateAnomalyAnnotations}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@ import {
Axis,
BarSeries,
Chart,
getAnnotationId,
getAxisId,
getSpecId,
niceTimeFormatter,
RectAnnotation,
RectAnnotationDatum,
Settings,
TooltipValue,
LIGHT_THEME,
Expand All @@ -23,18 +20,14 @@ import { i18n } from '@kbn/i18n';
import moment from 'moment';
import React, { useCallback, useMemo } from 'react';

import { GetLogEntryRateSuccessResponsePayload } from '../../../../../../common/http_api/log_analysis/results/log_entry_rate';
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting';

type LogEntryRateHistogramBuckets = GetLogEntryRateSuccessResponsePayload['data']['histogramBuckets'];

export const LogEntryRateBarChart: React.FunctionComponent<{
bucketDuration: number;
histogramBuckets: LogEntryRateHistogramBuckets | null;
setTimeRange: (timeRange: TimeRange) => void;
timeRange: TimeRange;
}> = ({ bucketDuration, histogramBuckets, setTimeRange, timeRange }) => {
series: Array<{ group: string; time: number; value: number }>;
}> = ({ series, setTimeRange, timeRange }) => {
const [dateFormat] = useKibanaUiSetting('dateFormat');
const [isDarkMode] = useKibanaUiSetting('theme:darkMode');

Expand All @@ -43,62 +36,7 @@ export const LogEntryRateBarChart: React.FunctionComponent<{
[timeRange]
);

const logEntryRateSeries = useMemo(
() =>
histogramBuckets
? histogramBuckets.reduce<Array<{ group: string; time: number; value: number }>>(
(buckets, bucket) => {
return [
...buckets,
...bucket.dataSets.map(dataSet => ({
group: dataSet.dataSetId === '' ? 'unknown' : dataSet.dataSetId,
time: bucket.startTime,
value: dataSet.averageActualLogEntryRate,
})),
];
},
[]
)
: [],
[histogramBuckets]
);

const logEntryRateAnomalyAnnotations = useMemo(
() =>
histogramBuckets
? histogramBuckets.reduce<RectAnnotationDatum[]>((annotatedBuckets, bucket) => {
const anomalies = bucket.dataSets.reduce<typeof bucket['dataSets'][0]['anomalies']>(
(accumulatedAnomalies, dataSet) => [...accumulatedAnomalies, ...dataSet.anomalies],
[]
);
if (anomalies.length <= 0) {
return annotatedBuckets;
}
return [
...annotatedBuckets,
{
coordinates: {
x0: bucket.startTime,
x1: bucket.startTime + bucketDuration,
},
details: i18n.translate(
'xpack.infra.logs.analysis.logRateSectionAnomalyCountTooltipLabel',
{
defaultMessage: `{anomalyCount, plural, one {# anomaly} other {# anomalies}}`,
values: {
anomalyCount: anomalies.length,
},
}
),
},
];
}, [])
: [],
[histogramBuckets]
);

const logEntryRateSpecId = getSpecId('averageValues');
const logEntryRateAnomalyAnnotationsId = getAnnotationId('anomalies');

const tooltipProps = useMemo(
() => ({
Expand Down Expand Up @@ -149,11 +87,7 @@ export const LogEntryRateBarChart: React.FunctionComponent<{
yAccessors={['value']}
splitSeriesAccessors={['group']}
stackAccessors={['time']}
data={logEntryRateSeries}
/>
<RectAnnotation
dataValues={logEntryRateAnomalyAnnotations}
annotationId={logEntryRateAnomalyAnnotationsId}
data={series}
/>
<Settings
onBrushEnd={handleBrushEnd}
Expand Down
Loading

0 comments on commit 0cefd23

Please sign in to comment.