Skip to content

Commit 6d54e58

Browse files
authored
[Logs UI] Adapt log entry rate data visualisations (#47558) (#48278)
Backports the following commits to 7.x: - [Logs UI] Adapt log entry rate data visualisations (#47558)
1 parent fd9f4a4 commit 6d54e58

File tree

15 files changed

+1076
-175
lines changed

15 files changed

+1076
-175
lines changed

x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,25 @@ export const logEntryRateAnomaly = rt.type({
3737
typicalLogEntryRate: rt.number,
3838
});
3939

40-
export const logEntryRateDataSetRT = rt.type({
40+
export const logEntryRatePartitionRT = rt.type({
4141
analysisBucketCount: rt.number,
4242
anomalies: rt.array(logEntryRateAnomaly),
4343
averageActualLogEntryRate: rt.number,
44-
dataSetId: rt.string,
44+
maximumAnomalyScore: rt.number,
45+
numberOfLogEntries: rt.number,
46+
partitionId: rt.string,
4547
});
4648

4749
export const logEntryRateHistogramBucket = rt.type({
48-
dataSets: rt.array(logEntryRateDataSetRT),
50+
partitions: rt.array(logEntryRatePartitionRT),
4951
startTime: rt.number,
5052
});
5153

5254
export const getLogEntryRateSuccessReponsePayloadRT = rt.type({
5355
data: rt.type({
5456
bucketDuration: rt.number,
5557
histogramBuckets: rt.array(logEntryRateHistogramBucket),
58+
totalNumberOfLogEntries: rt.number,
5659
}),
5760
});
5861

x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ import {
99
EuiFlexGroup,
1010
EuiFlexItem,
1111
EuiPage,
12-
EuiPageBody,
13-
EuiPageContent,
14-
EuiPageContentBody,
1512
EuiPanel,
1613
EuiSuperDatePicker,
14+
EuiBadge,
15+
EuiText,
1716
} from '@elastic/eui';
1817
import { i18n } from '@kbn/i18n';
18+
import numeral from '@elastic/numeral';
19+
import { FormattedMessage } from '@kbn/i18n/react';
1920
import moment from 'moment';
2021
import React, { useCallback, useMemo, useState } from 'react';
21-
22-
import euiStyled from '../../../../../../common/eui_styled_components';
2322
import { TimeRange } from '../../../../common/http_api/shared/time_range';
2423
import { bucketSpan } from '../../../../common/log_analysis';
24+
import euiStyled from '../../../../../../common/eui_styled_components';
2525
import { LoadingPage } from '../../../components/loading_page';
2626
import {
2727
StringTimeRange,
@@ -31,6 +31,8 @@ import {
3131
import { useTrackPageview } from '../../../hooks/use_track_metric';
3232
import { FirstUseCallout } from './first_use';
3333
import { LogRateResults } from './sections/log_rate';
34+
import { AnomaliesResults } from './sections/anomalies';
35+
import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting';
3436

3537
export const AnalysisResultsContent = ({
3638
sourceId,
@@ -42,6 +44,8 @@ export const AnalysisResultsContent = ({
4244
useTrackPageview({ app: 'infra_logs', path: 'analysis_results' });
4345
useTrackPageview({ app: 'infra_logs', path: 'analysis_results', delay: 15000 });
4446

47+
const [dateFormat] = useKibanaUiSetting('dateFormat', 'MMMM D, YYYY h:mm A');
48+
4549
const {
4650
timeRange: selectedTimeRange,
4751
setTimeRange: setSelectedTimeRange,
@@ -56,13 +60,13 @@ export const AnalysisResultsContent = ({
5660
const bucketDuration = useMemo(() => {
5761
// This function takes the current time range in ms,
5862
// works out the bucket interval we'd need to always
59-
// display 200 data points, and then takes that new
63+
// display 100 data points, and then takes that new
6064
// value and works out the nearest multiple of
6165
// 900000 (15 minutes) to it, so that we don't end up with
6266
// jaggy bucket boundaries between the ML buckets and our
6367
// aggregation buckets.
6468
const msRange = moment(queryTimeRange.endTime).diff(moment(queryTimeRange.startTime));
65-
const bucketIntervalInMs = msRange / 200;
69+
const bucketIntervalInMs = msRange / 100;
6670
const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan);
6771
const roundedResult = parseInt(Number(result).toFixed(0), 10);
6872
return roundedResult < bucketSpan ? bucketSpan : roundedResult;
@@ -130,39 +134,71 @@ export const AnalysisResultsContent = ({
130134
/>
131135
) : (
132136
<>
133-
<EuiPage>
134-
<EuiPanel paddingSize="l">
135-
<EuiFlexGroup justifyContent="spaceBetween">
136-
<EuiFlexItem></EuiFlexItem>
137-
<EuiFlexItem grow={false}>
138-
<EuiSuperDatePicker
139-
start={selectedTimeRange.startTime}
140-
end={selectedTimeRange.endTime}
141-
onTimeChange={handleSelectedTimeRangeChange}
142-
isPaused={autoRefresh.isPaused}
143-
refreshInterval={autoRefresh.interval}
144-
onRefreshChange={handleAutoRefreshChange}
145-
onRefresh={handleQueryTimeRangeChange}
146-
/>
147-
</EuiFlexItem>
148-
</EuiFlexGroup>
149-
</EuiPanel>
150-
</EuiPage>
151-
<ExpandingPage>
152-
<EuiPageBody>
153-
<EuiPageContent>
154-
<EuiPageContentBody>
137+
<ResultsContentPage>
138+
<EuiFlexGroup direction="column">
139+
<EuiFlexItem grow={false}>
140+
<EuiPanel paddingSize="l">
141+
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
142+
<EuiFlexItem grow={false}>
143+
{!isLoading && logEntryRate ? (
144+
<EuiText size="s">
145+
<FormattedMessage
146+
id="xpack.infra.logs.analysis.logRateResultsToolbarText"
147+
defaultMessage="Analyzed {numberOfLogs} log entries from {startTime} to {endTime}"
148+
values={{
149+
numberOfLogs: (
150+
<EuiBadge color="primary">
151+
<EuiText size="s" color="ghost">
152+
{numeral(logEntryRate.totalNumberOfLogEntries).format('0.00a')}
153+
</EuiText>
154+
</EuiBadge>
155+
),
156+
startTime: (
157+
<b>{moment(queryTimeRange.startTime).format(dateFormat)}</b>
158+
),
159+
endTime: <b>{moment(queryTimeRange.endTime).format(dateFormat)}</b>,
160+
}}
161+
/>
162+
</EuiText>
163+
) : null}
164+
</EuiFlexItem>
165+
<EuiFlexItem grow={false}>
166+
<EuiSuperDatePicker
167+
start={selectedTimeRange.startTime}
168+
end={selectedTimeRange.endTime}
169+
onTimeChange={handleSelectedTimeRangeChange}
170+
isPaused={autoRefresh.isPaused}
171+
refreshInterval={autoRefresh.interval}
172+
onRefreshChange={handleAutoRefreshChange}
173+
onRefresh={handleQueryTimeRangeChange}
174+
/>
175+
</EuiFlexItem>
176+
</EuiFlexGroup>
177+
</EuiPanel>
178+
</EuiFlexItem>
179+
<EuiFlexItem grow={false}>
180+
<EuiPanel paddingSize="l">
155181
{isFirstUse && !hasResults ? <FirstUseCallout /> : null}
156182
<LogRateResults
157183
isLoading={isLoading}
158184
results={logEntryRate}
159185
setTimeRange={handleChartTimeRangeChange}
160186
timeRange={queryTimeRange}
161187
/>
162-
</EuiPageContentBody>
163-
</EuiPageContent>
164-
</EuiPageBody>
165-
</ExpandingPage>
188+
</EuiPanel>
189+
</EuiFlexItem>
190+
<EuiFlexItem grow={false}>
191+
<EuiPanel paddingSize="l">
192+
<AnomaliesResults
193+
isLoading={isLoading}
194+
results={logEntryRate}
195+
setTimeRange={handleChartTimeRangeChange}
196+
timeRange={queryTimeRange}
197+
/>
198+
</EuiPanel>
199+
</EuiFlexItem>
200+
</EuiFlexGroup>
201+
</ResultsContentPage>
166202
</>
167203
)}
168204
</>
@@ -183,6 +219,10 @@ const stringToNumericTimeRange = (timeRange: StringTimeRange): TimeRange => ({
183219
).valueOf(),
184220
});
185221

186-
const ExpandingPage = euiStyled(EuiPage)`
187-
flex: 1 0 0%;
222+
// This is needed due to the flex-basis: 100% !important; rule that
223+
// kicks in on small screens via media queries breaking when using direction="column"
224+
export const ResultsContentPage = euiStyled(EuiPage)`
225+
.euiFlexGroup--responsive > .euiFlexItem {
226+
flex-basis: auto !important;
227+
}
188228
`;
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { RectAnnotationDatum, AnnotationId } from '@elastic/charts';
8+
import {
9+
Axis,
10+
BarSeries,
11+
Chart,
12+
getAxisId,
13+
getSpecId,
14+
niceTimeFormatter,
15+
Settings,
16+
TooltipValue,
17+
LIGHT_THEME,
18+
DARK_THEME,
19+
getAnnotationId,
20+
RectAnnotation,
21+
} from '@elastic/charts';
22+
import numeral from '@elastic/numeral';
23+
import { i18n } from '@kbn/i18n';
24+
import moment from 'moment';
25+
import React, { useCallback, useMemo } from 'react';
26+
27+
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
28+
import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting';
29+
import { MLSeverityScoreCategories } from '../helpers/data_formatters';
30+
31+
export const AnomaliesChart: React.FunctionComponent<{
32+
chartId: string;
33+
setTimeRange: (timeRange: TimeRange) => void;
34+
timeRange: TimeRange;
35+
series: Array<{ time: number; value: number }>;
36+
annotations: Record<MLSeverityScoreCategories, RectAnnotationDatum[]>;
37+
renderAnnotationTooltip?: (details?: string) => JSX.Element;
38+
}> = ({ chartId, series, annotations, setTimeRange, timeRange, renderAnnotationTooltip }) => {
39+
const [dateFormat] = useKibanaUiSetting('dateFormat', 'Y-MM-DD HH:mm:ss.SSS');
40+
const [isDarkMode] = useKibanaUiSetting('theme:darkMode');
41+
42+
const chartDateFormatter = useMemo(
43+
() => niceTimeFormatter([timeRange.startTime, timeRange.endTime]),
44+
[timeRange]
45+
);
46+
47+
const logEntryRateSpecId = getSpecId('averageValues');
48+
49+
const tooltipProps = useMemo(
50+
() => ({
51+
headerFormatter: (tooltipData: TooltipValue) => moment(tooltipData.value).format(dateFormat),
52+
}),
53+
[dateFormat]
54+
);
55+
56+
const handleBrushEnd = useCallback(
57+
(startTime: number, endTime: number) => {
58+
setTimeRange({
59+
endTime,
60+
startTime,
61+
});
62+
},
63+
[setTimeRange]
64+
);
65+
66+
return (
67+
<div style={{ height: 160, width: '100%' }}>
68+
<Chart className="log-entry-rate-chart">
69+
<Axis
70+
id={getAxisId('timestamp')}
71+
position="bottom"
72+
showOverlappingTicks
73+
tickFormat={chartDateFormatter}
74+
/>
75+
<Axis
76+
id={getAxisId('values')}
77+
position="left"
78+
tickFormat={value => numeral(value.toPrecision(3)).format('0[.][00]a')} // https://github.com/adamwdraper/Numeral-js/issues/194
79+
/>
80+
<BarSeries
81+
id={logEntryRateSpecId}
82+
name={i18n.translate('xpack.infra.logs.analysis.anomaliesSectionLineSeriesName', {
83+
defaultMessage: 'Log entries per 15 minutes (avg)',
84+
})}
85+
xScaleType="time"
86+
yScaleType="linear"
87+
xAccessor={'time'}
88+
yAccessors={['value']}
89+
data={series}
90+
barSeriesStyle={barSeriesStyle}
91+
/>
92+
{renderAnnotations(annotations, chartId, renderAnnotationTooltip)}
93+
<Settings
94+
onBrushEnd={handleBrushEnd}
95+
tooltip={tooltipProps}
96+
baseTheme={isDarkMode ? DARK_THEME : LIGHT_THEME}
97+
/>
98+
</Chart>
99+
</div>
100+
);
101+
};
102+
103+
interface SeverityConfig {
104+
annotationId: AnnotationId;
105+
style: {
106+
fill: string;
107+
opacity: number;
108+
};
109+
}
110+
111+
const severityConfigs: Record<string, SeverityConfig> = {
112+
warning: {
113+
annotationId: getAnnotationId(`anomalies-warning`),
114+
style: { fill: 'rgb(125, 180, 226)', opacity: 0.7 },
115+
},
116+
minor: {
117+
annotationId: getAnnotationId(`anomalies-minor`),
118+
style: { fill: 'rgb(255, 221, 0)', opacity: 0.7 },
119+
},
120+
major: {
121+
annotationId: getAnnotationId(`anomalies-major`),
122+
style: { fill: 'rgb(229, 113, 0)', opacity: 0.7 },
123+
},
124+
critical: {
125+
annotationId: getAnnotationId(`anomalies-critical`),
126+
style: { fill: 'rgb(228, 72, 72)', opacity: 0.7 },
127+
},
128+
};
129+
130+
const renderAnnotations = (
131+
annotations: Record<MLSeverityScoreCategories, RectAnnotationDatum[]>,
132+
chartId: string,
133+
renderAnnotationTooltip?: (details?: string) => JSX.Element
134+
) => {
135+
return Object.entries(annotations).map((entry, index) => {
136+
return (
137+
<RectAnnotation
138+
key={`${chartId}:${entry[0]}`}
139+
dataValues={entry[1]}
140+
annotationId={severityConfigs[entry[0]].annotationId}
141+
style={severityConfigs[entry[0]].style}
142+
renderTooltip={renderAnnotationTooltip}
143+
/>
144+
);
145+
});
146+
};
147+
148+
const barSeriesStyle = { rect: { fill: '#D3DAE6', opacity: 0.6 } }; // TODO: Acquire this from "theme" as euiColorLightShade

0 commit comments

Comments
 (0)