[ML] AIOps: Auto-detect if spike or dip selected in log rate analysis. (#163100)

This updates log rate analysis to be able to auto-detect whether the
selected deviation is a spike or dip compared to the baseline time
range. To achieve this, we compare the median bucket size of the two
selections. If a dip gets detected, the analysis will then switch the
window parameters sent to the API endpoint to run the analysis.

An info callout points out the auto-selected analysis type and explains
to which time range the analysis results refer to. We need to do this to
make it clear that for dip analysis the significant terms and their doc
counts refer to the baseline time range and vice versa for spike
analysis.
This commit is contained in:
Walter Rafelsberger 2023-08-09 08:05:07 +02:00 committed by GitHub
parent 0b95987782
commit da0fb1d987
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 401 additions and 237 deletions

View file

@ -7,5 +7,9 @@
export { DualBrush, DualBrushAnnotation } from './src/dual_brush';
export { ProgressControls } from './src/progress_controls';
export { DocumentCountChart } from './src/document_count_chart';
export type { DocumentCountChartPoint, DocumentCountChartProps } from './src/document_count_chart';
export {
DocumentCountChart,
type BrushSettings,
type BrushSelectionUpdateHandler,
} from './src/document_count_chart';
export type { DocumentCountChartProps } from './src/document_count_chart';

View file

@ -27,14 +27,21 @@ import {
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from '@kbn/core/public';
import { getSnappedWindowParameters, getWindowParameters } from '@kbn/aiops-utils';
import type { WindowParameters } from '@kbn/aiops-utils';
import {
getLogRateAnalysisType,
getSnappedWindowParameters,
getWindowParameters,
type LogRateAnalysisType,
type LogRateHistogramItem,
type WindowParameters,
} from '@kbn/aiops-utils';
import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { DualBrush, DualBrushAnnotation } from '../..';
import { BrushBadge } from './brush_badge';
declare global {
@ -51,20 +58,6 @@ interface TimeFilterRange {
to: number;
}
/**
* Datum for the bar chart
*/
export interface DocumentCountChartPoint {
/**
* Time of bucket
*/
time: number | string;
/**
* Number of doc count for that time bucket
*/
value: number;
}
/**
* Brush settings
*/
@ -83,6 +76,19 @@ export interface BrushSettings {
badgeWidth?: number;
}
/**
* Callback function which gets called when the brush selection has changed
*
* @param windowParameters Baseline and deviation time ranges.
* @param force Force update
* @param logRateAnalysisType `spike` or `dip` based on median log rate bucket size
*/
export type BrushSelectionUpdateHandler = (
windowParameters: WindowParameters,
force: boolean,
logRateAnalysisType: LogRateAnalysisType
) => void;
/**
* Props for document count chart
*/
@ -94,14 +100,14 @@ export interface DocumentCountChartProps {
fieldFormats: FieldFormatsStart;
uiSettings: IUiSettingsClient;
};
/** Optional callback function which gets called the brush selection has changed */
brushSelectionUpdateHandler?: (windowParameters: WindowParameters, force: boolean) => void;
/** Optional callback for handling brush selection updates */
brushSelectionUpdateHandler?: BrushSelectionUpdateHandler;
/** Optional width */
width?: number;
/** Data chart points */
chartPoints: DocumentCountChartPoint[];
chartPoints: LogRateHistogramItem[];
/** Data chart points split */
chartPointsSplit?: DocumentCountChartPoint[];
chartPointsSplit?: LogRateHistogramItem[];
/** Start time range for the chart */
timeRangeEarliest: number;
/** Ending time range for the chart */
@ -162,42 +168,30 @@ function getBaselineBadgeOverflow(
/**
* Document count chart with draggable brushes to select time ranges
* by default use `Baseline` and `Deviation` for the badge names
* @param dependencies - List of Kibana services that are required as dependencies
* @param brushSelectionUpdateHandler - Optional callback function which gets called the brush selection has changed
* @param width - Optional width
* @param chartPoints - Data chart points
* @param chartPointsSplit - Data chart points split
* @param timeRangeEarliest - Start time range for the chart
* @param timeRangeLatest - Ending time range for the chart
* @param interval - Time interval for the document count buckets
* @param chartPointsSplitLabel - Label to name the adjustedChartPointsSplit histogram
* @param isBrushCleared - Whether or not brush has been reset
* @param autoAnalysisStart - Timestamp for start of initial analysis
* @param barColorOverride - Optional color override for the default bar color for charts
* @param barStyleAccessor - Optional style to override bar chart
* @param barHighlightColorOverride - Optional color override for the highlighted bar color for charts
* @param deviationBrush - Optional settings override for the 'deviation' brush
* @param baselineBrush - Optional settings override for the 'baseline' brush
* @constructor
*
* @param props DocumentCountChart component props
* @returns The DocumentCountChart component.
*/
export const DocumentCountChart: FC<DocumentCountChartProps> = ({
dependencies,
brushSelectionUpdateHandler,
width,
chartPoints,
chartPointsSplit,
timeRangeEarliest,
timeRangeLatest,
interval,
chartPointsSplitLabel,
isBrushCleared,
autoAnalysisStart,
barColorOverride,
barStyleAccessor,
barHighlightColorOverride,
deviationBrush = {},
baselineBrush = {},
}) => {
export const DocumentCountChart: FC<DocumentCountChartProps> = (props) => {
const {
dependencies,
brushSelectionUpdateHandler,
width,
chartPoints,
chartPointsSplit,
timeRangeEarliest,
timeRangeLatest,
interval,
chartPointsSplitLabel,
isBrushCleared,
autoAnalysisStart,
barColorOverride,
barStyleAccessor,
barHighlightColorOverride,
deviationBrush = {},
baselineBrush = {},
} = props;
const { data, uiSettings, fieldFormats, charts } = dependencies;
const chartTheme = charts.theme.useChartsTheme();
@ -333,8 +327,13 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
const wpSnap = getSnappedWindowParameters(wp, snapTimestamps);
setOriginalWindowParameters(wpSnap);
setWindowParameters(wpSnap);
if (brushSelectionUpdateHandler !== undefined) {
brushSelectionUpdateHandler(wpSnap, true);
brushSelectionUpdateHandler(
wpSnap,
true,
getLogRateAnalysisType(adjustedChartPoints, wpSnap)
);
}
}
}
@ -385,7 +384,7 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
}
setWindowParameters(wp);
setWindowParametersAsPixels(wpPx);
brushSelectionUpdateHandler(wp, false);
brushSelectionUpdateHandler(wp, false, getLogRateAnalysisType(adjustedChartPoints, wp));
}
const [mlBrushWidth, setMlBrushWidth] = useState<number>();

View file

@ -6,4 +6,8 @@
*/
export { DocumentCountChart } from './document_count_chart';
export type { DocumentCountChartPoint, DocumentCountChartProps } from './document_count_chart';
export type {
BrushSelectionUpdateHandler,
BrushSettings,
DocumentCountChartProps,
} from './document_count_chart';

View file

@ -6,7 +6,7 @@
*/
import { isEqual } from 'lodash';
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, type FC } from 'react';
import * as d3Brush from 'd3-brush';
import * as d3Scale from 'd3-scale';
@ -54,6 +54,9 @@ const BRUSH_MARGIN = 4;
const BRUSH_HANDLE_SIZE = 4;
const BRUSH_HANDLE_ROUNDED_CORNER = 2;
/**
* Props for the DualBrush React Component
*/
interface DualBrushProps {
/**
* Min and max numeric timestamps for the two brushes
@ -88,40 +91,12 @@ interface DualBrushProps {
/**
* DualBrush React Component
* Dual brush component that overlays the document count chart
* @type {FC<DualBrushProps>}
* @param props - `DualBrushProps` component props
* @returns {React.ReactElement} The DualBrush component.
*
* @param props DualBrushProps component props
* @returns The DualBrush component.
*/
export function DualBrush({
/**
* Min and max numeric timestamps for the two brushes
*/
windowParameters,
/**
* Min timestamp for x domain
*/
min,
/**
* Max timestamp for x domain
*/
max,
/**
* Callback function whenever the brush changes
*/
onChange,
/**
* Margin left
*/
marginLeft,
/**
* Nearest timestamps to snap to the brushes to
*/
snapTimestamps,
/**
* Width
*/
width,
}: DualBrushProps) {
export const DualBrush: FC<DualBrushProps> = (props) => {
const { windowParameters, min, max, onChange, marginLeft, snapTimestamps, width } = props;
const d3BrushContainer = useRef(null);
const brushes = useRef<DualBrush[]>([]);
@ -383,4 +358,4 @@ export function DualBrush({
)}
</>
);
}
};

View file

@ -21,11 +21,12 @@ interface BrushAnnotationProps {
/**
* DualBrushAnnotation React Component
* Dual brush annotation component that overlays the document count chart
* @type {FC<BrushAnnotationProps>}
* @param props - `BrushAnnotationProps` component props
* @returns {React.ReactElement} The DualBrushAnnotation component.
*
* @param props BrushAnnotationProps component props
* @returns The DualBrushAnnotation component.
*/
export const DualBrushAnnotation: FC<BrushAnnotationProps> = ({ id, min, max, style }) => {
export const DualBrushAnnotation: FC<BrushAnnotationProps> = (props) => {
const { id, min, max, style } = props;
const { euiTheme } = useEuiTheme();
const { colors } = euiTheme;

View file

@ -44,38 +44,25 @@ interface ProgressControlProps {
/**
* ProgressControls React Component
* Component with ability to Run & cancel analysis
* by default use `Baseline` and `Deviation` for the badge name
* @type {FC<ProgressControlProps>}
* @param children - List of Kibana services that are required as dependencies
* @param brushSelectionUpdateHandler - Optional callback function which gets called the brush selection has changed
* @param width - Optional width
* @param chartPoints - Data chart points
* @param chartPointsSplit - Data chart points split
* @param timeRangeEarliest - Start time range for the chart
* @param timeRangeLatest - Ending time range for the chart
* @param interval - Time interval for the document count buckets
* @param chartPointsSplitLabel - Label to name the adjustedChartPointsSplit histogram
* @param isBrushCleared - Whether or not brush has been reset
* @param autoAnalysisStart - Timestamp for start of initial analysis
* @param barColorOverride - Optional color override for the default bar color for charts
* @param barStyleAccessor - Optional style to override bar chart
* @param barHighlightColorOverride - Optional color override for the highlighted bar color for charts
* @param deviationBrush - Optional settings override for the 'deviation' brush
* @param baselineBrush - Optional settings override for the 'baseline' brush
* @returns {React.ReactElement} The ProgressControls component.
* by default uses `Baseline` and `Deviation` for the badge name
*
* @param props ProgressControls component props
* @returns The ProgressControls component.
*/
export const ProgressControls: FC<ProgressControlProps> = ({
children,
isBrushCleared,
progress,
progressMessage,
onRefresh,
onCancel,
onReset,
isRunning,
shouldRerunAnalysis,
runAnalysisDisabled = false,
}) => {
export const ProgressControls: FC<ProgressControlProps> = (props) => {
const {
children,
isBrushCleared,
progress,
progressMessage,
onRefresh,
onCancel,
onReset,
isRunning,
shouldRerunAnalysis,
runAnalysisDisabled = false,
} = props;
const { euiTheme } = useEuiTheme();
const runningProgressBarStyles = useAnimatedProgressBarBackground(euiTheme.colors.success);
const analysisCompleteStyle = { display: 'none' };

View file

@ -0,0 +1,57 @@
/*
* 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 type { LogRateHistogramItem } from './log_rate_histogram_item';
import { getLogRateAnalysisType } from './get_log_rate_analysis_type';
describe('getLogRateAnalysisType', () => {
const LogRateHistogramMock: LogRateHistogramItem[] = [
{ time: 0, value: 10 },
{ time: 1, value: 10 },
{ time: 2, value: 10 },
{ time: 3, value: 5 },
{ time: 4, value: 10 },
{ time: 5, value: 10 },
{ time: 6, value: 10 },
{ time: 7, value: 20 },
{ time: 8, value: 10 },
{ time: 9, value: 10 },
];
test('returns "spike" for the given parameters', () => {
expect(
getLogRateAnalysisType(LogRateHistogramMock, {
baselineMin: 4,
baselineMax: 6,
deviationMin: 7,
deviationMax: 8,
})
).toBe('spike');
});
test('returns "dip" for the given parameters', () => {
expect(
getLogRateAnalysisType(LogRateHistogramMock, {
baselineMin: 0,
baselineMax: 2,
deviationMin: 3,
deviationMax: 4,
})
).toBe('dip');
});
test('falls back to "spike" if both time range have the same median', () => {
expect(
getLogRateAnalysisType(LogRateHistogramMock, {
baselineMin: 0,
baselineMax: 2,
deviationMin: 4,
deviationMax: 6,
})
).toBe('spike');
});
});

View file

@ -0,0 +1,40 @@
/*
* 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 { median } from 'd3-array';
import { LOG_RATE_ANALYSIS_TYPE, type LogRateAnalysisType } from './log_rate_analysis_type';
import type { LogRateHistogramItem } from './log_rate_histogram_item';
import type { WindowParameters } from './window_parameters';
/**
* Identify the log rate analysis type based on the baseline/deviation
* time ranges on a given log rate histogram.
*
* @param logRateHistogram The log rate histogram.
* @param windowParameters The window parameters with baseline and deviation time range.
* @returns The log rate analysis type.
*/
export function getLogRateAnalysisType(
logRateHistogram: LogRateHistogramItem[],
windowParameters: WindowParameters
): LogRateAnalysisType {
const { baselineMin, baselineMax, deviationMin, deviationMax } = windowParameters;
const baselineItems = logRateHistogram.filter(
(d) => d.time >= baselineMin && d.time < baselineMax
);
const baselineMedian = median(baselineItems.map((d) => d.value)) ?? 0;
const deviationItems = logRateHistogram.filter(
(d) => d.time >= deviationMin && d.time < deviationMax
);
const deviationMedian = median(deviationItems.map((d) => d.value)) ?? 0;
return deviationMedian >= baselineMedian
? LOG_RATE_ANALYSIS_TYPE.SPIKE
: LOG_RATE_ANALYSIS_TYPE.DIP;
}

View file

@ -5,5 +5,11 @@
* 2.0.
*/
export { getSnappedWindowParameters, getWindowParameters } from './src/get_window_parameters';
export type { WindowParameters } from './src/get_window_parameters';
export { getLogRateAnalysisType } from './get_log_rate_analysis_type';
export { LOG_RATE_ANALYSIS_TYPE, type LogRateAnalysisType } from './log_rate_analysis_type';
export { type LogRateHistogramItem } from './log_rate_histogram_item';
export {
getSnappedWindowParameters,
getWindowParameters,
type WindowParameters,
} from './window_parameters';

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* The type of log rate analysis (spike or dip) will affect how parameters are
* passed to the analysis API endpoint.
*/
export const LOG_RATE_ANALYSIS_TYPE = {
SPIKE: 'spike',
DIP: 'dip',
} as const;
/**
* Union type of log rate analysis types.
*/
export type LogRateAnalysisType =
typeof LOG_RATE_ANALYSIS_TYPE[keyof typeof LOG_RATE_ANALYSIS_TYPE];

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Log rate histogram item
*/
export interface LogRateHistogramItem {
/**
* Time of bucket
*/
time: number | string;
/**
* Number of doc count for that time bucket
*/
value: number;
}

View file

@ -10,20 +10,6 @@
*/
export const LOG_RATE_ANALYSIS_P_VALUE_THRESHOLD = 0.02;
/**
* The type of log rate analysis (spike or dip) will affect how parameters are
* passed to the analysis API endpoint.
*/
export const LOG_RATE_ANALYSIS_TYPE = {
SPIKE: 'spike',
DIP: 'dip',
} as const;
/**
* Union type of log rate analysis types.
*/
export type LogRateAnalysisType =
typeof LOG_RATE_ANALYSIS_TYPE[keyof typeof LOG_RATE_ANALYSIS_TYPE];
/**
* For the technical preview of Log Rate Analysis we use a hard coded seed.
* In future versions we might use a user specific seed or let the user customise it.

View file

@ -5,8 +5,6 @@
* 2.0.
*/
export type { LogRateAnalysisType } from './constants';
/**
* PLUGIN_ID is used as a unique identifier for the aiops plugin
*/

View file

@ -13,8 +13,8 @@ import {
RectAnnotationSpec,
} from '@elastic/charts/dist/chart_types/xy_chart/utils/specs';
import type { WindowParameters } from '@kbn/aiops-utils';
import { DocumentCountChart, type DocumentCountChartPoint } from '@kbn/aiops-components';
import type { LogRateHistogramItem, WindowParameters } from '@kbn/aiops-utils';
import { DocumentCountChart, type BrushSelectionUpdateHandler } from '@kbn/aiops-components';
import { useAiopsAppContext } from '../../../hooks/use_aiops_app_context';
import { DocumentCountStats } from '../../../get_document_stats';
@ -22,7 +22,7 @@ import { DocumentCountStats } from '../../../get_document_stats';
import { TotalCountHeader } from '../total_count_header';
export interface DocumentCountContentProps {
brushSelectionUpdateHandler: (d: WindowParameters, force: boolean) => void;
brushSelectionUpdateHandler: BrushSelectionUpdateHandler;
documentCountStats?: DocumentCountStats;
documentCountStatsSplit?: DocumentCountStats;
documentCountStatsSplitLabel?: string;
@ -78,14 +78,14 @@ export const DocumentCountContent: FC<DocumentCountContentProps> = ({
) : null;
}
const chartPoints: DocumentCountChartPoint[] = Object.entries(documentCountStats.buckets).map(
const chartPoints: LogRateHistogramItem[] = Object.entries(documentCountStats.buckets).map(
([time, value]) => ({
time: +time,
value,
})
);
let chartPointsSplit: DocumentCountChartPoint[] | undefined;
let chartPointsSplit: LogRateHistogramItem[] | undefined;
if (documentCountStatsSplit?.buckets !== undefined) {
chartPointsSplit = Object.entries(documentCountStatsSplit?.buckets).map(([time, value]) => ({
time: +time,

View file

@ -15,11 +15,13 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { Dictionary } from '@kbn/ml-url-state';
import type { WindowParameters } from '@kbn/aiops-utils';
import {
LOG_RATE_ANALYSIS_TYPE,
type LogRateAnalysisType,
type WindowParameters,
} from '@kbn/aiops-utils';
import type { SignificantTerm } from '@kbn/ml-agg-utils';
import { LOG_RATE_ANALYSIS_TYPE, type LogRateAnalysisType } from '../../../../common/constants';
import { useData } from '../../../hooks/use_data';
import { DocumentCountContent } from '../../document_count_content/document_count_content';
@ -48,8 +50,6 @@ export function getDocumentCountStatsSplitLabel(
export interface LogRateAnalysisContentProps {
/** The data view to analyze. */
dataView: DataView;
/** The type of analysis, whether it's a spike or dip */
analysisType?: LogRateAnalysisType;
setGlobalState?: (params: Dictionary<unknown>) => void;
/** Timestamp for the start of the range for initial analysis */
initialAnalysisStart?: number | WindowParameters;
@ -68,7 +68,6 @@ export interface LogRateAnalysisContentProps {
export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
dataView,
analysisType = LOG_RATE_ANALYSIS_TYPE.SPIKE,
setGlobalState,
initialAnalysisStart: incomingInitialAnalysisStart,
timeRange,
@ -83,6 +82,9 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
number | WindowParameters | undefined
>(incomingInitialAnalysisStart);
const [isBrushCleared, setIsBrushCleared] = useState(true);
const [logRateAnalysisType, setLogRateAnalysisType] = useState<LogRateAnalysisType>(
LOG_RATE_ANALYSIS_TYPE.SPIKE
);
useEffect(() => {
setIsBrushCleared(windowParameters === undefined);
@ -111,13 +113,18 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
const { sampleProbability, totalCount, documentCountStats, documentCountStatsCompare } =
documentStats;
function brushSelectionUpdate(d: WindowParameters, force: boolean) {
function brushSelectionUpdate(
windowParametersUpdate: WindowParameters,
force: boolean,
logRateAnalysisTypeUpdate: LogRateAnalysisType
) {
if (!isBrushCleared || force) {
setWindowParameters(d);
setWindowParameters(windowParametersUpdate);
}
if (force) {
setIsBrushCleared(false);
}
setLogRateAnalysisType(logRateAnalysisTypeUpdate);
}
function clearSelection() {
@ -153,7 +160,7 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
{earliest !== undefined && latest !== undefined && windowParameters !== undefined && (
<LogRateAnalysisResults
dataView={dataView}
analysisType={analysisType}
analysisType={logRateAnalysisType}
earliest={earliest}
isBrushCleared={isBrushCleared}
latest={latest}

View file

@ -20,7 +20,6 @@ import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { LOG_RATE_ANALYSIS_TYPE, type LogRateAnalysisType } from '../../../../common/constants';
import { timeSeriesDataViewWarning } from '../../../application/utils/time_series_dataview_check';
import { AiopsAppContext, type AiopsAppDependencies } from '../../../hooks/use_aiops_app_context';
import { DataSourceContext } from '../../../hooks/use_data_source';
@ -38,8 +37,6 @@ const localStorage = new Storage(window.localStorage);
export interface LogRateAnalysisContentWrapperProps {
/** The data view to analyze. */
dataView: DataView;
/** The type of analysis, whether it's a spike or dip */
analysisType?: LogRateAnalysisType;
/** Option to make main histogram sticky */
stickyHistogram?: boolean;
/** App dependencies */
@ -65,7 +62,6 @@ export interface LogRateAnalysisContentWrapperProps {
export const LogRateAnalysisContentWrapper: FC<LogRateAnalysisContentWrapperProps> = ({
dataView,
analysisType = LOG_RATE_ANALYSIS_TYPE.SPIKE,
appDependencies,
setGlobalState,
initialAnalysisStart,
@ -100,7 +96,6 @@ export const LogRateAnalysisContentWrapper: FC<LogRateAnalysisContentWrapperProp
<DatePickerContextProvider {...datePickerDeps}>
<LogRateAnalysisContent
dataView={dataView}
analysisType={analysisType}
setGlobalState={setGlobalState}
initialAnalysisStart={initialAnalysisStart}
timeRange={timeRange}

View file

@ -24,13 +24,16 @@ import {
import type { DataView } from '@kbn/data-views-plugin/public';
import { ProgressControls } from '@kbn/aiops-components';
import { useFetchStream } from '@kbn/ml-response-stream/client';
import type { WindowParameters } from '@kbn/aiops-utils';
import {
LOG_RATE_ANALYSIS_TYPE,
type LogRateAnalysisType,
type WindowParameters,
} from '@kbn/aiops-utils';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { SignificantTerm, SignificantTermGroup } from '@kbn/ml-agg-utils';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { LOG_RATE_ANALYSIS_TYPE, type LogRateAnalysisType } from '../../../common/constants';
import { initialState, streamReducer } from '../../../common/api/stream_reducer';
import type { AiopsApiLogRateAnalysis } from '../../../common/api';
import {
@ -73,6 +76,8 @@ const resultsGroupedOnId = 'aiopsLogRateAnalysisGroupingOn';
* Interface for log rate analysis results data.
*/
export interface LogRateAnalysisResultsData {
/** The type of analysis, whether it's a spike or dip */
analysisType: LogRateAnalysisType;
/** Statistically significant field/value items. */
significantTerms: SignificantTerm[];
/** Statistically significant groups of field/value items. */
@ -129,6 +134,7 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
const { clearAllRowState } = useLogRateAnalysisResultsTableRowContext();
const [currentAnalysisType, setCurrentAnalysisType] = useState<LogRateAnalysisType | undefined>();
const [currentAnalysisWindowParameters, setCurrentAnalysisWindowParameters] = useState<
WindowParameters | undefined
>();
@ -215,6 +221,7 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
setOverrides(undefined);
if (onAnalysisCompleted) {
onAnalysisCompleted({
analysisType,
significantTerms: data.significantTerms,
significantTermsGroups: data.significantTermsGroups,
});
@ -241,6 +248,7 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
clearAllRowState();
}
setCurrentAnalysisType(analysisType);
setCurrentAnalysisWindowParameters(windowParameters);
// We trigger hooks updates above so we cannot directly call `start()` here
@ -257,6 +265,7 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
}, [shouldStart]);
useEffect(() => {
setCurrentAnalysisType(analysisType);
setCurrentAnalysisWindowParameters(windowParameters);
start();
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -341,6 +350,40 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
/>
</EuiFlexItem>
</ProgressControls>
{showLogRateAnalysisResultsTable && (
<>
<EuiSpacer size="s" />
<EuiCallOut
title={
<span data-test-subj="aiopsAnalysisTypeCalloutTitle">
{currentAnalysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE
? i18n.translate('xpack.aiops.analysis.analysisTypeSpikeCallOutTitle', {
defaultMessage: 'Analysis type: Log rate spike',
})
: i18n.translate('xpack.aiops.analysis.analysisTypeDipCallOutTitle', {
defaultMessage: 'Analysis type: Log rate dip',
})}
</span>
}
color="primary"
iconType="pin"
size="s"
>
<EuiText size="s">
{currentAnalysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE
? i18n.translate('xpack.aiops.analysis.analysisTypeSpikeCallOutContent', {
defaultMessage:
'The median log rate in the selected deviation time range is higher than the baseline. Therefore, the analysis results table shows statistically significant items within the deviation time range that are contributors to the spike. The "doc count" column refers to the amount of documents in the deviation time range.',
})
: i18n.translate('xpack.aiops.analysis.analysisTypeDipCallOutContent', {
defaultMessage:
'The median log rate in the selected deviation time range is lower than the baseline. Therefore, the analysis results table shows statistically significant items within the baseline time range that are less in number or missing within the deviation time range. The "doc count" column refers to the amount of documents in the baseline time range.',
})}
</EuiText>
</EuiCallOut>
<EuiSpacer size="xs" />
</>
)}
{errors.length > 0 ? (
<>
<EuiSpacer size="xs" />

View file

@ -13,8 +13,6 @@ export function plugin() {
return new AiopsPlugin();
}
export { LOG_RATE_ANALYSIS_TYPE, type LogRateAnalysisType } from '../common/constants';
export type { AiopsAppDependencies } from './hooks/use_aiops_app_context';
export type { LogRateAnalysisAppStateProps } from './components/log_rate_analysis';
export type { LogRateAnalysisContentWrapperProps } from './components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content_wrapper';

View file

@ -22,17 +22,14 @@ import {
import moment from 'moment';
import { IUiSettingsClient } from '@kbn/core/public';
import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common';
import type { LogRateHistogramItem } from '@kbn/aiops-utils';
import { EuiFlexGroup, EuiLoadingSpinner, EuiFlexItem } from '@elastic/eui';
import { useDataVisualizerKibana } from '../../../../kibana_context';
export interface DocumentCountChartPoint {
time: number | string;
value: number;
}
interface Props {
width?: number;
chartPoints: DocumentCountChartPoint[];
chartPoints: LogRateHistogramItem[];
timeRangeEarliest: number;
timeRangeLatest: number;
interval?: number;

View file

@ -5,5 +5,4 @@
* 2.0.
*/
export type { DocumentCountChartPoint } from './document_count_chart';
export { DocumentCountChart } from './document_count_chart';

View file

@ -20,7 +20,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DocumentCountChartPoint } from './document_count_chart';
import type { LogRateHistogramItem } from '@kbn/aiops-utils';
import {
RandomSamplerOption,
RANDOM_SAMPLER_SELECT_OPTIONS,
@ -108,7 +108,7 @@ export const DocumentCountContent: FC<Props> = ({
if (timeRangeEarliest === undefined || timeRangeLatest === undefined)
return <TotalCountHeader totalCount={totalCount} />;
let chartPoints: DocumentCountChartPoint[] = [];
let chartPoints: LogRateHistogramItem[] = [];
if (documentCountStats.buckets !== undefined) {
const buckets: Record<string, number> = documentCountStats?.buckets;
chartPoints = Object.entries(buckets).map(([time, value]) => ({ time: +time, value }));

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { WindowParameters } from '@kbn/aiops-utils';
import type { WindowParameters, LogRateHistogramItem } from '@kbn/aiops-utils';
import React, { FC } from 'react';
import { DocumentCountChart, type DocumentCountChartPoint } from '@kbn/aiops-components';
import { DocumentCountChart } from '@kbn/aiops-components';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { DocumentCountChartProps } from '@kbn/aiops-components';
import type { BrushSelectionUpdateHandler, DocumentCountChartProps } from '@kbn/aiops-components';
import { RandomSampler } from '@kbn/ml-random-sampler-utils';
import { useDataVisualizerKibana } from '../kibana_context';
import { DocumentCountStats } from '../../../common/types/field_stats';
@ -26,7 +26,7 @@ export interface DocumentCountContentProps
| 'interval'
| 'chartPointsSplitLabel'
> {
brushSelectionUpdateHandler: (d: WindowParameters, force: boolean) => void;
brushSelectionUpdateHandler: BrushSelectionUpdateHandler;
documentCountStats?: DocumentCountStats;
documentCountStatsSplit?: DocumentCountStats;
documentCountStatsSplitLabel?: string;
@ -83,14 +83,14 @@ export const DocumentCountWithDualBrush: FC<DocumentCountContentProps> = ({
return totalCount !== undefined ? <TotalCountHeader totalCount={totalCount} /> : null;
}
const chartPoints: DocumentCountChartPoint[] = Object.entries(documentCountStats.buckets).map(
const chartPoints: LogRateHistogramItem[] = Object.entries(documentCountStats.buckets).map(
([time, value]) => ({
time: +time,
value,
})
);
let chartPointsSplit: DocumentCountChartPoint[] | undefined;
let chartPointsSplit: LogRateHistogramItem[] | undefined;
if (documentCountStatsSplit?.buckets !== undefined) {
chartPointsSplit = Object.entries(documentCountStatsSplit?.buckets).map(([time, value]) => ({
time: +time,

View file

@ -14,11 +14,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DataView } from '@kbn/data-views-plugin/common';
import {
LogRateAnalysisContent,
LOG_RATE_ANALYSIS_TYPE,
type LogRateAnalysisResultsData,
type LogRateAnalysisType,
} from '@kbn/aiops-plugin/public';
} from '@kbn/aiops-utils/log_rate_analysis_type';
import { LogRateAnalysisContent, type LogRateAnalysisResultsData } from '@kbn/aiops-plugin/public';
import { Rule } from '@kbn/alerting-plugin/common';
import { TopAlert } from '@kbn/observability-plugin/public';
import {
@ -33,7 +32,6 @@ import { ALERT_END } from '@kbn/rule-data-utils';
import { Color, colorTransformer } from '../../../../../../common/color_palette';
import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana';
import {
Comparator,
CountRuleParams,
isRatioRuleParams,
PartialRuleParams,
@ -60,11 +58,9 @@ export const LogRateAnalysis: FC<AlertDetailsLogRateAnalysisSectionProps> = ({ r
const [dataView, setDataView] = useState<DataView | undefined>();
const [esSearchQuery, setEsSearchQuery] = useState<QueryDslQueryContainer | undefined>();
const [logRateAnalysisParams, setLogRateAnalysisParams] = useState<
{ significantFieldValues: SignificantFieldValue[] } | undefined
| { logRateAnalysisType: LogRateAnalysisType; significantFieldValues: SignificantFieldValue[] }
| undefined
>();
const [logRateAnalysisType, setLogRateAnalysisType] = useState<LogRateAnalysisType | undefined>(
undefined
);
const validatedParams = useMemo(() => decodeOrThrow(ruleParamsRT)(rule.params), [rule]);
@ -95,19 +91,6 @@ export const LogRateAnalysis: FC<AlertDetailsLogRateAnalysisSectionProps> = ({ r
if (!isRatioRuleParams(validatedParams)) {
getDataView();
switch (validatedParams.count.comparator) {
case Comparator.GT:
case Comparator.GT_OR_EQ:
setLogRateAnalysisType(LOG_RATE_ANALYSIS_TYPE.SPIKE);
break;
case Comparator.LT:
case Comparator.LT_OR_EQ:
setLogRateAnalysisType(LOG_RATE_ANALYSIS_TYPE.DIP);
break;
default:
setLogRateAnalysisType(undefined);
}
}
}, [validatedParams, alert, dataViews, logsShared]);
@ -188,7 +171,13 @@ export const LogRateAnalysis: FC<AlertDetailsLogRateAnalysisSectionProps> = ({ r
['pValue', 'docCount'],
['asc', 'asc']
).slice(0, 50);
setLogRateAnalysisParams(significantFieldValues ? { significantFieldValues } : undefined);
const logRateAnalysisType = analysisResults?.analysisType;
setLogRateAnalysisParams(
significantFieldValues && logRateAnalysisType
? { logRateAnalysisType, significantFieldValues }
: undefined
);
};
const aiAssistant = useObservabilityAIAssistant();
@ -201,6 +190,8 @@ export const LogRateAnalysis: FC<AlertDetailsLogRateAnalysisSectionProps> = ({ r
return undefined;
}
const { logRateAnalysisType } = logRateAnalysisParams;
const header = 'Field name,Field value,Doc count,p-value';
const rows = logRateAnalysisParams.significantFieldValues
.map((item) => Object.values(item).join(','))
@ -210,27 +201,34 @@ export const LogRateAnalysis: FC<AlertDetailsLogRateAnalysisSectionProps> = ({ r
"Log Rate Analysis" is an AIOps feature that uses advanced statistical methods to identify reasons for increases and decreases in log rates. It makes it easy to find and investigate causes of unusual spikes or dips by using the analysis workflow view.
You are using "Log Rate Analysis" and ran the statistical analysis on the log messages which occured during the alert.
You received the following analysis results from "Log Rate Analysis" which list statistically significant co-occuring field/value combinations sorted from most significant (lower p-values) to least significant (higher p-values) that ${
logRateAnalysisType === 'spike'
logRateAnalysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE
? 'contribute to the log rate spike'
: 'are less or not present in the log rate dip'
}:
${
logRateAnalysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE
? 'The median log rate in the selected deviation time range is higher than the baseline. Therefore, the results shows statistically significant items within the deviation time range that are contributors to the spike. The "doc count" column refers to the amount of documents in the deviation time range.'
: 'The median log rate in the selected deviation time range is lower than the baseline. Therefore, the analysis results table shows statistically significant items within the baseline time range that are less in number or missing within the deviation time range. The "doc count" column refers to the amount of documents in the baseline time range.'
}
${header}
${rows}
Based on the above analysis results and your observability expert knowledge, output the following:
Analyse the type of these logs and explain their usual purpose (1 paragraph).
${
logRateAnalysisType === 'spike'
logRateAnalysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE
? 'Based on the type of these logs do a root cause analysis on why the field and value combinations from the analysis results are causing this log rate spike (2 parapraphs)'
: 'Based on the type of these logs do a concise analysis why the statistically significant field and value combinations are less present or missing from the log rate dip with concrete examples based on the analysis results data. Do not guess, just output what you are sure of (2 paragraphs)'
: 'Based on the type of these logs explain why the statistically significant field and value combinations are less in number or missing from the log rate dip with concrete examples based on the analysis results data which contains items that are present in the baseline time range and are missing or less in number in the deviation time range (2 paragraphs)'
}.
${
logRateAnalysisType === 'spike'
logRateAnalysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE
? 'Recommend concrete remediations to resolve the root cause (3 bullet points).'
: ''
}
Do not repeat the given instructions in your output.`;
Do not mention indidivual p-values from the analysis results. Do not guess, just say what you are sure of. Do not repeat the given instructions in your output.`;
const now = new Date().toString();
@ -251,7 +249,7 @@ export const LogRateAnalysis: FC<AlertDetailsLogRateAnalysisSectionProps> = ({ r
},
},
];
}, [logRateAnalysisParams, logRateAnalysisType]);
}, [logRateAnalysisParams]);
if (!dataView || !esSearchQuery) return null;
@ -271,7 +269,6 @@ export const LogRateAnalysis: FC<AlertDetailsLogRateAnalysisSectionProps> = ({ r
<EuiFlexItem>
<LogRateAnalysisContent
dataView={dataView}
analysisType={logRateAnalysisType}
timeRange={timeRange}
esSearchQuery={esSearchQuery}
initialAnalysisStart={initialAnalysisStart}

View file

@ -68,6 +68,7 @@
"@kbn/core-http-server",
"@kbn/logs-shared-plugin",
"@kbn/licensing-plugin",
"@kbn/aiops-utils",
],
"exclude": ["target/**/*"]
}

View file

@ -147,7 +147,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await aiops.logRateAnalysisPage.clickRerunAnalysisButton(true);
}
await aiops.logRateAnalysisPage.assertAnalysisComplete();
await aiops.logRateAnalysisPage.assertAnalysisComplete(testData.analysisType);
// The group switch should be disabled by default
await aiops.logRateAnalysisPage.assertLogRateAnalysisResultsGroupSwitchExists(false);

View file

@ -5,10 +5,13 @@
* 2.0.
*/
import { LOG_RATE_ANALYSIS_TYPE, type LogRateAnalysisType } from '@kbn/aiops-utils';
import type { TestData } from './types';
export const kibanaLogsDataViewTestData: TestData = {
suiteTitle: 'kibana sample data logs',
analysisType: LOG_RATE_ANALYSIS_TYPE.SPIKE,
dataGenerator: 'kibana_sample_data_logs',
isSavedSearch: false,
sourceIndexOrSavedSearch: 'kibana_sample_data_logs',
@ -114,6 +117,7 @@ export const kibanaLogsDataViewTestData: TestData = {
export const farequoteDataViewTestData: TestData = {
suiteTitle: 'farequote with spike',
analysisType: LOG_RATE_ANALYSIS_TYPE.SPIKE,
dataGenerator: 'farequote_with_spike',
isSavedSearch: false,
sourceIndexOrSavedSearch: 'ft_farequote',
@ -131,6 +135,7 @@ export const farequoteDataViewTestData: TestData = {
export const farequoteDataViewTestDataWithQuery: TestData = {
suiteTitle: 'farequote with spike',
analysisType: LOG_RATE_ANALYSIS_TYPE.SPIKE,
dataGenerator: 'farequote_with_spike',
isSavedSearch: false,
sourceIndexOrSavedSearch: 'ft_farequote',
@ -171,11 +176,12 @@ const DAY_MS = 86400000;
const DEVIATION_TS = REFERENCE_TS - DAY_MS * 2;
const BASELINE_TS = DEVIATION_TS - DAY_MS * 1;
export const artificialLogDataViewTestData: TestData = {
suiteTitle: 'artificial logs with spike',
dataGenerator: 'artificial_logs_with_spike',
const getArtificialLogDataViewTestData = (analysisType: LogRateAnalysisType): TestData => ({
suiteTitle: `artificial logs with ${analysisType}`,
analysisType,
dataGenerator: `artificial_logs_with_${analysisType}`,
isSavedSearch: false,
sourceIndexOrSavedSearch: 'artificial_logs_with_spike',
sourceIndexOrSavedSearch: `artificial_logs_with_${analysisType}`,
brushBaselineTargetTimestamp: BASELINE_TS + DAY_MS / 2,
brushDeviationTargetTimestamp: DEVIATION_TS + DAY_MS / 2,
brushIntervalFactor: 10,
@ -224,11 +230,12 @@ export const artificialLogDataViewTestData: TestData = {
],
fieldSelectorPopover: ['response_code', 'url', 'user'],
},
};
});
export const logRateAnalysisTestData: TestData[] = [
kibanaLogsDataViewTestData,
farequoteDataViewTestData,
farequoteDataViewTestDataWithQuery,
artificialLogDataViewTestData,
getArtificialLogDataViewTestData(LOG_RATE_ANALYSIS_TYPE.SPIKE),
getArtificialLogDataViewTestData(LOG_RATE_ANALYSIS_TYPE.DIP),
];

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { LogRateAnalysisType } from '@kbn/aiops-utils';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
interface TestDataTableActionLogPatternAnalysis {
@ -44,6 +45,7 @@ interface TestDataExpectedWithoutSampleProbability {
export interface TestData {
suiteTitle: string;
analysisType: LogRateAnalysisType;
dataGenerator: string;
isSavedSearch?: boolean;
sourceIndexOrSavedSearch: string;

View file

@ -7,6 +7,8 @@
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { LOG_RATE_ANALYSIS_TYPE } from '@kbn/aiops-utils';
import { FtrProviderContext } from '../../ftr_provider_context';
export interface GeneratedDoc {
@ -24,7 +26,7 @@ const DAY_MS = 86400000;
const DEVIATION_TS = REFERENCE_TS - DAY_MS * 2;
const BASELINE_TS = DEVIATION_TS - DAY_MS * 1;
function getArtificialLogsWithSpike(index: string) {
function getArtificialLogsWithDeviation(index: string, deviationType: string) {
const bulkBody: estypes.BulkRequest<GeneratedDoc, GeneratedDoc>['body'] = [];
const action = { index: { _index: index } };
let tsOffset = 0;
@ -44,7 +46,7 @@ function getArtificialLogsWithSpike(index: string) {
) {
tsOffset = 0;
[...Array(100)].forEach(() => {
tsOffset += DAY_MS / 100;
tsOffset += Math.round(DAY_MS / 100);
const doc: GeneratedDoc = {
user,
response_code: responseCode,
@ -74,14 +76,16 @@ function getArtificialLogsWithSpike(index: string) {
['login.php', 'user.php', 'home.php'].forEach((url) => {
tsOffset = 0;
[...Array(docsPerUrl1[url])].forEach(() => {
tsOffset += DAY_MS / docsPerUrl1[url];
tsOffset += Math.round(DAY_MS / docsPerUrl1[url]);
bulkBody.push(action);
bulkBody.push({
user: 'Peter',
response_code: responseCode,
url,
version: 'v1.0.0',
'@timestamp': DEVIATION_TS + tsOffset,
'@timestamp':
(deviationType === LOG_RATE_ANALYSIS_TYPE.SPIKE ? DEVIATION_TS : BASELINE_TS) +
tsOffset,
should_ignore_this_field: 'should_ignore_this_field',
});
});
@ -97,14 +101,16 @@ function getArtificialLogsWithSpike(index: string) {
['login.php', 'home.php'].forEach((url) => {
tsOffset = 0;
[...Array(docsPerUrl2[url] + userIndex)].forEach(() => {
tsOffset += DAY_MS / docsPerUrl2[url];
tsOffset += Math.round(DAY_MS / docsPerUrl2[url]);
bulkBody.push(action);
bulkBody.push({
user,
response_code: '500',
url,
version: 'v1.0.0',
'@timestamp': DEVIATION_TS + tsOffset,
'@timestamp':
(deviationType === LOG_RATE_ANALYSIS_TYPE.SPIKE ? DEVIATION_TS : BASELINE_TS) +
tsOffset,
should_ignore_this_field: 'should_ignore_this_field',
});
});
@ -159,17 +165,18 @@ export function LogRateAnalysisDataGeneratorProvider({ getService }: FtrProvider
break;
case 'artificial_logs_with_spike':
case 'artificial_logs_with_dip':
try {
await es.indices.delete({
index: 'artificial_logs_with_spike',
index: dataGenerator,
});
} catch (e) {
log.info(`Could not delete index 'artificial_logs_with_spike' in before() callback`);
log.info(`Could not delete index '${dataGenerator}' in before() callback`);
}
// Create index with mapping
await es.indices.create({
index: 'artificial_logs_with_spike',
index: dataGenerator,
mappings: {
properties: {
user: { type: 'keyword' },
@ -184,7 +191,10 @@ export function LogRateAnalysisDataGeneratorProvider({ getService }: FtrProvider
await es.bulk({
refresh: 'wait_for',
body: getArtificialLogsWithSpike('artificial_logs_with_spike'),
body: getArtificialLogsWithDeviation(
dataGenerator,
dataGenerator.split('_').pop() ?? LOG_RATE_ANALYSIS_TYPE.SPIKE
),
});
break;
@ -204,12 +214,13 @@ export function LogRateAnalysisDataGeneratorProvider({ getService }: FtrProvider
break;
case 'artificial_logs_with_spike':
case 'artificial_logs_with_dip':
try {
await es.indices.delete({
index: 'artificial_logs_with_spike',
index: dataGenerator,
});
} catch (e) {
log.error(`Error deleting index 'artificial_logs_with_spike' in after() callback`);
log.error(`Error deleting index '${dataGenerator}' in after() callback`);
}
break;

View file

@ -7,6 +7,8 @@
import expect from '@kbn/expect';
import type { LogRateAnalysisType } from '@kbn/aiops-utils';
import type { FtrProviderContext } from '../../ftr_provider_context';
export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrProviderContext) {
@ -239,11 +241,17 @@ export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrPr
});
},
async assertAnalysisComplete() {
async assertAnalysisComplete(analisysType: LogRateAnalysisType) {
await retry.tryForTime(30 * 1000, async () => {
await testSubjects.existOrFail('aiopsAnalysisComplete');
const currentProgressTitle = await testSubjects.getVisibleText('aiopsAnalysisComplete');
expect(currentProgressTitle).to.be('Analysis complete');
await testSubjects.existOrFail('aiopsAnalysisTypeCalloutTitle');
const currentAnalysisTypeCalloutTitle = await testSubjects.getVisibleText(
'aiopsAnalysisTypeCalloutTitle'
);
expect(currentAnalysisTypeCalloutTitle).to.be(`Analysis type: Log rate ${analisysType}`);
});
},

View file

@ -137,6 +137,7 @@
"@kbn/uptime-plugin",
"@kbn/ml-category-validator",
"@kbn/observability-ai-assistant-plugin",
"@kbn/stack-connectors-plugin"
"@kbn/stack-connectors-plugin",
"@kbn/aiops-utils"
]
}