Skip to content

Commit

Permalink
[Logs UI] Adapt log entry rate data visualisations (#47558)
Browse files Browse the repository at this point in the history
This adapts the log rate page to the new sets of data visualisations.

closes #47201
  • Loading branch information
Kerry350 authored and weltenwort committed Oct 15, 2019
1 parent 533162f commit 1fa1e54
Show file tree
Hide file tree
Showing 15 changed files with 1,076 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,25 @@ export const logEntryRateAnomaly = rt.type({
typicalLogEntryRate: rt.number,
});

export const logEntryRateDataSetRT = rt.type({
export const logEntryRatePartitionRT = rt.type({
analysisBucketCount: rt.number,
anomalies: rt.array(logEntryRateAnomaly),
averageActualLogEntryRate: rt.number,
dataSetId: rt.string,
maximumAnomalyScore: rt.number,
numberOfLogEntries: rt.number,
partitionId: rt.string,
});

export const logEntryRateHistogramBucket = rt.type({
dataSets: rt.array(logEntryRateDataSetRT),
partitions: rt.array(logEntryRatePartitionRT),
startTime: rt.number,
});

export const getLogEntryRateSuccessReponsePayloadRT = rt.type({
data: rt.type({
bucketDuration: rt.number,
histogramBuckets: rt.array(logEntryRateHistogramBucket),
totalNumberOfLogEntries: rt.number,
}),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPanel,
EuiSuperDatePicker,
EuiBadge,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import numeral from '@elastic/numeral';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import React, { useCallback, useMemo, useState } from 'react';

import euiStyled from '../../../../../../common/eui_styled_components';
import { TimeRange } from '../../../../common/http_api/shared/time_range';
import { bucketSpan } from '../../../../common/log_analysis';
import euiStyled from '../../../../../../common/eui_styled_components';
import { LoadingPage } from '../../../components/loading_page';
import {
StringTimeRange,
Expand All @@ -31,6 +31,8 @@ import {
import { useTrackPageview } from '../../../hooks/use_track_metric';
import { FirstUseCallout } from './first_use';
import { LogRateResults } from './sections/log_rate';
import { AnomaliesResults } from './sections/anomalies';
import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting';

export const AnalysisResultsContent = ({
sourceId,
Expand All @@ -42,6 +44,8 @@ export const AnalysisResultsContent = ({
useTrackPageview({ app: 'infra_logs', path: 'analysis_results' });
useTrackPageview({ app: 'infra_logs', path: 'analysis_results', delay: 15000 });

const [dateFormat] = useKibanaUiSetting('dateFormat', 'MMMM D, YYYY h:mm A');

const {
timeRange: selectedTimeRange,
setTimeRange: setSelectedTimeRange,
Expand All @@ -56,13 +60,13 @@ export const AnalysisResultsContent = ({
const bucketDuration = useMemo(() => {
// This function takes the current time range in ms,
// works out the bucket interval we'd need to always
// display 200 data points, and then takes that new
// display 100 data points, and then takes that new
// value and works out the nearest multiple of
// 900000 (15 minutes) to it, so that we don't end up with
// jaggy bucket boundaries between the ML buckets and our
// aggregation buckets.
const msRange = moment(queryTimeRange.endTime).diff(moment(queryTimeRange.startTime));
const bucketIntervalInMs = msRange / 200;
const bucketIntervalInMs = msRange / 100;
const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan);
const roundedResult = parseInt(Number(result).toFixed(0), 10);
return roundedResult < bucketSpan ? bucketSpan : roundedResult;
Expand Down Expand Up @@ -130,39 +134,71 @@ export const AnalysisResultsContent = ({
/>
) : (
<>
<EuiPage>
<EuiPanel paddingSize="l">
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem></EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSuperDatePicker
start={selectedTimeRange.startTime}
end={selectedTimeRange.endTime}
onTimeChange={handleSelectedTimeRangeChange}
isPaused={autoRefresh.isPaused}
refreshInterval={autoRefresh.interval}
onRefreshChange={handleAutoRefreshChange}
onRefresh={handleQueryTimeRangeChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiPage>
<ExpandingPage>
<EuiPageBody>
<EuiPageContent>
<EuiPageContentBody>
<ResultsContentPage>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="l">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
{!isLoading && logEntryRate ? (
<EuiText size="s">
<FormattedMessage
id="xpack.infra.logs.analysis.logRateResultsToolbarText"
defaultMessage="Analyzed {numberOfLogs} log entries from {startTime} to {endTime}"
values={{
numberOfLogs: (
<EuiBadge color="primary">
<EuiText size="s" color="ghost">
{numeral(logEntryRate.totalNumberOfLogEntries).format('0.00a')}
</EuiText>
</EuiBadge>
),
startTime: (
<b>{moment(queryTimeRange.startTime).format(dateFormat)}</b>
),
endTime: <b>{moment(queryTimeRange.endTime).format(dateFormat)}</b>,
}}
/>
</EuiText>
) : null}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSuperDatePicker
start={selectedTimeRange.startTime}
end={selectedTimeRange.endTime}
onTimeChange={handleSelectedTimeRangeChange}
isPaused={autoRefresh.isPaused}
refreshInterval={autoRefresh.interval}
onRefreshChange={handleAutoRefreshChange}
onRefresh={handleQueryTimeRangeChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="l">
{isFirstUse && !hasResults ? <FirstUseCallout /> : null}
<LogRateResults
isLoading={isLoading}
results={logEntryRate}
setTimeRange={handleChartTimeRangeChange}
timeRange={queryTimeRange}
/>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</ExpandingPage>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="l">
<AnomaliesResults
isLoading={isLoading}
results={logEntryRate}
setTimeRange={handleChartTimeRangeChange}
timeRange={queryTimeRange}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</ResultsContentPage>
</>
)}
</>
Expand All @@ -183,6 +219,10 @@ const stringToNumericTimeRange = (timeRange: StringTimeRange): TimeRange => ({
).valueOf(),
});

const ExpandingPage = euiStyled(EuiPage)`
flex: 1 0 0%;
// This is needed due to the flex-basis: 100% !important; rule that
// kicks in on small screens via media queries breaking when using direction="column"
export const ResultsContentPage = euiStyled(EuiPage)`
.euiFlexGroup--responsive > .euiFlexItem {
flex-basis: auto !important;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* 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, AnnotationId } from '@elastic/charts';
import {
Axis,
BarSeries,
Chart,
getAxisId,
getSpecId,
niceTimeFormatter,
Settings,
TooltipValue,
LIGHT_THEME,
DARK_THEME,
getAnnotationId,
RectAnnotation,
} from '@elastic/charts';
import numeral from '@elastic/numeral';
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';
import { MLSeverityScoreCategories } from '../helpers/data_formatters';

export const AnomaliesChart: React.FunctionComponent<{
chartId: string;
setTimeRange: (timeRange: TimeRange) => void;
timeRange: TimeRange;
series: Array<{ time: number; value: number }>;
annotations: Record<MLSeverityScoreCategories, RectAnnotationDatum[]>;
renderAnnotationTooltip?: (details?: string) => JSX.Element;
}> = ({ chartId, series, annotations, setTimeRange, timeRange, renderAnnotationTooltip }) => {
const [dateFormat] = useKibanaUiSetting('dateFormat', 'Y-MM-DD HH:mm:ss.SSS');
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),
}),
[dateFormat]
);

const handleBrushEnd = useCallback(
(startTime: number, endTime: number) => {
setTimeRange({
endTime,
startTime,
});
},
[setTimeRange]
);

return (
<div style={{ height: 160, width: '100%' }}>
<Chart className="log-entry-rate-chart">
<Axis
id={getAxisId('timestamp')}
position="bottom"
showOverlappingTicks
tickFormat={chartDateFormatter}
/>
<Axis
id={getAxisId('values')}
position="left"
tickFormat={value => numeral(value.toPrecision(3)).format('0[.][00]a')} // https://github.com/adamwdraper/Numeral-js/issues/194
/>
<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}
barSeriesStyle={barSeriesStyle}
/>
{renderAnnotations(annotations, chartId, renderAnnotationTooltip)}
<Settings
onBrushEnd={handleBrushEnd}
tooltip={tooltipProps}
baseTheme={isDarkMode ? DARK_THEME : LIGHT_THEME}
/>
</Chart>
</div>
);
};

interface SeverityConfig {
annotationId: AnnotationId;
style: {
fill: string;
opacity: number;
};
}

const severityConfigs: Record<string, SeverityConfig> = {
warning: {
annotationId: getAnnotationId(`anomalies-warning`),
style: { fill: 'rgb(125, 180, 226)', opacity: 0.7 },
},
minor: {
annotationId: getAnnotationId(`anomalies-minor`),
style: { fill: 'rgb(255, 221, 0)', opacity: 0.7 },
},
major: {
annotationId: getAnnotationId(`anomalies-major`),
style: { fill: 'rgb(229, 113, 0)', opacity: 0.7 },
},
critical: {
annotationId: getAnnotationId(`anomalies-critical`),
style: { fill: 'rgb(228, 72, 72)', opacity: 0.7 },
},
};

const renderAnnotations = (
annotations: Record<MLSeverityScoreCategories, RectAnnotationDatum[]>,
chartId: string,
renderAnnotationTooltip?: (details?: string) => JSX.Element
) => {
return Object.entries(annotations).map((entry, index) => {
return (
<RectAnnotation
key={`${chartId}:${entry[0]}`}
dataValues={entry[1]}
annotationId={severityConfigs[entry[0]].annotationId}
style={severityConfigs[entry[0]].style}
renderTooltip={renderAnnotationTooltip}
/>
);
});
};

const barSeriesStyle = { rect: { fill: '#D3DAE6', opacity: 0.6 } }; // TODO: Acquire this from "theme" as euiColorLightShade
Loading

0 comments on commit 1fa1e54

Please sign in to comment.