mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[ML] Log pattern analysis UI (#139005)
* [ML] Log caegorization UI POC * improvements * code structure changes * further refactoring * fixes after merge with main * fixes after merging with main * adding table pagination * catching category request errors * small refactor * fixes after merge with main * further fixes are merge with main * showing errors in toast * updating breadcrumbs * translations and removing unused files * changing case of side nav items * translations * updating actions * fixing unused variable * translations * adding comment * small changes based on review * improving search request type * capitalizing discover * changes based on review * fixing chart tooltip * fixing examples from fields containing dots * fixing breadcrumbs * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
78f00e12af
commit
0bfc3fd41a
28 changed files with 1362 additions and 27 deletions
|
@ -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 { categorizeSchema } from './schema';
|
||||
export type { CategorizeSchema } from './schema';
|
20
x-pack/plugins/aiops/common/api/log_categorization/schema.ts
Normal file
20
x-pack/plugins/aiops/common/api/log_categorization/schema.ts
Normal 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.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const categorizeSchema = schema.object({
|
||||
index: schema.string(),
|
||||
field: schema.string(),
|
||||
timeField: schema.string(),
|
||||
to: schema.number(),
|
||||
from: schema.number(),
|
||||
query: schema.any(),
|
||||
intervalMs: schema.maybe(schema.number()),
|
||||
});
|
||||
|
||||
export type CategorizeSchema = TypeOf<typeof categorizeSchema>;
|
|
@ -185,3 +185,9 @@ export const extractErrorProperties = (error: ErrorType): AiOpsErrorObject => {
|
|||
message: '',
|
||||
};
|
||||
};
|
||||
|
||||
export const extractErrorMessage = (error: ErrorType): string => {
|
||||
// extract only the error message within the response error coming from Kibana, Elasticsearch, and our own ML messages
|
||||
const errorObj = extractErrorProperties(error);
|
||||
return errorObj.message;
|
||||
};
|
||||
|
|
|
@ -27,7 +27,6 @@ import { DualBrush, DualBrushAnnotation } from '@kbn/aiops-components';
|
|||
import { getSnappedWindowParameters, getWindowParameters } from '@kbn/aiops-utils';
|
||||
import type { WindowParameters } from '@kbn/aiops-utils';
|
||||
import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common';
|
||||
import type { ChangePoint } from '@kbn/ml-agg-utils';
|
||||
|
||||
import { useAiopsAppContext } from '../../../hooks/use_aiops_app_context';
|
||||
|
||||
|
@ -48,14 +47,14 @@ export interface DocumentCountChartPoint {
|
|||
}
|
||||
|
||||
interface DocumentCountChartProps {
|
||||
brushSelectionUpdateHandler: (d: WindowParameters, force: boolean) => void;
|
||||
brushSelectionUpdateHandler?: (d: WindowParameters, force: boolean) => void;
|
||||
width?: number;
|
||||
chartPoints: DocumentCountChartPoint[];
|
||||
chartPointsSplit?: DocumentCountChartPoint[];
|
||||
timeRangeEarliest: number;
|
||||
timeRangeLatest: number;
|
||||
interval: number;
|
||||
changePoint?: ChangePoint;
|
||||
chartPointsSplitLabel: string;
|
||||
isBrushCleared: boolean;
|
||||
}
|
||||
|
||||
|
@ -100,7 +99,7 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
|
|||
timeRangeEarliest,
|
||||
timeRangeLatest,
|
||||
interval,
|
||||
changePoint,
|
||||
chartPointsSplitLabel,
|
||||
isBrushCleared,
|
||||
}) => {
|
||||
const { data, uiSettings, fieldFormats, charts } = useAiopsAppContext();
|
||||
|
@ -125,8 +124,6 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
|
|||
}
|
||||
);
|
||||
|
||||
const splitSeriesName = `${changePoint?.fieldName}:${changePoint?.fieldValue}`;
|
||||
|
||||
// TODO Let user choose between ZOOM and BRUSH mode.
|
||||
const [viewMode] = useState<VIEW_MODE>(VIEW_MODE.BRUSH);
|
||||
|
||||
|
@ -198,6 +195,9 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
|
|||
};
|
||||
|
||||
const onElementClick: ElementClickListener = ([elementData]) => {
|
||||
if (brushSelectionUpdateHandler === undefined) {
|
||||
return;
|
||||
}
|
||||
const startRange = (elementData as XYChartElementEvent)[0].x;
|
||||
|
||||
const range = {
|
||||
|
@ -245,6 +245,9 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
|
|||
}, [isBrushCleared, originalWindowParameters]);
|
||||
|
||||
function onWindowParametersChange(wp: WindowParameters, wpPx: WindowParameters) {
|
||||
if (brushSelectionUpdateHandler === undefined) {
|
||||
return;
|
||||
}
|
||||
setWindowParameters(wp);
|
||||
setWindowParametersAsPixels(wpPx);
|
||||
brushSelectionUpdateHandler(wp, false);
|
||||
|
@ -360,7 +363,7 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
|
|||
{chartPointsSplit && (
|
||||
<HistogramBarSeries
|
||||
id={`${SPEC_ID}_split`}
|
||||
name={splitSeriesName}
|
||||
name={chartPointsSplitLabel}
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor="time"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, FC } from 'react';
|
||||
import React, { useEffect, useState, FC, useMemo } from 'react';
|
||||
import { min, max } from 'd3-array';
|
||||
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
@ -53,6 +53,10 @@ export const DocumentCountContent: FC<DocumentCountContentProps> = ({
|
|||
const bucketTimestamps = Object.keys(documentCountStats?.buckets ?? {}).map((time) => +time);
|
||||
const timeRangeEarliest = min(bucketTimestamps);
|
||||
const timeRangeLatest = max(bucketTimestamps);
|
||||
const chartPointsSplitLabel = useMemo(
|
||||
() => `${changePoint?.fieldName}:${changePoint?.fieldValue}`,
|
||||
[changePoint]
|
||||
);
|
||||
|
||||
if (
|
||||
documentCountStats === undefined ||
|
||||
|
@ -118,7 +122,7 @@ export const DocumentCountContent: FC<DocumentCountContentProps> = ({
|
|||
timeRangeEarliest={timeRangeEarliest}
|
||||
timeRangeLatest={timeRangeLatest}
|
||||
interval={documentCountStats.interval}
|
||||
changePoint={changePoint}
|
||||
chartPointsSplitLabel={chartPointsSplitLabel}
|
||||
isBrushCleared={isBrushCleared}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,270 @@
|
|||
/*
|
||||
* 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, { FC, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiCode,
|
||||
EuiText,
|
||||
EuiTableSelectionType,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { useDiscoverLinks } from '../use_discover_links';
|
||||
import { MiniHistogram } from '../../mini_histogram';
|
||||
import { useEuiTheme } from '../../../hooks/use_eui_theme';
|
||||
import type { AiOpsIndexBasedAppState } from '../../explain_log_rate_spikes/explain_log_rate_spikes_app_state';
|
||||
import type { EventRate, Category, SparkLinesPerCategory } from '../use_categorize_request';
|
||||
import { useTableState } from './use_table_state';
|
||||
|
||||
const QUERY_MODE = {
|
||||
INCLUDE: 'should',
|
||||
EXCLUDE: 'must_not',
|
||||
} as const;
|
||||
export type QueryMode = typeof QUERY_MODE[keyof typeof QUERY_MODE];
|
||||
|
||||
interface Props {
|
||||
categories: Category[];
|
||||
sparkLines: SparkLinesPerCategory;
|
||||
eventRate: EventRate;
|
||||
dataViewId: string;
|
||||
selectedField: string | undefined;
|
||||
timefilter: TimefilterContract;
|
||||
aiopsListState: Required<AiOpsIndexBasedAppState>;
|
||||
pinnedCategory: Category | null;
|
||||
setPinnedCategory: (category: Category | null) => void;
|
||||
selectedCategory: Category | null;
|
||||
setSelectedCategory: (category: Category | null) => void;
|
||||
}
|
||||
|
||||
export const CategoryTable: FC<Props> = ({
|
||||
categories,
|
||||
sparkLines,
|
||||
eventRate,
|
||||
dataViewId,
|
||||
selectedField,
|
||||
timefilter,
|
||||
aiopsListState,
|
||||
pinnedCategory,
|
||||
setPinnedCategory,
|
||||
selectedCategory,
|
||||
setSelectedCategory,
|
||||
}) => {
|
||||
const euiTheme = useEuiTheme();
|
||||
const { openInDiscoverWithFilter } = useDiscoverLinks();
|
||||
const [selectedCategories, setSelectedCategories] = useState<Category[]>([]);
|
||||
const { onTableChange, pagination, sorting } = useTableState<Category>(categories ?? [], 'key');
|
||||
|
||||
const openInDiscover = (mode: QueryMode, category?: Category) => {
|
||||
const timefilterActiveBounds = timefilter.getActiveBounds();
|
||||
if (timefilterActiveBounds === undefined || selectedField === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
openInDiscoverWithFilter(
|
||||
dataViewId,
|
||||
selectedField,
|
||||
selectedCategories,
|
||||
aiopsListState,
|
||||
timefilterActiveBounds,
|
||||
mode,
|
||||
category
|
||||
);
|
||||
};
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<Category>> = [
|
||||
{
|
||||
field: 'count',
|
||||
name: i18n.translate('xpack.aiops.logCategorization.column.count', {
|
||||
defaultMessage: 'Count',
|
||||
}),
|
||||
sortable: true,
|
||||
width: '80px',
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
name: i18n.translate('xpack.aiops.logCategorization.column.logRate', {
|
||||
defaultMessage: 'Log rate',
|
||||
}),
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (_, { key }) => {
|
||||
const sparkLine = sparkLines[key];
|
||||
if (sparkLine === undefined) {
|
||||
return null;
|
||||
}
|
||||
const histogram = eventRate.map((e) => ({
|
||||
doc_count_overall: e.docCount,
|
||||
doc_count_change_point: sparkLine[e.key],
|
||||
key: e.key,
|
||||
key_as_string: `${e.key}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<MiniHistogram
|
||||
chartData={histogram}
|
||||
isLoading={categories === null && histogram === undefined}
|
||||
label={''}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'examples',
|
||||
name: i18n.translate('xpack.aiops.logCategorization.column.examples', {
|
||||
defaultMessage: 'Examples',
|
||||
}),
|
||||
sortable: true,
|
||||
style: { display: 'block' },
|
||||
render: (examples: string[]) => (
|
||||
<div style={{ display: 'block' }}>
|
||||
{examples.map((e) => (
|
||||
<>
|
||||
<EuiText size="s">
|
||||
<EuiCode language="log" transparentBackground>
|
||||
{e}
|
||||
</EuiCode>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
width: '60px',
|
||||
actions: [
|
||||
{
|
||||
name: i18n.translate('xpack.aiops.logCategorization.showInDiscover', {
|
||||
defaultMessage: 'Show these in Discover',
|
||||
}),
|
||||
description: i18n.translate('xpack.aiops.logCategorization.showInDiscover', {
|
||||
defaultMessage: 'Show these in Discover',
|
||||
}),
|
||||
icon: 'discoverApp',
|
||||
type: 'icon',
|
||||
onClick: (category) => openInDiscover(QUERY_MODE.INCLUDE, category),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.aiops.logCategorization.filterOutInDiscover', {
|
||||
defaultMessage: 'Filter out in Discover',
|
||||
}),
|
||||
description: i18n.translate('xpack.aiops.logCategorization.filterOutInDiscover', {
|
||||
defaultMessage: 'Filter out in Discover',
|
||||
}),
|
||||
icon: 'filter',
|
||||
type: 'icon',
|
||||
onClick: (category) => openInDiscover(QUERY_MODE.EXCLUDE, category),
|
||||
},
|
||||
// Disabled for now
|
||||
// {
|
||||
// name: i18n.translate('xpack.aiops.logCategorization.openInDataViz', {
|
||||
// defaultMessage: 'Open in data visualizer',
|
||||
// }),
|
||||
// icon: 'stats',
|
||||
// type: 'icon',
|
||||
// onClick: () => {},
|
||||
// },
|
||||
],
|
||||
},
|
||||
] as Array<EuiBasicTableColumn<Category>>;
|
||||
|
||||
const selectionValue: EuiTableSelectionType<Category> | undefined = {
|
||||
selectable: () => true,
|
||||
onSelectionChange: (selectedItems) => setSelectedCategories(selectedItems),
|
||||
};
|
||||
|
||||
const getRowStyle = (category: Category) => {
|
||||
if (
|
||||
pinnedCategory &&
|
||||
pinnedCategory.key === category.key &&
|
||||
pinnedCategory.key === category.key
|
||||
) {
|
||||
return {
|
||||
backgroundColor: 'rgb(227,240,249,0.37)',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
selectedCategory &&
|
||||
selectedCategory.key === category.key &&
|
||||
selectedCategory.key === category.key
|
||||
) {
|
||||
return {
|
||||
backgroundColor: euiTheme.euiColorLightestShade,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundColor: 'white',
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedCategories.length > 0 ? (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton size="s" onClick={() => openInDiscover(QUERY_MODE.INCLUDE)}>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.showInDiscover"
|
||||
defaultMessage="Show these in Discover"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton size="s" onClick={() => openInDiscover(QUERY_MODE.EXCLUDE)}>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.filterOutInDiscover"
|
||||
defaultMessage="Filter out in Discover"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
) : null}
|
||||
<EuiInMemoryTable<Category>
|
||||
compressed
|
||||
items={categories}
|
||||
columns={columns}
|
||||
isSelectable={true}
|
||||
selection={selectionValue}
|
||||
itemId="key"
|
||||
onTableChange={onTableChange}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
rowProps={(category) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
if (category.key === pinnedCategory?.key) {
|
||||
setPinnedCategory(null);
|
||||
} else {
|
||||
setPinnedCategory(category);
|
||||
}
|
||||
},
|
||||
onMouseEnter: () => {
|
||||
setSelectedCategory(category);
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setSelectedCategory(null);
|
||||
},
|
||||
style: getRowStyle(category),
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 { CategoryTable } from './category_table';
|
||||
export type { QueryMode } from './category_table';
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { useState } from 'react';
|
||||
import { EuiInMemoryTable, Direction, Pagination } from '@elastic/eui';
|
||||
|
||||
export function useTableState<T>(items: T[], initialSortField: string) {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [sortField, setSortField] = useState<string>(initialSortField);
|
||||
const [sortDirection, setSortDirection] = useState<Direction>('asc');
|
||||
|
||||
const onTableChange: EuiInMemoryTable<T>['onTableChange'] = ({
|
||||
page = { index: 0, size: 10 },
|
||||
sort = { field: sortField, direction: sortDirection },
|
||||
}) => {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
|
||||
const { field, direction } = sort;
|
||||
setSortField(field as string);
|
||||
setSortDirection(direction as Direction);
|
||||
};
|
||||
|
||||
const pagination: Pagination = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: (items ?? []).length,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
showPerPageOptions: true,
|
||||
};
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
return { onTableChange, pagination, sorting, setPageIndex };
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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, { FC, useMemo } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DocumentCountChart as DocumentCountChartRoot } from '../document_count_content/document_count_chart';
|
||||
import { TotalCountHeader } from '../document_count_content/total_count_header';
|
||||
import type { Category, SparkLinesPerCategory } from './use_categorize_request';
|
||||
import type { EventRate } from './use_categorize_request';
|
||||
import { DocumentCountStats } from '../../get_document_stats';
|
||||
|
||||
interface Props {
|
||||
totalCount: number;
|
||||
pinnedCategory: Category | null;
|
||||
selectedCategory: Category | null;
|
||||
eventRate: EventRate;
|
||||
sparkLines: SparkLinesPerCategory;
|
||||
documentCountStats?: DocumentCountStats;
|
||||
}
|
||||
|
||||
export const DocumentCountChart: FC<Props> = ({
|
||||
eventRate,
|
||||
sparkLines,
|
||||
totalCount,
|
||||
pinnedCategory,
|
||||
selectedCategory,
|
||||
documentCountStats,
|
||||
}) => {
|
||||
const chartPointsSplitLabel = i18n.translate(
|
||||
'xpack.aiops.logCategorization.chartPointsSplitLabel',
|
||||
{
|
||||
defaultMessage: 'Selected category',
|
||||
}
|
||||
);
|
||||
const chartPoints = useMemo(() => {
|
||||
const category = selectedCategory ?? pinnedCategory ?? null;
|
||||
return eventRate.map(({ key, docCount }) => {
|
||||
let value = docCount;
|
||||
if (category && sparkLines[category.key] && sparkLines[category.key][key]) {
|
||||
value -= sparkLines[category.key][key];
|
||||
}
|
||||
return { time: key, value };
|
||||
});
|
||||
}, [eventRate, pinnedCategory, selectedCategory, sparkLines]);
|
||||
|
||||
const chartPointsSplit = useMemo(() => {
|
||||
const category = selectedCategory ?? pinnedCategory ?? null;
|
||||
return category !== null
|
||||
? eventRate.map(({ key }) => {
|
||||
const value =
|
||||
sparkLines && sparkLines[category.key] && sparkLines[category.key][key]
|
||||
? sparkLines[category.key][key]
|
||||
: 0;
|
||||
return { time: key, value };
|
||||
})
|
||||
: undefined;
|
||||
}, [eventRate, pinnedCategory, selectedCategory, sparkLines]);
|
||||
|
||||
if (documentCountStats?.interval === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TotalCountHeader totalCount={totalCount} />
|
||||
<DocumentCountChartRoot
|
||||
chartPoints={chartPoints}
|
||||
chartPointsSplit={chartPointsSplit}
|
||||
timeRangeEarliest={eventRate[0].key}
|
||||
timeRangeLatest={eventRate[eventRate.length - 1].key}
|
||||
interval={documentCountStats.interval}
|
||||
chartPointsSplitLabel={chartPointsSplitLabel}
|
||||
isBrushCleared={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 type { LogCategorizationAppStateProps } from './log_categorization_app_state';
|
||||
import { LogCategorizationAppState } from './log_categorization_app_state';
|
||||
|
||||
// required for dynamic import using React.lazy()
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default LogCategorizationAppState;
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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, { FC } from 'react';
|
||||
import type { SavedSearch } from '@kbn/discover-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { LogCategorizationPage } from './log_categorization_page';
|
||||
import { SavedSearchSavedObject } from '../../application/utils/search_utils';
|
||||
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
|
||||
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
|
||||
export interface LogCategorizationAppStateProps {
|
||||
dataView: DataView;
|
||||
savedSearch: SavedSearch | SavedSearchSavedObject | null;
|
||||
appDependencies: AiopsAppDependencies;
|
||||
}
|
||||
|
||||
export const LogCategorizationAppState: FC<LogCategorizationAppStateProps> = ({
|
||||
dataView,
|
||||
savedSearch,
|
||||
appDependencies,
|
||||
}) => {
|
||||
return (
|
||||
<AiopsAppContext.Provider value={appDependencies}>
|
||||
<LogCategorizationPage dataView={dataView} savedSearch={savedSearch} />
|
||||
</AiopsAppContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,333 @@
|
|||
/*
|
||||
* 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, { FC, useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import type { SavedSearch } from '@kbn/discover-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPageBody,
|
||||
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection,
|
||||
EuiTitle,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFormRow,
|
||||
EuiLoadingContent,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FullTimeRangeSelector } from '../full_time_range_selector';
|
||||
import { DatePickerWrapper } from '../date_picker_wrapper';
|
||||
import { useData } from '../../hooks/use_data';
|
||||
import { SearchPanel } from '../search_panel';
|
||||
import type {
|
||||
SearchQueryLanguage,
|
||||
SavedSearchSavedObject,
|
||||
} from '../../application/utils/search_utils';
|
||||
import { useUrlState } from '../../hooks/use_url_state';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import { restorableDefaults } from '../explain_log_rate_spikes/explain_log_rate_spikes_app_state';
|
||||
import { useCategorizeRequest } from './use_categorize_request';
|
||||
import type { EventRate, Category, SparkLinesPerCategory } from './use_categorize_request';
|
||||
import { CategoryTable } from './category_table';
|
||||
import { DocumentCountChart } from './document_count_chart';
|
||||
|
||||
export interface LogCategorizationPageProps {
|
||||
dataView: DataView;
|
||||
savedSearch: SavedSearch | SavedSearchSavedObject | null;
|
||||
}
|
||||
|
||||
const BAR_TARGET = 20;
|
||||
|
||||
export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({
|
||||
dataView,
|
||||
savedSearch,
|
||||
}) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useAiopsAppContext();
|
||||
|
||||
const { runCategorizeRequest, cancelRequest } = useCategorizeRequest();
|
||||
const [aiopsListState, setAiopsListState] = useState(restorableDefaults);
|
||||
const [globalState, setGlobalState] = useUrlState('_g');
|
||||
const [selectedField, setSelectedField] = useState<string | undefined>();
|
||||
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null);
|
||||
const [categories, setCategories] = useState<Category[] | null>(null);
|
||||
const [currentSavedSearch, setCurrentSavedSearch] = useState(savedSearch);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [eventRate, setEventRate] = useState<EventRate>([]);
|
||||
const [pinnedCategory, setPinnedCategory] = useState<Category | null>(null);
|
||||
const [sparkLines, setSparkLines] = useState<SparkLinesPerCategory>({});
|
||||
|
||||
useEffect(
|
||||
function cancelRequestOnLeave() {
|
||||
return () => {
|
||||
cancelRequest();
|
||||
};
|
||||
},
|
||||
[cancelRequest]
|
||||
);
|
||||
|
||||
const setSearchParams = useCallback(
|
||||
(searchParams: {
|
||||
searchQuery: Query['query'];
|
||||
searchString: Query['query'];
|
||||
queryLanguage: SearchQueryLanguage;
|
||||
filters: Filter[];
|
||||
}) => {
|
||||
// When the user loads saved search and then clear or modify the query
|
||||
// we should remove the saved search and replace it with the index pattern id
|
||||
if (currentSavedSearch !== null) {
|
||||
setCurrentSavedSearch(null);
|
||||
}
|
||||
|
||||
setAiopsListState({
|
||||
...aiopsListState,
|
||||
searchQuery: searchParams.searchQuery,
|
||||
searchString: searchParams.searchString,
|
||||
searchQueryLanguage: searchParams.queryLanguage,
|
||||
filters: searchParams.filters,
|
||||
});
|
||||
},
|
||||
[currentSavedSearch, aiopsListState, setAiopsListState]
|
||||
);
|
||||
|
||||
const {
|
||||
documentStats,
|
||||
timefilter,
|
||||
earliest,
|
||||
latest,
|
||||
searchQueryLanguage,
|
||||
searchString,
|
||||
searchQuery,
|
||||
intervalMs,
|
||||
} = useData(
|
||||
{ currentDataView: dataView, currentSavedSearch },
|
||||
aiopsListState,
|
||||
setGlobalState,
|
||||
undefined,
|
||||
BAR_TARGET
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (globalState?.time !== undefined) {
|
||||
timefilter.setTime({
|
||||
from: globalState.time.from,
|
||||
to: globalState.time.to,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(globalState?.time), timefilter]);
|
||||
|
||||
const fields = useMemo(
|
||||
() =>
|
||||
dataView.fields
|
||||
.filter(
|
||||
({ displayName, esTypes, count }) =>
|
||||
esTypes && esTypes.includes('text') && !['_id', '_index'].includes(displayName)
|
||||
)
|
||||
.map(({ displayName }) => ({
|
||||
label: displayName,
|
||||
})),
|
||||
[dataView]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function setSingleFieldAsSelected() {
|
||||
if (fields.length === 1) {
|
||||
setSelectedField(fields[0].label);
|
||||
}
|
||||
},
|
||||
[fields]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (documentStats.documentCountStats?.buckets) {
|
||||
setEventRate(
|
||||
Object.entries(documentStats.documentCountStats.buckets).map(([key, docCount]) => ({
|
||||
key: +key,
|
||||
docCount,
|
||||
}))
|
||||
);
|
||||
setTotalCount(documentStats.totalCount);
|
||||
}
|
||||
}, [documentStats, earliest, latest, searchQueryLanguage, searchString, searchQuery]);
|
||||
|
||||
const loadCategories = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setCategories(null);
|
||||
const { title: index, timeFieldName: timeField } = dataView;
|
||||
|
||||
if (selectedField === undefined || timeField === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelRequest();
|
||||
|
||||
try {
|
||||
const resp = await runCategorizeRequest(
|
||||
index,
|
||||
selectedField,
|
||||
timeField,
|
||||
earliest,
|
||||
latest,
|
||||
searchQuery,
|
||||
intervalMs
|
||||
);
|
||||
|
||||
setCategories(resp.categories);
|
||||
setSparkLines(resp.sparkLinesPerCategory);
|
||||
} catch (error) {
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.aiops.logCategorization.errorLoadingCategories', {
|
||||
defaultMessage: 'Error loading categories',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, [
|
||||
selectedField,
|
||||
dataView,
|
||||
searchQuery,
|
||||
earliest,
|
||||
latest,
|
||||
runCategorizeRequest,
|
||||
cancelRequest,
|
||||
intervalMs,
|
||||
toasts,
|
||||
]);
|
||||
|
||||
const onFieldChange = (value: EuiComboBoxOptionOption[] | undefined) => {
|
||||
setSelectedField(value && value.length ? value[0].label : undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPageBody data-test-subj="aiopsExplainLogRateSpikesPage" paddingSize="none" panelled={false}>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiPageContentHeader className="aiopsPageHeader">
|
||||
<EuiPageContentHeaderSection>
|
||||
<div className="dataViewTitleHeader">
|
||||
<EuiTitle size="s">
|
||||
<h2>{dataView.getName()}</h2>
|
||||
</EuiTitle>
|
||||
</div>
|
||||
</EuiPageContentHeaderSection>
|
||||
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="s"
|
||||
data-test-subj="aiopsTimeRangeSelectorSection"
|
||||
>
|
||||
{dataView.timeFieldName !== undefined && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<FullTimeRangeSelector
|
||||
dataView={dataView}
|
||||
query={undefined}
|
||||
disabled={false}
|
||||
timefilter={timefilter}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentHeader>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<SearchPanel
|
||||
dataView={dataView}
|
||||
searchString={searchString ?? ''}
|
||||
searchQuery={searchQuery}
|
||||
searchQueryLanguage={searchQueryLanguage}
|
||||
setSearchParams={setSearchParams}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false} css={{ minWidth: '410px' }}>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.aiops.logCategorization.categoryFieldSelect', {
|
||||
defaultMessage: 'Category field',
|
||||
})}
|
||||
>
|
||||
<EuiComboBox
|
||||
isDisabled={loading === true}
|
||||
options={fields}
|
||||
onChange={onFieldChange}
|
||||
selectedOptions={selectedField === undefined ? undefined : [{ label: selectedField }]}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={{ marginTop: 'auto' }}>
|
||||
{loading === false ? (
|
||||
<EuiButton
|
||||
disabled={selectedField === undefined}
|
||||
onClick={() => {
|
||||
loadCategories();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.runButton"
|
||||
defaultMessage="Run categorization"
|
||||
/>
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiButton onClick={() => cancelRequest()}>Cancel</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={{ marginTop: 'auto' }} />
|
||||
<EuiFlexItem />
|
||||
</EuiFlexGroup>
|
||||
|
||||
{eventRate.length ? (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<DocumentCountChart
|
||||
eventRate={eventRate}
|
||||
pinnedCategory={pinnedCategory}
|
||||
selectedCategory={selectedCategory}
|
||||
sparkLines={sparkLines}
|
||||
totalCount={totalCount}
|
||||
documentCountStats={documentStats.documentCountStats}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
{loading === true ? <EuiLoadingContent lines={10} /> : null}
|
||||
{categories !== null ? (
|
||||
<CategoryTable
|
||||
categories={categories}
|
||||
aiopsListState={aiopsListState}
|
||||
dataViewId={dataView.id!}
|
||||
eventRate={eventRate}
|
||||
sparkLines={sparkLines}
|
||||
selectedField={selectedField}
|
||||
pinnedCategory={pinnedCategory}
|
||||
setPinnedCategory={setPinnedCategory}
|
||||
selectedCategory={selectedCategory}
|
||||
setSelectedCategory={setSelectedCategory}
|
||||
timefilter={timefilter}
|
||||
/>
|
||||
) : null}
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* 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 { cloneDeep, get } from 'lodash';
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/public';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
|
||||
const CATEGORY_LIMIT = 1000;
|
||||
const EXAMPLE_LIMIT = 1;
|
||||
|
||||
interface CatResponse {
|
||||
rawResponse: {
|
||||
aggregations: {
|
||||
categories: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
hit: { hits: { hits: Array<{ _source: { message: string } }> } };
|
||||
sparkline: { buckets: Array<{ key_as_string: string; key: number; doc_count: number }> };
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
key: string;
|
||||
count: number;
|
||||
examples: string[];
|
||||
sparkline?: Array<{ doc_count: number; key: number; key_as_string: string }>;
|
||||
}
|
||||
|
||||
export type EventRate = Array<{
|
||||
key: number;
|
||||
docCount: number;
|
||||
}>;
|
||||
|
||||
export type SparkLinesPerCategory = Record<string, Record<number, number>>;
|
||||
|
||||
export function useCategorizeRequest() {
|
||||
const { data } = useAiopsAppContext();
|
||||
|
||||
const abortController = useRef(new AbortController());
|
||||
|
||||
const runCategorizeRequest = useCallback(
|
||||
(
|
||||
index: string,
|
||||
field: string,
|
||||
timeField: string,
|
||||
from: number | undefined,
|
||||
to: number | undefined,
|
||||
query: QueryDslQueryContainer,
|
||||
intervalMs?: number
|
||||
): Promise<{ categories: Category[]; sparkLinesPerCategory: SparkLinesPerCategory }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
data.search
|
||||
.search<ReturnType<typeof createCategoryRequest>, CatResponse>(
|
||||
createCategoryRequest(index, field, timeField, from, to, query, intervalMs),
|
||||
{ abortSignal: abortController.current.signal }
|
||||
)
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
if (isCompleteResponse(result)) {
|
||||
resolve(processCategoryResults(result, field));
|
||||
} else if (isErrorResponse(result)) {
|
||||
reject(result);
|
||||
} else {
|
||||
// partial results
|
||||
// Ignore partial results for now.
|
||||
// An issue with the search function means partial results are not being returned correctly.
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
if (error.name === 'AbortError') {
|
||||
return resolve({ categories: [], sparkLinesPerCategory: {} });
|
||||
}
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[data.search]
|
||||
);
|
||||
|
||||
const cancelRequest = useCallback(() => {
|
||||
abortController.current.abort();
|
||||
abortController.current = new AbortController();
|
||||
}, []);
|
||||
|
||||
return { runCategorizeRequest, cancelRequest };
|
||||
}
|
||||
|
||||
function createCategoryRequest(
|
||||
index: string,
|
||||
field: string,
|
||||
timeField: string,
|
||||
from: number | undefined,
|
||||
to: number | undefined,
|
||||
queryIn: QueryDslQueryContainer,
|
||||
intervalMs?: number
|
||||
) {
|
||||
const query = cloneDeep(queryIn);
|
||||
|
||||
if (query.bool === undefined) {
|
||||
query.bool = {};
|
||||
}
|
||||
if (query.bool.must === undefined) {
|
||||
query.bool.must = [];
|
||||
if (query.match_all !== undefined) {
|
||||
query.bool.must.push({ match_all: query.match_all });
|
||||
delete query.match_all;
|
||||
}
|
||||
}
|
||||
if (query.multi_match !== undefined) {
|
||||
query.bool.should = {
|
||||
multi_match: query.multi_match,
|
||||
};
|
||||
delete query.multi_match;
|
||||
}
|
||||
|
||||
(query.bool.must as QueryDslQueryContainer[]).push({
|
||||
range: {
|
||||
[timeField]: {
|
||||
gte: from,
|
||||
lte: to,
|
||||
format: 'epoch_millis',
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
params: {
|
||||
index,
|
||||
size: 0,
|
||||
body: {
|
||||
query,
|
||||
aggs: {
|
||||
categories: {
|
||||
categorize_text: {
|
||||
field,
|
||||
size: CATEGORY_LIMIT,
|
||||
},
|
||||
aggs: {
|
||||
hit: {
|
||||
top_hits: {
|
||||
size: EXAMPLE_LIMIT,
|
||||
sort: [timeField],
|
||||
_source: field,
|
||||
},
|
||||
},
|
||||
...(intervalMs
|
||||
? {
|
||||
sparkline: {
|
||||
date_histogram: {
|
||||
field: timeField,
|
||||
fixed_interval: `${intervalMs}ms`,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function processCategoryResults(result: CatResponse, field: string) {
|
||||
const sparkLinesPerCategory: SparkLinesPerCategory = {};
|
||||
|
||||
if (result.rawResponse.aggregations === undefined) {
|
||||
throw new Error('processCategoryResults failed, did not return aggregations.');
|
||||
}
|
||||
|
||||
const categories: Category[] = result.rawResponse.aggregations.categories.buckets.map((b) => {
|
||||
sparkLinesPerCategory[b.key] =
|
||||
b.sparkline === undefined
|
||||
? {}
|
||||
: b.sparkline.buckets.reduce<Record<number, number>>((acc2, cur2) => {
|
||||
acc2[cur2.key] = cur2.doc_count;
|
||||
return acc2;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
key: b.key,
|
||||
count: b.doc_count,
|
||||
examples: b.hit.hits.hits.map((h) => get(h._source, field)),
|
||||
};
|
||||
});
|
||||
return {
|
||||
categories,
|
||||
sparkLinesPerCategory,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 rison from 'rison-node';
|
||||
import moment from 'moment';
|
||||
|
||||
import type { TimeRangeBounds } from '@kbn/data-plugin/common';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import type { Category } from './use_categorize_request';
|
||||
import type { QueryMode } from './category_table';
|
||||
import type { AiOpsIndexBasedAppState } from '../explain_log_rate_spikes/explain_log_rate_spikes_app_state';
|
||||
|
||||
export function useDiscoverLinks() {
|
||||
const {
|
||||
http: { basePath },
|
||||
} = useAiopsAppContext();
|
||||
|
||||
const openInDiscoverWithFilter = (
|
||||
index: string,
|
||||
field: string,
|
||||
selection: Category[],
|
||||
aiopsListState: Required<AiOpsIndexBasedAppState>,
|
||||
timefilterActiveBounds: TimeRangeBounds,
|
||||
mode: QueryMode,
|
||||
category?: Category
|
||||
) => {
|
||||
const selectedRows = category === undefined ? selection : [category];
|
||||
|
||||
const _g = rison.encode({
|
||||
time: {
|
||||
from: moment(timefilterActiveBounds.min?.valueOf()).toISOString(),
|
||||
to: moment(timefilterActiveBounds.max?.valueOf()).toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
const _a = rison.encode({
|
||||
filters: [
|
||||
...aiopsListState.filters,
|
||||
{
|
||||
query: {
|
||||
bool: {
|
||||
[mode]: selectedRows.map(({ key: query }) => ({
|
||||
match: {
|
||||
[field]: {
|
||||
auto_generate_synonyms_phrase_query: false,
|
||||
fuzziness: 0,
|
||||
operator: 'and',
|
||||
query,
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
index,
|
||||
interval: 'auto',
|
||||
query: {
|
||||
language: aiopsListState.searchQueryLanguage,
|
||||
query: aiopsListState.searchString,
|
||||
},
|
||||
});
|
||||
|
||||
let path = basePath.get();
|
||||
path += '/app/discover#/';
|
||||
path += '?_g=' + _g;
|
||||
path += '&_a=' + encodeURIComponent(_a);
|
||||
window.open(path, '_blank');
|
||||
};
|
||||
|
||||
return { openInDiscoverWithFilter };
|
||||
}
|
|
@ -29,6 +29,8 @@ import { useTimefilter } from './use_time_filter';
|
|||
import { useDocumentCountStats } from './use_document_count_stats';
|
||||
import type { Dictionary } from './use_url_state';
|
||||
|
||||
const DEFAULT_BAR_TARGET = 75;
|
||||
|
||||
export const useData = (
|
||||
{
|
||||
currentDataView,
|
||||
|
@ -36,7 +38,8 @@ export const useData = (
|
|||
}: { currentDataView: DataView; currentSavedSearch: SavedSearch | SavedSearchSavedObject | null },
|
||||
aiopsListState: AiOpsIndexBasedAppState,
|
||||
onUpdate: (params: Dictionary<unknown>) => void,
|
||||
selectedChangePoint?: ChangePoint
|
||||
selectedChangePoint?: ChangePoint,
|
||||
barTarget: number = DEFAULT_BAR_TARGET
|
||||
) => {
|
||||
const {
|
||||
uiSettings,
|
||||
|
@ -125,10 +128,9 @@ export const useData = (
|
|||
function updateFieldStatsRequest() {
|
||||
const timefilterActiveBounds = timefilter.getActiveBounds();
|
||||
if (timefilterActiveBounds !== undefined) {
|
||||
const BAR_TARGET = 75;
|
||||
_timeBuckets.setInterval('auto');
|
||||
_timeBuckets.setBounds(timefilterActiveBounds);
|
||||
_timeBuckets.setBarTarget(BAR_TARGET);
|
||||
_timeBuckets.setBarTarget(barTarget);
|
||||
setFieldStatsRequest({
|
||||
earliest: timefilterActiveBounds.min?.valueOf(),
|
||||
latest: timefilterActiveBounds.max?.valueOf(),
|
||||
|
@ -186,6 +188,7 @@ export const useData = (
|
|||
earliest: fieldStatsRequest?.earliest,
|
||||
/** End timestamp filter */
|
||||
latest: fieldStatsRequest?.latest,
|
||||
intervalMs: fieldStatsRequest?.intervalMs,
|
||||
searchQueryLanguage,
|
||||
searchString,
|
||||
searchQuery,
|
||||
|
|
|
@ -13,4 +13,4 @@ export function plugin() {
|
|||
return new AiopsPlugin();
|
||||
}
|
||||
|
||||
export { ExplainLogRateSpikes } from './shared_lazy_components';
|
||||
export { ExplainLogRateSpikes, LogCategorization } from './shared_lazy_components';
|
||||
|
|
|
@ -9,12 +9,13 @@ import React, { FC, Suspense } from 'react';
|
|||
|
||||
import { EuiErrorBoundary, EuiLoadingContent } from '@elastic/eui';
|
||||
import type { ExplainLogRateSpikesAppStateProps } from './components/explain_log_rate_spikes';
|
||||
import type { LogCategorizationAppStateProps } from './components/log_categorization';
|
||||
|
||||
const ExplainLogRateSpikesAppStateLazy = React.lazy(
|
||||
() => import('./components/explain_log_rate_spikes')
|
||||
);
|
||||
|
||||
const LazyWrapper: FC = ({ children }) => (
|
||||
const ExplainLogRateSpikesLazyWrapper: FC = ({ children }) => (
|
||||
<EuiErrorBoundary>
|
||||
<Suspense fallback={<EuiLoadingContent lines={3} />}>{children}</Suspense>
|
||||
</EuiErrorBoundary>
|
||||
|
@ -25,7 +26,25 @@ const LazyWrapper: FC = ({ children }) => (
|
|||
* @param {ExplainLogRateSpikesAppStateProps} props - properties specifying the data on which to run the analysis.
|
||||
*/
|
||||
export const ExplainLogRateSpikes: FC<ExplainLogRateSpikesAppStateProps> = (props) => (
|
||||
<LazyWrapper>
|
||||
<ExplainLogRateSpikesLazyWrapper>
|
||||
<ExplainLogRateSpikesAppStateLazy {...props} />
|
||||
</LazyWrapper>
|
||||
</ExplainLogRateSpikesLazyWrapper>
|
||||
);
|
||||
|
||||
const LogCategorizationAppStateLazy = React.lazy(() => import('./components/log_categorization'));
|
||||
|
||||
const LogCategorizationLazyWrapper: FC = ({ children }) => (
|
||||
<EuiErrorBoundary>
|
||||
<Suspense fallback={<EuiLoadingContent lines={3} />}>{children}</Suspense>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
|
||||
/**
|
||||
* Lazy-wrapped LogCategorizationAppStateProps React component
|
||||
* @param {LogCategorizationAppStateProps} props - properties specifying the data on which to run the analysis.
|
||||
*/
|
||||
export const LogCategorization: FC<LogCategorizationAppStateProps> = (props) => (
|
||||
<LogCategorizationLazyWrapper>
|
||||
<LogCategorizationAppStateLazy {...props} />
|
||||
</LogCategorizationLazyWrapper>
|
||||
);
|
||||
|
|
|
@ -32,7 +32,10 @@ export class AiopsPlugin
|
|||
this.logger = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup<AiopsPluginStartDeps>, plugins: AiopsPluginSetupDeps) {
|
||||
public setup(
|
||||
core: CoreSetup<AiopsPluginStartDeps, AiopsPluginSetupDeps>,
|
||||
plugins: AiopsPluginSetupDeps
|
||||
) {
|
||||
this.logger.debug('aiops: Setup');
|
||||
|
||||
// Subscribe to license changes and store the current license in `currentLicense`.
|
||||
|
|
|
@ -56,6 +56,8 @@ export const ML_PAGES = {
|
|||
AIOPS: 'aiops',
|
||||
AIOPS_EXPLAIN_LOG_RATE_SPIKES: 'aiops/explain_log_rate_spikes',
|
||||
AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT: 'aiops/explain_log_rate_spikes_index_select',
|
||||
AIOPS_LOG_CATEGORIZATION: 'aiops/log_categorization',
|
||||
AIOPS_LOG_CATEGORIZATION_INDEX_SELECT: 'aiops/log_categorization_index_select',
|
||||
} as const;
|
||||
|
||||
export type MlPages = typeof ML_PAGES[keyof typeof ML_PAGES];
|
||||
|
|
|
@ -63,7 +63,9 @@ export type MlGenericUrlState = MLPageState<
|
|||
| typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT
|
||||
| typeof ML_PAGES.AIOPS
|
||||
| typeof ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES
|
||||
| typeof ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT,
|
||||
| typeof ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT
|
||||
| typeof ML_PAGES.AIOPS_LOG_CATEGORIZATION
|
||||
| typeof ML_PAGES.AIOPS_LOG_CATEGORIZATION_INDEX_SELECT,
|
||||
MlGenericUrlPageState | undefined
|
||||
>;
|
||||
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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, { FC } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { LogCategorization } from '@kbn/aiops-plugin/public';
|
||||
|
||||
import { useMlContext } from '../contexts/ml';
|
||||
import { useMlKibana } from '../contexts/kibana';
|
||||
import { HelpMenu } from '../components/help_menu';
|
||||
import { TechnicalPreviewBadge } from '../components/technical_preview_badge';
|
||||
|
||||
import { MlPageHeader } from '../components/page_header';
|
||||
|
||||
export const LogCategorizationPage: FC = () => {
|
||||
const { services } = useMlKibana();
|
||||
|
||||
const context = useMlContext();
|
||||
const dataView = context.currentDataView;
|
||||
const savedSearch = context.currentSavedSearch;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MlPageHeader>
|
||||
<EuiFlexGroup responsive={false} wrap={false} alignItems={'center'} gutterSize={'m'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.logCategorization.pageHeader"
|
||||
defaultMessage="Log Pattern Analysis"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TechnicalPreviewBadge />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</MlPageHeader>
|
||||
{dataView && (
|
||||
<LogCategorization
|
||||
dataView={dataView}
|
||||
savedSearch={savedSearch}
|
||||
appDependencies={pick(services, [
|
||||
'application',
|
||||
'data',
|
||||
'charts',
|
||||
'fieldFormats',
|
||||
'http',
|
||||
'notifications',
|
||||
'share',
|
||||
'storage',
|
||||
'uiSettings',
|
||||
'unifiedSearch',
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
<HelpMenu docLink={services.docLinks.links.ml.guide} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -234,7 +234,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) {
|
|||
mlTabs.push({
|
||||
id: 'aiops_section',
|
||||
name: i18n.translate('xpack.ml.navMenu.aiopsTabLinkText', {
|
||||
defaultMessage: 'AIOps',
|
||||
defaultMessage: 'AIOps Labs',
|
||||
}),
|
||||
disabled: disableLinks,
|
||||
items: [
|
||||
|
@ -242,11 +242,20 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) {
|
|||
id: 'explainlogratespikes',
|
||||
pathId: ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT,
|
||||
name: i18n.translate('xpack.ml.navMenu.explainLogRateSpikesLinkText', {
|
||||
defaultMessage: 'Explain log rate spikes',
|
||||
defaultMessage: 'Explain Log Rate Spikes',
|
||||
}),
|
||||
disabled: disableLinks,
|
||||
testSubj: 'mlMainTab explainLogRateSpikes',
|
||||
},
|
||||
{
|
||||
id: 'logCategorization',
|
||||
pathId: ML_PAGES.AIOPS_LOG_CATEGORIZATION_INDEX_SELECT,
|
||||
name: i18n.translate('xpack.ml.navMenu.logCategorizationLinkText', {
|
||||
defaultMessage: 'Log Pattern Analysis',
|
||||
}),
|
||||
disabled: disableLinks,
|
||||
testSubj: 'mlMainTab logCategorization',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -55,13 +55,36 @@ export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
|
|||
href: '/datavisualizer',
|
||||
});
|
||||
|
||||
export const AIOPS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
|
||||
// we need two AIOPS_BREADCRUMB breadcrumb items as they each need to link
|
||||
// to either the explain log rate spikes page or the log categorization page
|
||||
export const AIOPS_BREADCRUMB_EXPLAIN_LOG_RATE_SPIKES: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbLabel', {
|
||||
defaultMessage: 'AIOps',
|
||||
defaultMessage: 'AIOps Labs',
|
||||
}),
|
||||
href: '/aiops/explain_log_rate_spikes_index_select',
|
||||
});
|
||||
|
||||
export const AIOPS_BREADCRUMB_LOG_PATTERN_ANALYSIS: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbLabel', {
|
||||
defaultMessage: 'AIOps Labs',
|
||||
}),
|
||||
href: '/aiops/log_categorization_index_select',
|
||||
});
|
||||
|
||||
export const EXPLAIN_LOG_RATE_SPIKES: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.aiops.explainLogRateSpikesBreadcrumbLabel', {
|
||||
defaultMessage: 'Explain Log Rate Spikes',
|
||||
}),
|
||||
href: '/aiops/explain_log_rate_spikes_index_select',
|
||||
});
|
||||
|
||||
export const LOG_PATTERN_ANALYSIS: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.aiops.logPatternAnalysisBreadcrumbLabel', {
|
||||
defaultMessage: 'Log Pattern Analysis',
|
||||
}),
|
||||
href: '/aiops/log_categorization_index_select',
|
||||
});
|
||||
|
||||
export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.createJobsBreadcrumbLabel', {
|
||||
defaultMessage: 'Create job',
|
||||
|
@ -90,7 +113,10 @@ const breadcrumbs = {
|
|||
DATA_FRAME_ANALYTICS_BREADCRUMB,
|
||||
TRAINED_MODELS,
|
||||
DATA_VISUALIZER_BREADCRUMB,
|
||||
AIOPS_BREADCRUMB,
|
||||
AIOPS_BREADCRUMB_EXPLAIN_LOG_RATE_SPIKES,
|
||||
AIOPS_BREADCRUMB_LOG_PATTERN_ANALYSIS,
|
||||
EXPLAIN_LOG_RATE_SPIKES,
|
||||
LOG_PATTERN_ANALYSIS,
|
||||
CREATE_JOB_BREADCRUMB,
|
||||
CALENDAR_MANAGEMENT_BREADCRUMB,
|
||||
FILTER_LISTS_BREADCRUMB,
|
||||
|
|
|
@ -35,7 +35,11 @@ export const explainLogRateSpikesRouteFactory = (
|
|||
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
|
||||
breadcrumbs: [
|
||||
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp(
|
||||
'AIOPS_BREADCRUMB_EXPLAIN_LOG_RATE_SPIKES',
|
||||
navigateToPath,
|
||||
basePath
|
||||
),
|
||||
{
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbs.explainLogRateSpikesLabel', {
|
||||
defaultMessage: 'Explain log rate spikes',
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './explain_log_rate_spikes';
|
||||
export * from './log_categorization';
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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, { FC } from 'react';
|
||||
import { parse } from 'query-string';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AIOPS_ENABLED } from '@kbn/aiops-plugin/common';
|
||||
|
||||
import { NavigateToPath } from '../../../contexts/kibana';
|
||||
|
||||
import { MlRoute, PageLoader, PageProps } from '../../router';
|
||||
import { useResolver } from '../../use_resolver';
|
||||
import { LogCategorizationPage as Page } from '../../../aiops/log_categorization';
|
||||
|
||||
import { checkBasicLicense } from '../../../license';
|
||||
import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities';
|
||||
import { cacheDataViewsContract } from '../../../util/index_utils';
|
||||
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
|
||||
|
||||
export const logCategorizationRouteFactory = (
|
||||
navigateToPath: NavigateToPath,
|
||||
basePath: string
|
||||
): MlRoute => ({
|
||||
id: 'log_categorization',
|
||||
path: '/aiops/log_categorization',
|
||||
title: i18n.translate('xpack.ml.aiops.logCategorization.docTitle', {
|
||||
defaultMessage: 'Log Pattern Analysis',
|
||||
}),
|
||||
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
|
||||
breadcrumbs: [
|
||||
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB_LOG_PATTERN_ANALYSIS', navigateToPath, basePath),
|
||||
{
|
||||
text: i18n.translate('xpack.ml.aiops.logCategorization.docTitle', {
|
||||
defaultMessage: 'Log Pattern Analysis',
|
||||
}),
|
||||
},
|
||||
],
|
||||
disabled: !AIOPS_ENABLED,
|
||||
});
|
||||
|
||||
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
|
||||
const { redirectToMlAccessDeniedPage } = deps;
|
||||
|
||||
const { index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false });
|
||||
const { context } = useResolver(index, savedSearchId, deps.config, deps.dataViewsContract, {
|
||||
checkBasicLicense,
|
||||
cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
|
||||
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
|
||||
});
|
||||
|
||||
return (
|
||||
<PageLoader context={context}>
|
||||
<Page />
|
||||
</PageLoader>
|
||||
);
|
||||
};
|
|
@ -45,17 +45,29 @@ const getDataVisBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string)
|
|||
getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath),
|
||||
{
|
||||
text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectDateViewLabel', {
|
||||
defaultMessage: 'Data View',
|
||||
defaultMessage: 'Select Data View',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const getExplainLogRateSpikesBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
|
||||
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB_EXPLAIN_LOG_RATE_SPIKES', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('EXPLAIN_LOG_RATE_SPIKES', navigateToPath, basePath),
|
||||
{
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbs.selectDateViewLabel', {
|
||||
defaultMessage: 'Data View',
|
||||
defaultMessage: 'Select Data View',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const getLogCategorizationBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
|
||||
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB_LOG_PATTERN_ANALYSIS', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('LOG_PATTERN_ANALYSIS', navigateToPath, basePath),
|
||||
{
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbs.selectDateViewLabel', {
|
||||
defaultMessage: 'Select Data View',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
@ -116,6 +128,26 @@ export const explainLogRateSpikesIndexOrSearchRouteFactory = (
|
|||
breadcrumbs: getExplainLogRateSpikesBreadcrumbs(navigateToPath, basePath),
|
||||
});
|
||||
|
||||
export const logCategorizationIndexOrSearchRouteFactory = (
|
||||
navigateToPath: NavigateToPath,
|
||||
basePath: string
|
||||
): MlRoute => ({
|
||||
id: 'data_view_log_categorization',
|
||||
path: '/aiops/log_categorization_index_select',
|
||||
title: i18n.translate('xpack.ml.selectDataViewLabel', {
|
||||
defaultMessage: 'Select Data View',
|
||||
}),
|
||||
render: (props, deps) => (
|
||||
<PageWrapper
|
||||
{...props}
|
||||
nextStepPath="aiops/log_categorization"
|
||||
deps={deps}
|
||||
mode={MODE.DATAVISUALIZER}
|
||||
/>
|
||||
),
|
||||
breadcrumbs: getLogCategorizationBreadcrumbs(navigateToPath, basePath),
|
||||
});
|
||||
|
||||
const PageWrapper: FC<IndexOrSearchPageProps> = ({ nextStepPath, deps, mode }) => {
|
||||
const {
|
||||
services: {
|
||||
|
|
|
@ -88,6 +88,8 @@ export class MlLocatorDefinition implements LocatorDefinition<MlLocatorParams> {
|
|||
case ML_PAGES.AIOPS:
|
||||
case ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES:
|
||||
case ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT:
|
||||
case ML_PAGES.AIOPS_LOG_CATEGORIZATION:
|
||||
case ML_PAGES.AIOPS_LOG_CATEGORIZATION_INDEX_SELECT:
|
||||
case ML_PAGES.OVERVIEW:
|
||||
case ML_PAGES.SETTINGS:
|
||||
case ML_PAGES.FILTER_LISTS_MANAGE:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue