[Alerting] Fix the charts on Log Threshold Rule Alert Detail page (#160321)

## Summary

This PR fixes #160320 by changing the chart from the `CriterionPreview`
chart, borrowed from the Log Threshold Rule, to an embedded Lens
visualization that represents the correct document count in one chart. I
also took the liberty of changing the ratio chart to use the same
technique for consistency sake.

## Count with multiple conditions

### Before

<img width="736" alt="image"
src="6c6a27ea-f8e4-491f-8a12-261d0ed13dcb">

### After

<img width="736" alt="image"
src="9b18ebe9-e911-4e40-8911-bee55cd7d245">

## Count with multiple conditions and a group by

### Before

<img width="736" alt="image"
src="7b9462da-55b2-4f54-ba09-3c55b372ae2c">


### After

<img width="736" alt="image"
src="b268caed-242f-430a-ade0-14bf491ec899">

## Ratio with multiple conditions

### Before

<img width="736" alt="image"
src="55b8dfa2-7789-433b-bffd-e412bdb08b3f">

### After

<img width="736" alt="image"
src="a029bf8a-3ba1-4e16-87bd-097ebc526a4e">


## Ratio with multiple conditions and a  group by

### Before

<img width="736" alt="image"
src="61ddf1e9-c5ad-4546-a539-15a51ee563c0">

### After

<img width="736" alt="image"
src="15b0aaa3-4ef9-47f6-baba-24869feae77e">
This commit is contained in:
Chris Cowan 2023-06-27 10:18:31 -06:00 committed by GitHub
parent 58b3c3298f
commit a8322d2711
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 688 additions and 369 deletions

View file

@ -8,9 +8,8 @@
import moment from 'moment';
export interface TimeRange {
from?: string;
to?: string;
interval?: string;
from: string;
to: string;
}
export const getPaddedAlertTimeRange = (alertStart: string, alertEnd?: string): TimeRange => {

View file

@ -1,238 +0,0 @@
/*
* 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 {
Chart,
BarSeries,
ScaleType,
LineAnnotation,
RectAnnotation,
Axis,
Settings,
Position,
AnnotationDomainType,
Tooltip,
} from '@elastic/charts';
import React, { ReactElement, useEffect, useMemo } from 'react';
import { useIsDarkMode } from '../../../../../hooks/use_is_dark_mode';
import { ExecutionTimeRange } from '../../../../../types';
import { decodeOrThrow } from '../../../../../../common/runtime_types';
import {
GetLogAlertsChartPreviewDataAlertParamsSubset,
getLogAlertsChartPreviewDataAlertParamsSubsetRT,
} from '../../../../../../common/http_api';
import {
Comparator,
PartialRuleParams,
Threshold,
} from '../../../../../../common/alerting/logs/log_threshold';
import { PersistedLogViewReference } from '../../../../../../common/log_views';
import { useKibanaTimeZoneSetting } from '../../../../../hooks/use_kibana_time_zone_setting';
import { getChartTheme } from '../../../../../utils/get_chart_theme';
import {
yAxisFormatter,
tooltipProps,
getDomain,
useDateFormatter,
LoadingState,
ErrorState,
NoDataState,
ChartContainer,
} from '../../../../common/criterion_preview_chart/criterion_preview_chart';
import { Color, colorTransformer } from '../../../../../../common/color_palette';
import { useChartPreviewData } from '../../expression_editor/hooks/use_chart_preview_data';
interface ChartProps {
buckets: number;
logViewReference: PersistedLogViewReference;
ruleParams: PartialRuleParams;
threshold?: Threshold;
showThreshold: boolean;
executionTimeRange?: ExecutionTimeRange;
filterSeriesByGroupName?: string;
annotations?: Array<ReactElement<typeof RectAnnotation | typeof LineAnnotation>>;
}
const LogsRatioChart: React.FC<ChartProps> = ({
buckets,
ruleParams,
logViewReference,
threshold,
showThreshold,
executionTimeRange,
filterSeriesByGroupName,
annotations,
}) => {
const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => {
const params = {
criteria: ruleParams.criteria,
count: {
comparator: ruleParams.count.comparator,
value: ruleParams.count.value,
},
timeSize: ruleParams.timeSize,
timeUnit: ruleParams.timeUnit,
groupBy: ruleParams.groupBy,
};
try {
return decodeOrThrow(getLogAlertsChartPreviewDataAlertParamsSubsetRT)(params);
} catch (error) {
return null;
}
}, [
ruleParams.criteria,
ruleParams.count.comparator,
ruleParams.count.value,
ruleParams.timeSize,
ruleParams.timeUnit,
ruleParams.groupBy,
]);
const {
getChartPreviewData,
isLoading,
hasError,
chartPreviewData: series,
} = useChartPreviewData({
logViewReference,
ruleParams: chartAlertParams,
buckets,
executionTimeRange,
filterSeriesByGroupName,
});
useEffect(() => {
getChartPreviewData();
}, [getChartPreviewData]);
const isDarkMode = useIsDarkMode();
const timezone = useKibanaTimeZoneSetting();
const { yMin, yMax, xMin, xMax } = getDomain(series, false);
const dateFormatter = useDateFormatter(xMin, xMax);
const hasData = series.length > 0;
const THRESHOLD_OPACITY = 0.3;
const chartDomain = {
max: showThreshold && threshold ? Math.max(yMax, threshold.value) * 1.1 : yMax * 1.1, // Add 10% headroom.
min: showThreshold && threshold ? Math.min(yMin, threshold.value) : yMin,
};
const isAbove =
showThreshold && threshold && threshold.comparator
? [Comparator.GT, Comparator.GT_OR_EQ].includes(threshold.comparator)
: false;
const isBelow =
showThreshold && threshold && threshold.comparator
? [Comparator.LT, Comparator.LT_OR_EQ].includes(threshold.comparator)
: false;
const barSeries = useMemo(() => {
return series.flatMap(({ points, id }) => points.map((point) => ({ ...point, groupBy: id })));
}, [series]);
if (isLoading) {
return <LoadingState />;
} else if (hasError) {
return <ErrorState />;
} else if (!hasData) {
return <NoDataState />;
}
return (
<ChartContainer>
<Chart>
<BarSeries
id="criterion-preview"
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="timestamp"
yAccessors={['value']}
splitSeriesAccessors={['groupBy']}
data={barSeries}
barSeriesStyle={{
rectBorder: {
stroke: colorTransformer(Color.color0),
strokeWidth: 1,
visible: true,
},
rect: {
opacity: 1,
},
}}
color={colorTransformer(Color.color0)}
timeZone={timezone}
/>
{showThreshold && threshold ? (
<LineAnnotation
id={`threshold-line`}
domainType={AnnotationDomainType.YDomain}
dataValues={[{ dataValue: threshold.value }]}
style={{
line: {
strokeWidth: 2,
stroke: colorTransformer(Color.color1),
opacity: 1,
},
}}
/>
) : null}
{showThreshold && threshold && isBelow ? (
<RectAnnotation
id="below-threshold"
style={{
fill: colorTransformer(Color.color1),
opacity: THRESHOLD_OPACITY,
}}
dataValues={[
{
coordinates: {
x0: xMin,
x1: xMax,
y0: chartDomain.min,
y1: threshold.value,
},
},
]}
/>
) : null}
{annotations}
{showThreshold && threshold && isAbove ? (
<RectAnnotation
id="above-threshold"
style={{
fill: colorTransformer(Color.color1),
opacity: THRESHOLD_OPACITY,
}}
dataValues={[
{
coordinates: {
x0: xMin,
x1: xMax,
y0: threshold.value,
y1: chartDomain.max,
},
},
]}
/>
) : null}
<Axis
id={'timestamp'}
position={Position.Bottom}
showOverlappingTicks={true}
tickFormat={dateFormatter}
/>
<Axis
id={'values'}
position={Position.Left}
tickFormat={yAxisFormatter}
domain={chartDomain}
/>
<Settings theme={getChartTheme(isDarkMode)} />
<Tooltip {...tooltipProps} />
</Chart>
</ChartContainer>
);
};
// eslint-disable-next-line import/no-default-export
export default LogsRatioChart;

View file

@ -0,0 +1,395 @@
/*
* 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 { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { EuiThemeComputed, transparentize } from '@elastic/eui';
export interface IndexPattern {
pattern: string;
timestampField: string;
}
export interface Threshold {
value: number;
fill: 'above' | 'below';
}
export interface Timerange {
from: number;
to?: number;
}
function createBaseLensDefinition<D extends {}>(
index: IndexPattern,
euiTheme: EuiThemeComputed,
threshold: Threshold,
alertRange: Timerange,
layerDef: D,
filter?: string
) {
return {
title: 'Threshold Chart',
visualizationType: 'lnsXY',
type: 'lens',
references: [],
state: {
visualization: {
legend: {
isVisible: false,
position: 'right',
},
valueLabels: 'hide',
fittingFunction: 'None',
axisTitlesVisibilitySettings: {
x: false,
yLeft: false,
yRight: false,
},
tickLabelsVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
labelsOrientation: {
x: 0,
yLeft: 0,
yRight: 0,
},
gridlinesVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
preferredSeriesType: 'bar_stacked',
layers: [
{
layerId: 'e6f553a0-9e36-4eea-8ecf-8261523c6f44',
accessors: ['607b2253-ed20-4f0a-bf62-07a1f846cca4'],
position: 'top',
seriesType: 'bar_stacked',
showGridlines: false,
layerType: 'data',
xAccessor: '8ed7d473-ff48-4c90-be2c-ae46f3a11030',
yConfig: [
{
forAccessor: '607b2253-ed20-4f0a-bf62-07a1f846cca4',
color: '#6092c0',
},
],
},
{
layerId: '62dfc313-3922-4870-b568-ff0818da38b3',
layerType: 'annotations',
annotations: [
{
type: 'manual',
id: 'ffe44253-a8c7-4755-821f-47be5bfac288',
label: 'Alert Line',
key: {
type: 'point_in_time',
timestamp: moment(alertRange.from).toISOString(),
},
lineWidth: 3,
color: euiTheme.colors.danger,
icon: 'alert',
},
{
type: 'manual',
label: 'Alert',
key: {
type: 'range',
timestamp: moment(alertRange.from).toISOString(),
endTimestamp: moment(alertRange.to).toISOString(),
},
id: '07d15b13-4b6c-4d82-b45d-9d58ced1c2a8',
color: transparentize(euiTheme.colors.danger, 0.2),
},
],
ignoreGlobalFilters: true,
persistanceType: 'byValue',
},
{
layerId: '90f87c46-9685-49af-b4ed-066eb65e2b39',
layerType: 'referenceLine',
accessors: ['7fb02af1-0823-4787-a316-3b05a4539d2c'],
yConfig: [
{
forAccessor: '7fb02af1-0823-4787-a316-3b05a4539d2c',
axisMode: 'left',
color: euiTheme.colors.danger,
lineWidth: 2,
fill: threshold.fill,
},
],
},
],
},
query: {
query: filter || '',
language: 'kuery',
},
filters: [],
datasourceStates: {
formBased: {
layers: {
'e6f553a0-9e36-4eea-8ecf-8261523c6f44': layerDef,
'90f87c46-9685-49af-b4ed-066eb65e2b39': {
linkToLayers: [],
columns: {
'7fb02af1-0823-4787-a316-3b05a4539d2c': {
label: 'Threshold',
dataType: 'number',
operationType: 'static_value',
isStaticValue: true,
isBucketed: false,
scale: 'ratio',
params: {
value: threshold.value,
},
references: [],
customLabel: true,
},
},
columnOrder: ['7fb02af1-0823-4787-a316-3b05a4539d2c'],
sampling: 1,
ignoreGlobalFilters: false,
incompleteColumns: {},
},
},
},
indexpattern: {
layers: {},
},
textBased: {
layers: {},
},
},
internalReferences: [
{
type: 'index-pattern',
id: 'd09436e6-20c0-4982-aaf6-b67ec371b27d',
name: 'indexpattern-datasource-layer-e6f553a0-9e36-4eea-8ecf-8261523c6f44',
},
{
type: 'index-pattern',
id: 'd09436e6-20c0-4982-aaf6-b67ec371b27d',
name: 'indexpattern-datasource-layer-90f87c46-9685-49af-b4ed-066eb65e2b39',
},
{
type: 'index-pattern',
id: 'd09436e6-20c0-4982-aaf6-b67ec371b27d',
name: 'xy-visualization-layer-62dfc313-3922-4870-b568-ff0818da38b3',
},
],
adHocDataViews: {
'd09436e6-20c0-4982-aaf6-b67ec371b27d': {
id: 'd09436e6-20c0-4982-aaf6-b67ec371b27d',
title: index.pattern,
timeFieldName: index.timestampField,
sourceFilters: [],
fieldFormats: {},
runtimeFieldMap: {},
fieldAttrs: {},
allowNoIndex: false,
name: 'adhoc',
},
},
},
};
}
export function createLensDefinitionForRatioChart(
index: IndexPattern,
euiTheme: EuiThemeComputed,
numeratorKql: string,
denominatorKql: string,
threshold: Threshold,
alertRange: Timerange,
interval: string,
filter?: string
) {
const layerDef = {
columns: {
'8ed7d473-ff48-4c90-be2c-ae46f3a11030': {
label: index.timestampField,
dataType: 'date',
operationType: 'date_histogram',
sourceField: index.timestampField,
isBucketed: true,
scale: 'interval',
params: {
interval,
includeEmptyRows: true,
dropPartials: false,
},
},
'607b2253-ed20-4f0a-bf62-07a1f846cca4X0': {
label: 'Part of ratio',
dataType: 'number',
operationType: 'count',
isBucketed: false,
scale: 'ratio',
sourceField: '___records___',
filter: {
query: numeratorKql,
language: 'kuery',
},
params: {
emptyAsNull: false,
},
customLabel: true,
},
'607b2253-ed20-4f0a-bf62-07a1f846cca4X1': {
label: 'Part of ratio',
dataType: 'number',
operationType: 'count',
isBucketed: false,
scale: 'ratio',
sourceField: '___records___',
filter: {
query: denominatorKql,
language: 'kuery',
},
params: {
emptyAsNull: false,
},
customLabel: true,
},
'607b2253-ed20-4f0a-bf62-07a1f846cca4X2': {
label: 'Part of ratio',
dataType: 'number',
operationType: 'math',
isBucketed: false,
scale: 'ratio',
params: {
tinymathAst: {
type: 'function',
name: 'divide',
args: [
'607b2253-ed20-4f0a-bf62-07a1f846cca4X0',
'607b2253-ed20-4f0a-bf62-07a1f846cca4X1',
],
location: {
min: 0,
max: 94,
},
text: `count(kql=\'${numeratorKql}\') / count(kql=\'${denominatorKql}\')`,
},
},
references: [
'607b2253-ed20-4f0a-bf62-07a1f846cca4X0',
'607b2253-ed20-4f0a-bf62-07a1f846cca4X1',
],
customLabel: true,
},
'607b2253-ed20-4f0a-bf62-07a1f846cca4': {
label: 'ratio',
dataType: 'number',
operationType: 'formula',
isBucketed: false,
scale: 'ratio',
params: {
formula: `count(kql=\'${numeratorKql}\') / count(kql=\'${denominatorKql}\')`,
isFormulaBroken: false,
},
references: ['607b2253-ed20-4f0a-bf62-07a1f846cca4X2'],
customLabel: true,
},
},
columnOrder: [
'8ed7d473-ff48-4c90-be2c-ae46f3a11030',
'607b2253-ed20-4f0a-bf62-07a1f846cca4',
'607b2253-ed20-4f0a-bf62-07a1f846cca4X0',
'607b2253-ed20-4f0a-bf62-07a1f846cca4X1',
'607b2253-ed20-4f0a-bf62-07a1f846cca4X2',
],
incompleteColumns: {},
sampling: 1,
};
return createBaseLensDefinition(
index,
euiTheme,
threshold,
alertRange,
layerDef,
filter
) as unknown as TypedLensByValueInput['attributes'];
}
export function createLensDefinitionForCountChart(
index: IndexPattern,
euiTheme: EuiThemeComputed,
kql: string,
threshold: Threshold,
alertRange: Timerange,
interval: string,
filter?: string
) {
const layerDef = {
columns: {
'8ed7d473-ff48-4c90-be2c-ae46f3a11030': {
label: index.timestampField,
dataType: 'date',
operationType: 'date_histogram',
sourceField: index.timestampField,
isBucketed: true,
scale: 'interval',
params: {
interval,
includeEmptyRows: true,
dropPartials: false,
},
},
'607b2253-ed20-4f0a-bf62-07a1f846cca4X0': {
label: `Part of count(kql=\'${kql}\')`,
dataType: 'number',
operationType: 'count',
isBucketed: false,
scale: 'ratio',
sourceField: '___records___',
filter: {
query: kql,
language: 'kuery',
},
params: {
emptyAsNull: false,
},
customLabel: true,
},
'607b2253-ed20-4f0a-bf62-07a1f846cca4': {
label: 'document count',
dataType: 'number',
operationType: 'formula',
isBucketed: false,
scale: 'ratio',
params: {
formula: `count(kql=\'${kql}\')`,
isFormulaBroken: false,
},
references: ['607b2253-ed20-4f0a-bf62-07a1f846cca4X0'],
customLabel: true,
},
},
columnOrder: [
'8ed7d473-ff48-4c90-be2c-ae46f3a11030',
'607b2253-ed20-4f0a-bf62-07a1f846cca4',
'607b2253-ed20-4f0a-bf62-07a1f846cca4X0',
],
incompleteColumns: {},
sampling: 1,
};
return createBaseLensDefinition(
index,
euiTheme,
threshold,
alertRange,
layerDef,
filter
) as unknown as TypedLensByValueInput['attributes'];
}

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { LogThresholdCountChart } from './log_threshold_count_chart';
export { LogThresholdRatioChart } from './log_threshold_ratio_chart';

View file

@ -0,0 +1,64 @@
/*
* 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 { ViewMode } from '@kbn/embeddable-plugin/public';
import { useEuiTheme } from '@elastic/eui';
import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
import {
createLensDefinitionForCountChart,
IndexPattern,
Threshold,
Timerange,
} from './create_lens_definition';
interface LogThresholdCountChartProps {
index: IndexPattern;
threshold: Threshold;
timeRange: { from: string; to: string };
alertRange: Timerange;
kql: string;
height: number;
interval?: string;
filter?: string;
}
export function LogThresholdCountChart({
kql,
index,
threshold,
timeRange,
alertRange,
height,
interval = 'auto',
filter = '',
}: LogThresholdCountChartProps) {
const {
lens: { EmbeddableComponent },
} = useKibanaContextForPlugin().services;
const { euiTheme } = useEuiTheme();
const lensDef = createLensDefinitionForCountChart(
index,
euiTheme,
kql,
threshold,
alertRange,
interval,
filter
);
return (
<div>
<EmbeddableComponent
id="logThresholdCountChart"
style={{ height }}
timeRange={timeRange}
attributes={lensDef}
viewMode={ViewMode.VIEW}
noPadding
/>
</div>
);
}

View file

@ -0,0 +1,67 @@
/*
* 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 { ViewMode } from '@kbn/embeddable-plugin/public';
import { useEuiTheme } from '@elastic/eui';
import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
import {
createLensDefinitionForRatioChart,
IndexPattern,
Threshold,
Timerange,
} from './create_lens_definition';
interface LogThresholdRatioChartProps {
index: IndexPattern;
threshold: Threshold;
timeRange: { from: string; to: string };
alertRange: Timerange;
numeratorKql: string;
denominatorKql: string;
height: number;
interval?: string;
filter?: string;
}
export function LogThresholdRatioChart({
numeratorKql,
denominatorKql,
index,
threshold,
timeRange,
alertRange,
height,
interval = 'auto',
filter = '',
}: LogThresholdRatioChartProps) {
const {
lens: { EmbeddableComponent },
} = useKibanaContextForPlugin().services;
const { euiTheme } = useEuiTheme();
const lensDef = createLensDefinitionForRatioChart(
index,
euiTheme,
numeratorKql,
denominatorKql,
threshold,
alertRange,
interval,
filter
);
return (
<div>
<EmbeddableComponent
id="logThresholdRatioChart"
style={{ height }}
timeRange={timeRange}
attributes={lensDef}
viewMode={ViewMode.VIEW}
noPadding
/>
</div>
);
}

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { LIGHT_THEME } from '@elastic/charts';
import { EuiPanel } from '@elastic/eui';
@ -18,28 +18,22 @@ import moment from 'moment';
import { useTheme } from '@emotion/react';
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
AlertAnnotation,
getPaddedAlertTimeRange,
AlertActiveTimeRangeAnnotation,
} from '@kbn/observability-alert-details';
import { useEuiTheme } from '@elastic/eui';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import { get } from 'lodash';
import { getPaddedAlertTimeRange } from '@kbn/observability-alert-details';
import { get, identity } from 'lodash';
import { CoPilotContextProvider } from '@kbn/observability-plugin/public';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { getChartGroupNames } from '../../../../../common/utils/get_chart_group_names';
import {
Comparator,
ComparatorToi18nMap,
ComparatorToi18nSymbolsMap,
isRatioRule,
type PartialCriterion,
} from '../../../../../common/alerting/logs/log_threshold';
import { CriterionPreview } from '../expression_editor/criterion_preview_chart';
import { AlertDetailsAppSectionProps } from './types';
import { Threshold } from '../../../common/components/threshold';
import LogsRatioChart from './components/logs_ratio_chart';
import { ExplainLogRateSpikes } from './components/explain_log_rate_spike';
import { LogThresholdCountChart, LogThresholdRatioChart } from './components/threhsold_chart';
import { useLogView } from '../../../../hooks/use_log_view';
import { useLicense } from '../../../../hooks/use_license';
const LogsHistoryChart = React.lazy(() => import('./components/logs_history_chart'));
@ -50,12 +44,30 @@ const AlertDetailsAppSection = ({
alert,
setAlertSummaryFields,
}: AlertDetailsAppSectionProps) => {
const [selectedSeries, setSelectedSeries] = useState<string>('');
const { uiSettings, observability } = useKibanaContextForPlugin().services;
const { euiTheme } = useEuiTheme();
const { observability, logViews } = useKibanaContextForPlugin().services;
const theme = useTheme();
const timeRange = getPaddedAlertTimeRange(alert.fields[ALERT_START]!, alert.fields[ALERT_END]);
const alertEnd = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]).valueOf() : undefined;
const interval = `${rule.params.timeSize}${rule.params.timeUnit}`;
const thresholdFill = convertComparatorToFill(rule.params.count.comparator);
const filter = rule.params.groupBy
? rule.params.groupBy
.map((field) => {
const value = get(
alert.fields[ALERT_CONTEXT],
['groupByKeys', ...field.split('.')],
null
);
return value ? `${field} : "${value}"` : null;
})
.filter(identity)
.join(' AND ')
: '';
const { derivedDataView } = useLogView({
initialLogViewReference: rule.params.logView,
logViews: logViews.client,
});
const { hasAtLeast } = useLicense();
const hasLicenseForExplainLogSpike = hasAtLeast('platinum');
@ -80,7 +92,6 @@ const AlertDetailsAppSection = ({
{}
) || {};
setSelectedSeries(getChartGroupNames(Object.values(alertFieldsFromGroupBy)));
const alertSummaryFields = Object.entries(alertFieldsFromGroupBy).map(([label, value]) => ({
label,
value,
@ -90,6 +101,13 @@ const AlertDetailsAppSection = ({
const getLogRatioChart = () => {
if (isRatioRule(rule.params.criteria)) {
const numeratorKql = rule.params.criteria[0]
.map((criteria) => convertCriteriaToKQL(criteria))
.join(' AND ');
const denominatorKql = rule.params.criteria[1]
.map((criteria) => convertCriteriaToKQL(criteria))
.join(' AND ');
return (
<EuiPanel hasBorder={true} data-test-subj="logsRatioChartAlertDetails">
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
@ -106,7 +124,6 @@ const AlertDetailsAppSection = ({
<EuiFlexGroup>
<EuiFlexItem style={{ maxHeight: 120 }} grow={1}>
<EuiSpacer size="s" />
<Threshold
title={`Threshold breached`}
chartProps={{ theme, baseTheme: LIGHT_THEME }}
@ -119,38 +136,22 @@ const AlertDetailsAppSection = ({
</EuiFlexItem>
<EuiFlexItem grow={5}>
<EuiSpacer size="s" />
<LogsRatioChart
buckets={1}
logViewReference={{
type: 'log-view-reference',
logViewId: rule.params.logView.logViewId,
}}
ruleParams={rule.params}
filterSeriesByGroupName={selectedSeries}
showThreshold={true}
threshold={rule.params.count}
executionTimeRange={{
gte: Number(moment(timeRange.from).format('x')),
lte: Number(moment(timeRange.to).format('x')),
}}
annotations={[
<AlertAnnotation
key={`${alert.start}-start-alert-annotation`}
id={`${alert.start}-start-alert-annotation`}
alertStart={alert.start}
color={euiTheme.colors.danger}
dateFormat={uiSettings.get(UI_SETTINGS.DATE_FORMAT)}
/>,
<AlertActiveTimeRangeAnnotation
key={`${alert.start}-active-alert-annotation`}
id={`${alert.start}-active-alert-annotation`}
alertStart={alert.start}
alertEnd={alertEnd}
color={euiTheme.colors.danger}
/>,
]}
/>
{derivedDataView && (
<LogThresholdRatioChart
filter={filter}
numeratorKql={numeratorKql}
denominatorKql={denominatorKql}
threshold={{ value: rule.params.count.value, fill: thresholdFill }}
timeRange={timeRange}
alertRange={{ from: alert.start, to: alertEnd }}
index={{
pattern: derivedDataView.getIndexPattern(),
timestampField: derivedDataView.timeFieldName || '@timestamp',
}}
height={150}
interval={interval}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
@ -160,84 +161,66 @@ const AlertDetailsAppSection = ({
const getLogCountChart = () => {
if (!!rule.params.criteria && !isRatioRule(rule.params.criteria)) {
return rule.params.criteria.map((criteria, idx) => {
const chartCriterion = criteria as PartialCriterion;
return (
<EuiPanel
key={`${chartCriterion.field}${idx}`}
hasBorder={true}
data-test-subj={`logsCountChartAlertDetails-${chartCriterion.field}${idx}`}
>
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
{chartCriterion.comparator && (
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.infra.logs.alertDetails.chart.chartTitle', {
defaultMessage: 'Logs for {field} {comparator} {value}',
values: {
field: chartCriterion.field,
comparator: ComparatorToi18nMap[chartCriterion.comparator],
value: chartCriterion.value,
},
})}
</h2>
</EuiTitle>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem style={{ maxHeight: 120 }} grow={1}>
<EuiSpacer size="s" />
{chartCriterion.comparator && (
<Threshold
title={`Threshold breached`}
chartProps={{ theme, baseTheme: LIGHT_THEME }}
comparator={ComparatorToi18nSymbolsMap[rule.params.count.comparator]}
id={`${chartCriterion.field}-${chartCriterion.value}`}
threshold={rule.params.count.value}
value={Number(alert.fields[ALERT_EVALUATION_VALUE])}
valueFormatter={formatThreshold}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={5}>
<CriterionPreview
ruleParams={rule.params}
logViewReference={{
type: 'log-view-reference',
logViewId: rule.params.logView.logViewId,
const kql = rule.params.criteria
.map((criteria) => convertCriteriaToKQL(criteria))
.join(' AND ');
const criteriaAsText = rule.params.criteria
.map((criteria) => {
if (!criteria.field || !criteria.comparator || !criteria.value) {
return '';
}
return `${criteria.field} ${ComparatorToi18nMap[criteria.comparator]} ${criteria.value}`;
})
.filter((text) => text)
.join(' AND ');
return (
<EuiPanel hasBorder={true} data-test-subj={`logsCountChartAlertDetails`}>
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.infra.logs.alertDetails.chart.chartTitle', {
defaultMessage: 'Logs for {criteria}',
values: { criteria: criteriaAsText },
})}
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem style={{ maxHeight: 120 }} grow={1}>
<EuiSpacer size="s" />
<Threshold
title={`Threshold breached`}
chartProps={{ theme, baseTheme: LIGHT_THEME }}
comparator={ComparatorToi18nSymbolsMap[rule.params.count.comparator]}
id="logCountThreshold"
threshold={rule.params.count.value}
value={Number(alert.fields[ALERT_EVALUATION_VALUE])}
valueFormatter={formatThreshold}
/>
</EuiFlexItem>
<EuiFlexItem grow={5}>
{derivedDataView && (
<LogThresholdCountChart
filter={filter}
kql={kql}
threshold={{ value: rule.params.count.value, fill: thresholdFill }}
timeRange={timeRange}
alertRange={{ from: alert.start, to: alertEnd }}
index={{
pattern: derivedDataView.getIndexPattern(),
timestampField: derivedDataView.timeFieldName || '@timestamp',
}}
chartCriterion={chartCriterion}
showThreshold={true}
executionTimeRange={{
gte: Number(moment(timeRange.from).format('x')),
lte: Number(moment(timeRange.to).format('x')),
}}
annotations={[
<AlertAnnotation
key={`${alert.start}${chartCriterion.field}${idx}-start-alert-annotation`}
id={`${alert.start}${chartCriterion.field}${idx}-start-alert-annotation`}
alertStart={alert.start}
color={euiTheme.colors.danger}
dateFormat={uiSettings.get(UI_SETTINGS.DATE_FORMAT)}
/>,
<AlertActiveTimeRangeAnnotation
key={`${alert.start}${chartCriterion.field}${idx}-active-alert-annotation`}
id={`${alert.start}${chartCriterion.field}${idx}-active-alert-annotation`}
alertStart={alert.start}
alertEnd={alertEnd}
color={euiTheme.colors.danger}
/>,
]}
filterSeriesByGroupName={[selectedSeries]}
height={150}
interval={interval}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
});
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
} else return null;
};
@ -269,5 +252,45 @@ const AlertDetailsAppSection = ({
</CoPilotContextProvider>
);
};
function convertComparatorToFill(comparator: Comparator) {
switch (comparator) {
case Comparator.GT:
case Comparator.GT_OR_EQ:
return 'above';
default:
return 'below';
}
}
function convertCriteriaToKQL(criteria: PartialCriterion) {
if (!criteria.value || !criteria.comparator || !criteria.field) {
return '';
}
switch (criteria.comparator) {
case Comparator.MATCH:
case Comparator.EQ:
return `${criteria.field} : "${criteria.value}"`;
case Comparator.NOT_MATCH:
case Comparator.NOT_EQ:
return `NOT ${criteria.field} : "${criteria.value}"`;
case Comparator.MATCH_PHRASE:
return `${criteria.field} : ${criteria.value}`;
case Comparator.NOT_MATCH_PHRASE:
return `NOT ${criteria.field} : ${criteria.value}`;
case Comparator.GT:
return `${criteria.field} > ${criteria.value}`;
case Comparator.GT_OR_EQ:
return `${criteria.field} >= ${criteria.value}`;
case Comparator.LT:
return `${criteria.field} < ${criteria.value}`;
case Comparator.LT_OR_EQ:
return `${criteria.field} <= ${criteria.value}`;
default:
return '';
}
}
// eslint-disable-next-line import/no-default-export
export default AlertDetailsAppSection;

View file

@ -18160,7 +18160,7 @@
"xpack.infra.linkTo.hostWithIp.loading": "Chargement de l'hôte avec l'adresse IP \"{hostIp}\" en cours.",
"xpack.infra.logFlyout.flyoutSubTitle": "À partir de l'index {indexName}",
"xpack.infra.logFlyout.flyoutTitle": "Détails de l'entrée de log {logEntryId}",
"xpack.infra.logs.alertDetails.chart.chartTitle": "Logs pour {field} {comparator} {value}",
"xpack.infra.logs.alertDetails.chart.chartTitle": "Logs pour {criteria}",
"xpack.infra.logs.alertFlyout.groupByOptimizationWarning": "Lors de la définition d'une valeur \"regrouper par\", nous recommandons fortement d'utiliser le comparateur \"{comparator}\" pour votre seuil. Cela peut permettre d'améliorer considérablement les performances.",
"xpack.infra.logs.alerting.threshold.groupedCountAlertReasonDescription": "{actualCount, plural, one {la {actualCount} dernière entrée de log} many {les {actualCount} dernières entrées de log} other {les {actualCount} dernières entrées de log}} dans {duration} pour {groupName}. Alerte lorsque {comparator} {expectedCount}.",
"xpack.infra.logs.alerting.threshold.groupedRatioAlertReasonDescription": "Le rapport des logs sélectionnés est de {actualRatio} dans les dernières {duration} pour {groupName}. Alerte lorsque {comparator} {expectedRatio}.",

View file

@ -18159,7 +18159,7 @@
"xpack.infra.linkTo.hostWithIp.loading": "IPアドレス「{hostIp}」のホストを読み込み中です。",
"xpack.infra.logFlyout.flyoutSubTitle": "インデックス{indexName}から",
"xpack.infra.logFlyout.flyoutTitle": "ログエントリ{logEntryId}の詳細",
"xpack.infra.logs.alertDetails.chart.chartTitle": "{field} {comparator} {value}のログ",
"xpack.infra.logs.alertDetails.chart.chartTitle": "{criteria} のログ",
"xpack.infra.logs.alertFlyout.groupByOptimizationWarning": "「group by」を設定するときには、しきい値で\"{comparator}\"比較演算子を使用することを強くお勧めします。これにより、パフォーマンスを大きく改善できます。",
"xpack.infra.logs.alerting.threshold.groupedCountAlertReasonDescription": "{groupName}の過去{duration}の{actualCount, plural, other {{actualCount}件のログエントリ}}。{comparator} {expectedCount}のときにアラートを通知します。",
"xpack.infra.logs.alerting.threshold.groupedRatioAlertReasonDescription": "{groupName}の過去{duration}における選択されたログの比率は{actualRatio}です。{comparator} {expectedRatio}のときにアラートを通知します。",

View file

@ -18159,7 +18159,7 @@
"xpack.infra.linkTo.hostWithIp.loading": "正在加载 IP 地址为“{hostIp}”的主机。",
"xpack.infra.logFlyout.flyoutSubTitle": "从索引 {indexName}",
"xpack.infra.logFlyout.flyoutTitle": "日志条目 {logEntryId} 的详细信息",
"xpack.infra.logs.alertDetails.chart.chartTitle": "{field} {comparator} {value} 的日志",
"xpack.infra.logs.alertDetails.chart.chartTitle": "{criteria} 的日志",
"xpack.infra.logs.alertFlyout.groupByOptimizationWarning": "设置“分组依据”时,强烈建议将“{comparator}”比较符用于阈值。这会使性能有较大提升。",
"xpack.infra.logs.alerting.threshold.groupedCountAlertReasonDescription": "对于 {groupName},过去 {duration}中有 {actualCount, plural, other {{actualCount} 个日志条目}}。{comparator} {expectedCount} 时告警。",
"xpack.infra.logs.alerting.threshold.groupedRatioAlertReasonDescription": "对于 {groupName},过去 {duration}选定日志的比率为 {actualRatio}。{comparator} {expectedRatio} 时告警。",