[ML] AIOps Log Rate Analysis: improve explanation of log rate spike/dip (#186342)

## Summary

Related issue: https://github.com/elastic/kibana/issues/182714

This PR adds a `Log rate change` column to Log rate analysis results
table.

The log rate change is calculated by getting the number of buckets for
baseline/deviation using the timerange and interval and then comparing
the average rates per bucket for baseline vs deviation.

<img width="1466" alt="image"
src="9b2f1b80-d1d5-407e-908c-f611e54be4f3">

<img width="1471" alt="image"
src="56cb2c35-3758-4b24-9f50-2f99242af1b3">



### Checklist

Delete any items that are not applicable to this PR.

- [ ] 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Melissa Alvarez 2024-06-24 10:41:59 -06:00 committed by GitHub
parent 1cbe34ba35
commit 702442d8c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 353 additions and 13 deletions

View file

@ -186,7 +186,11 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
);
const [shouldStart, setShouldStart] = useState(false);
const [toggleIdSelected, setToggleIdSelected] = useState(resultsGroupedOffId);
const [skippedColumns, setSkippedColumns] = useState<ColumnNames[]>(['p-value']);
const [skippedColumns, setSkippedColumns] = useState<ColumnNames[]>([
'p-value',
'Baseline rate',
'Deviation rate',
]);
const onGroupResultsToggle = (optionId: string) => {
setToggleIdSelected(optionId);

View file

@ -0,0 +1,97 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { LOG_RATE_ANALYSIS_TYPE } from '@kbn/aiops-log-rate-analysis';
export function getLogRateChange(
analysisType: typeof LOG_RATE_ANALYSIS_TYPE[keyof typeof LOG_RATE_ANALYSIS_TYPE],
baselineBucketRate: number,
deviationBucketRate: number
) {
let message;
let factor;
if (analysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE) {
if (baselineBucketRate > 0) {
factor = Math.round(((deviationBucketRate / baselineBucketRate) * 100) / 100);
message = i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTableGroups.logRateFactorIncreaseLabel',
{
defaultMessage: '{factor}x higher',
values: {
factor,
},
}
);
} else {
message = i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTableGroups.logRateDocIncreaseLabel',
{
defaultMessage:
'{deviationBucketRate} {deviationBucketRate, plural, one {doc} other {docs}} rate up from 0 in baseline',
values: { deviationBucketRate },
}
);
}
} else {
if (deviationBucketRate > 0) {
// For dip, "doc count" refers to the amount of documents in the baseline time range so we use baselineBucketRate
factor = Math.round(((baselineBucketRate / deviationBucketRate) * 100) / 100);
message = i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTableGroups.logRateFactorDecreaseLabel',
{
defaultMessage: '{factor}x lower',
values: {
factor,
},
}
);
} else {
message = i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTableGroups.logRateDocDecreaseLabel',
{
defaultMessage: 'docs rate down to 0 from {baselineBucketRate} in baseline',
values: { baselineBucketRate },
}
);
}
}
return { message, factor };
}
export function getBaselineAndDeviationRates(
analysisType: typeof LOG_RATE_ANALYSIS_TYPE[keyof typeof LOG_RATE_ANALYSIS_TYPE],
baselineBuckets: number,
deviationBuckets: number,
docCount: number | undefined,
bgCount: number | undefined
) {
let baselineBucketRate;
let deviationBucketRate;
if (analysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE) {
if (bgCount !== undefined) {
baselineBucketRate = Math.round(bgCount / baselineBuckets);
}
if (docCount !== undefined) {
deviationBucketRate = Math.round(docCount / deviationBuckets);
}
} else {
// For dip, the "doc count" refers to the amount of documents in the baseline time range so we set baselineBucketRate
if (docCount !== undefined) {
baselineBucketRate = Math.round(docCount / baselineBuckets);
}
if (bgCount !== undefined) {
deviationBucketRate = Math.round(bgCount / deviationBuckets);
}
}
return { baselineBucketRate, deviationBucketRate };
}

View file

@ -149,6 +149,7 @@ export const LogRateAnalysisResultsGroupsTable: FC<LogRateAnalysisResultsTablePr
{
'data-test-subj': 'aiopsLogRateAnalysisResultsGroupsTableColumnGroup',
field: 'group',
width: skippedColumns.length < 3 ? '34%' : '50%',
name: (
<>
<FormattedMessage

View file

@ -6,7 +6,14 @@
*/
import React, { useMemo } from 'react';
import { type EuiBasicTableColumn, EuiBadge, EuiCode, EuiIconTip, EuiText } from '@elastic/eui';
import {
type EuiBasicTableColumn,
EuiBadge,
EuiCode,
EuiIcon,
EuiIconTip,
EuiText,
} from '@elastic/eui';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@ -14,6 +21,7 @@ import { type SignificantItem, SIGNIFICANT_ITEM_TYPE } from '@kbn/ml-agg-utils';
import { getCategoryQuery } from '@kbn/aiops-log-pattern-analysis/get_category_query';
import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats';
import { useAppSelector } from '@kbn/aiops-log-rate-analysis/state';
import { LOG_RATE_ANALYSIS_TYPE } from '@kbn/aiops-log-rate-analysis';
import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label';
import { FieldStatsPopover } from '../field_stats_popover';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
@ -23,10 +31,9 @@ import { useViewInDiscoverAction } from './use_view_in_discover_action';
import { useViewInLogPatternAnalysisAction } from './use_view_in_log_pattern_analysis_action';
import { useCopyToClipboardAction } from './use_copy_to_clipboard_action';
import { MiniHistogram } from '../mini_histogram';
import { getBaselineAndDeviationRates, getLogRateChange } from './get_baseline_and_deviation_rates';
const TRUNCATE_TEXT_LINES = 3;
const ACTIONS_COLUMN_WIDTH = '60px';
const NARROW_COLUMN_WIDTH = '120px';
const UNIQUE_COLUMN_WIDTH = '40px';
const NOT_AVAILABLE = '--';
@ -43,6 +50,24 @@ export const commonColumns = {
['Impact']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.impactColumnTitle', {
defaultMessage: 'Impact',
}),
['Baseline rate']: i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTable.baselineRateColumnTitle',
{
defaultMessage: 'Baseline rate',
}
),
['Deviation rate']: i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTable.deviationRateColumnTitle',
{
defaultMessage: 'Deviation rate',
}
),
['Log rate change']: i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTable.logRateChangeColumnTitle',
{
defaultMessage: 'Log rate change',
}
),
['Actions']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.actionsColumnTitle', {
defaultMessage: 'Actions',
}),
@ -96,6 +121,25 @@ const impactMessage = i18n.translate(
defaultMessage: 'The level of impact of the field on the message rate difference.',
}
);
const logRateChangeMessage = i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTableGroups.logRateChangeLabelColumnTooltip',
{
defaultMessage:
'The factor by which the log rate changed. This value is normalized to account for differing lengths in baseline and deviation time ranges.',
}
);
const baselineRateMessage = i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTableGroups.baselineRateLabelColumnTooltip',
{
defaultMessage: 'The average number of documents per baseline bucket.',
}
);
const deviationRateMessage = i18n.translate(
'xpack.aiops.logRateAnalysis.resultsTableGroups.deviationRateLabelColumnTooltip',
{
defaultMessage: 'The average number of documents per deviation bucket.',
}
);
export const useColumns = (
tableType: LogRateAnalysisResultsTableType,
@ -117,8 +161,14 @@ export const useColumns = (
const loading = useAppSelector((s) => s.logRateAnalysisStream.isRunning);
const zeroDocsFallback = useAppSelector((s) => s.logRateAnalysisResults.zeroDocsFallback);
const {
analysisType,
windowParameters,
documentStats: { documentCountStats },
} = useAppSelector((s) => s.logRateAnalysis);
const isGroupsTable = tableType === LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE.GROUPS;
const interval = documentCountStats?.interval ?? 0;
const fieldStatsServices: FieldStatsServices = useMemo(() => {
return {
@ -130,11 +180,22 @@ export const useColumns = (
};
}, [uiSettings, data, fieldFormats, charts]);
const buckets = useMemo(() => {
if (windowParameters === undefined) return;
const { baselineMin, baselineMax, deviationMin, deviationMax } = windowParameters;
const baselineBuckets = (baselineMax - baselineMin) / interval;
const deviationBuckets = (deviationMax - deviationMin) / interval;
return { baselineBuckets, deviationBuckets };
}, [windowParameters, interval]);
const columnsMap: Record<ColumnNames, EuiBasicTableColumn<SignificantItem>> = useMemo(
() => ({
['Field name']: {
'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnFieldName',
field: 'fieldName',
width: skippedColumns.length < 3 ? '17%' : '25%',
name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.fieldNameLabel', {
defaultMessage: 'Field name',
}),
@ -197,6 +258,7 @@ export const useColumns = (
['Field value']: {
'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnFieldValue',
field: 'fieldValue',
width: skippedColumns.length < 3 ? '17%' : '25%',
name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.fieldValueLabel', {
defaultMessage: 'Field value',
}),
@ -220,7 +282,7 @@ export const useColumns = (
},
['Log rate']: {
'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnLogRate',
width: NARROW_COLUMN_WIDTH,
width: '8%',
field: 'pValue',
name: (
<>
@ -253,7 +315,7 @@ export const useColumns = (
},
['Impact']: {
'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnImpact',
width: NARROW_COLUMN_WIDTH,
width: '8%',
field: 'pValue',
name: (
<>
@ -280,9 +342,149 @@ export const useColumns = (
sortable: true,
valign: 'middle',
},
['Baseline rate']: {
'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnBaselineRateChange',
field: 'bg_count',
name: (
<>
<FormattedMessage
id="xpack.aiops.logRateAnalysis.resultsTable.baselineRateLabel"
defaultMessage="Baseline rate"
/>
&nbsp;
<EuiIconTip
size="s"
position="top"
color="subdued"
type="questionInCircle"
className="eui-alignTop"
content={baselineRateMessage}
/>
</>
),
render: (_, { bg_count: bgCount, doc_count: docCount }) => {
if (
interval === 0 ||
windowParameters === undefined ||
buckets === undefined ||
isGroupsTable
)
return NOT_AVAILABLE;
const { baselineBucketRate } = getBaselineAndDeviationRates(
analysisType,
buckets.baselineBuckets,
buckets.deviationBuckets,
docCount,
bgCount
);
return <>{baselineBucketRate}</>;
},
sortable: true,
valign: 'middle',
},
['Deviation rate']: {
'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnDeviationRateChange',
field: 'doc_count',
name: (
<>
<FormattedMessage
id="xpack.aiops.logRateAnalysis.resultsTable.deviationRateLabel"
defaultMessage="Deviation rate"
/>
&nbsp;
<EuiIconTip
size="s"
position="top"
color="subdued"
type="questionInCircle"
className="eui-alignTop"
content={deviationRateMessage}
/>
</>
),
render: (_, { doc_count: docCount, bg_count: bgCount }) => {
if (
interval === 0 ||
windowParameters === undefined ||
buckets === undefined ||
isGroupsTable
)
return NOT_AVAILABLE;
const { deviationBucketRate } = getBaselineAndDeviationRates(
analysisType,
buckets.baselineBuckets,
buckets.deviationBuckets,
docCount,
bgCount
);
return <>{deviationBucketRate}</>;
},
sortable: true,
valign: 'middle',
},
['Log rate change']: {
'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnLogRateChange',
name: (
<>
<FormattedMessage
id="xpack.aiops.logRateAnalysis.resultsTable.logRateChangeLabel"
defaultMessage="Log rate change"
/>
&nbsp;
<EuiIconTip
size="s"
position="top"
color="subdued"
type="questionInCircle"
className="eui-alignTop"
content={logRateChangeMessage}
/>
</>
),
render: ({ doc_count: docCount, bg_count: bgCount }: SignificantItem) => {
if (
interval === 0 ||
windowParameters === undefined ||
buckets === undefined ||
isGroupsTable
)
return NOT_AVAILABLE;
const { baselineBucketRate, deviationBucketRate } = getBaselineAndDeviationRates(
analysisType,
buckets.baselineBuckets,
buckets.deviationBuckets,
docCount,
bgCount
);
const logRateChange = getLogRateChange(
analysisType,
baselineBucketRate!,
deviationBucketRate!
);
return (
<>
<EuiIcon
size="s"
color="subdued"
type={analysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE ? 'sortUp' : 'sortDown'}
className="eui-alignTop"
/>
&nbsp;
{logRateChange.message}
</>
);
},
valign: 'middle',
},
['p-value']: {
'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnPValue',
width: NARROW_COLUMN_WIDTH,
field: 'pValue',
name: (
<>
@ -315,7 +517,7 @@ export const useColumns = (
'data-test-subj': isGroupsTable
? 'aiopsLogRateAnalysisResultsGroupsTableColumnDocCount'
: 'aiopsLogRateAnalysisResultsTableColumnDocCount',
width: NARROW_COLUMN_WIDTH,
width: '8%',
field: isGroupsTable ? 'docCount' : 'doc_count',
name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.docCountLabel', {
defaultMessage: 'Doc count',
@ -333,7 +535,7 @@ export const useColumns = (
...(viewInLogPatternAnalysisAction ? [viewInLogPatternAnalysisAction] : []),
copyToClipBoardAction,
],
width: ACTIONS_COLUMN_WIDTH,
width: '4%',
valign: 'middle',
},
unique: {

View file

@ -151,7 +151,16 @@ export const getArtificialLogDataViewTestData = ({
analysisGroupsTable: getAnalysisGroupsTable(),
filteredAnalysisGroupsTable: getFilteredAnalysisGroupsTable(),
analysisTable: getAnalysisTable(),
columnSelectorPopover: ['Log rate', 'Doc count', 'p-value', 'Impact', 'Actions'],
columnSelectorPopover: [
'Log rate',
'Doc count',
'p-value',
'Impact',
'Baseline rate',
'Deviation rate',
'Log rate change',
'Actions',
],
fieldSelectorPopover: getFieldSelectorPopover(),
globalState: {
refreshInterval: { pause: true, value: 60000 },

View file

@ -25,7 +25,16 @@ export const farequoteDataViewTestData: TestData = {
expected: {
totalDocCountFormatted: '86,374',
sampleProbabilityFormatted: '0.5',
columnSelectorPopover: ['Log rate', 'Doc count', 'p-value', 'Impact', 'Actions'],
columnSelectorPopover: [
'Log rate',
'Doc count',
'p-value',
'Impact',
'Baseline rate',
'Deviation rate',
'Log rate change',
'Actions',
],
fieldSelectorPopover: ['airline', 'custom_field.keyword'],
globalState: {
refreshInterval: { pause: true, value: 60000 },

View file

@ -44,7 +44,16 @@ export const farequoteDataViewTestDataWithQuery: TestData = {
impact: 'High',
},
],
columnSelectorPopover: ['Log rate', 'Doc count', 'p-value', 'Impact', 'Actions'],
columnSelectorPopover: [
'Log rate',
'Doc count',
'p-value',
'Impact',
'Baseline rate',
'Deviation rate',
'Log rate change',
'Actions',
],
fieldSelectorPopover: ['airline', 'custom_field.keyword'],
globalState: {
refreshInterval: { pause: true, value: 60000 },

View file

@ -70,7 +70,16 @@ export const kibanaLogsDataViewTestData: TestData = {
logRate: 'Chart type:bar chart',
impact: 'High',
})),
columnSelectorPopover: ['Log rate', 'Doc count', 'p-value', 'Impact', 'Actions'],
columnSelectorPopover: [
'Log rate',
'Doc count',
'p-value',
'Impact',
'Baseline rate',
'Deviation rate',
'Log rate change',
'Actions',
],
fieldSelectorPopover: [
'agent.keyword',
'clientip',