diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts index 7c6a0f6d2ada7..bd9776741d165 100644 --- a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts @@ -74,6 +74,7 @@ export const getMetricsHostsAnomaliesRequestPayloadRT = rt.type({ }), rt.partial({ query: rt.string, + hostName: rt.string, metric: metricRT, // Pagination properties pagination: paginationRT, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx index 409c11cbbe897..4915f4cc6422a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx @@ -30,7 +30,6 @@ import { FormattedDate, FormattedMessage } from 'react-intl'; import { datemathToEpochMillis } from '../../../../../../../utils/datemath'; import { SnapshotMetricType } from '../../../../../../../../common/inventory_models/types'; import { withTheme } from '../../../../../../../../../../../src/plugins/kibana_react/common'; -import { PrefilledAnomalyAlertFlyout } from '../../../../../../../alerting/metric_anomaly/components/alert_flyout'; import { useLinkProps } from '../../../../../../../hooks/use_link_props'; import { useSorting } from '../../../../../../../hooks/use_sorting'; import { useMetricsK8sAnomaliesResults } from '../../../../hooks/use_metrics_k8s_anomalies'; @@ -46,6 +45,7 @@ import { AnomalySeverityIndicator } from '../../../../../../../components/loggin import { useSourceContext } from '../../../../../../../containers/metrics_source'; import { createResultsUrl } from '../flyout_home'; import { useWaffleViewState, WaffleViewState } from '../../../../hooks/use_waffle_view_state'; +import { useUiTracker } from '../../../../../../../../../observability/public'; type JobType = 'k8s' | 'hosts'; type SortField = 'anomalyScore' | 'startTime'; interface JobOption { @@ -57,22 +57,21 @@ const AnomalyActionMenu = ({ type, startTime, closeFlyout, - partitionFieldName, - partitionFieldValue, + influencerField, + influencers, + disableShowInInventory, }: { jobId: string; type: string; startTime: number; closeFlyout: () => void; - partitionFieldName?: string; - partitionFieldValue?: string; + influencerField: string; + influencers: string[]; + disableShowInInventory?: boolean; }) => { const [isOpen, setIsOpen] = useState(false); - const [isAlertOpen, setIsAlertOpen] = useState(false); const close = useCallback(() => setIsOpen(false), [setIsOpen]); const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); - const openAlert = useCallback(() => setIsAlertOpen(true), [setIsAlertOpen]); - const closeAlert = useCallback(() => setIsAlertOpen(false), [setIsAlertOpen]); const { onViewChange } = useWaffleViewState(); const showInInventory = useCallback(() => { @@ -99,10 +98,12 @@ const AnomalyActionMenu = ({ region: '', autoReload: false, filterQuery: { - expression: - partitionFieldName && partitionFieldValue - ? `${partitionFieldName}: "${partitionFieldValue}"` - : ``, + expression: influencers.reduce((query, i) => { + if (query) { + query = `${query} or `; + } + return `${query} ${influencerField}: "${i}"`; + }, ''), kind: 'kuery', }, legend: { palette: 'cool', reverseColors: false, steps: 10 }, @@ -110,7 +111,7 @@ const AnomalyActionMenu = ({ }; onViewChange(anomalyViewParams); closeFlyout(); - }, [jobId, onViewChange, startTime, type, partitionFieldName, partitionFieldValue, closeFlyout]); + }, [jobId, onViewChange, startTime, type, influencers, influencerField, closeFlyout]); const anomaliesUrl = useLinkProps({ app: 'ml', @@ -118,26 +119,25 @@ const AnomalyActionMenu = ({ }); const items = [ - - - , , - - - , ]; + if (!disableShowInInventory) { + items.push( + + + + ); + } + return ( <> } - isOpen={isOpen && !isAlertOpen} + isOpen={isOpen} closePopover={close} > - {isAlertOpen && } ); }; @@ -184,12 +183,14 @@ export const NoAnomaliesFound = withTheme(({ theme }) => ( )); interface Props { closeFlyout(): void; + hostName?: string; } export const AnomaliesTable = (props: Props) => { - const { closeFlyout } = props; + const { closeFlyout, hostName } = props; const [search, setSearch] = useState(''); const [start, setStart] = useState('now-30d'); const [end, setEnd] = useState('now'); + const trackMetric = useUiTracker({ app: 'infra_metrics' }); const [timeRange, setTimeRange] = useState<{ start: number; end: number }>({ start: datemathToEpochMillis(start) || 0, end: datemathToEpochMillis(end, 'up') || 0, @@ -321,6 +322,16 @@ export const AnomaliesTable = (props: Props) => { [hostChangeSort, k8sChangeSort, jobType] ); + useEffect(() => { + if (results) { + results.forEach((r) => { + if (r.influencers.length > 100) { + trackMetric({ metric: 'metrics_ml_anomaly_detection_more_than_100_influencers' }); + } + }); + } + }, [results, trackMetric]); + const onTableChange = (criteria: Criteria) => { setSorting(criteria.sort); changeSortOptions({ @@ -329,7 +340,7 @@ export const AnomaliesTable = (props: Props) => { }); }; - const columns: Array< + let columns: Array< | EuiTableFieldDataColumnType | EuiTableActionsColumnType > = [ @@ -394,8 +405,11 @@ export const AnomaliesTable = (props: Props) => { 100} + influencers={anomaly.influencers} startTime={anomaly.startTime} closeFlyout={closeFlyout} /> @@ -406,11 +420,20 @@ export const AnomaliesTable = (props: Props) => { }, ]; + columns = hostName + ? columns.filter((c) => { + if ('field' in c) { + return c.field !== 'influencers'; + } + return true; + }) + : columns; + useEffect(() => { if (getAnomalies) { - getAnomalies(undefined, search); + getAnomalies(undefined, search, hostName); } - }, [getAnomalies, search]); + }, [getAnomalies, search, hostName]); return (
@@ -425,31 +448,33 @@ export const AnomaliesTable = (props: Props) => { - - - - - - - - + {!hostName && ( + + + + + + + + + )} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index f21a9d1e1f591..eb5a3f3f9fee1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -19,6 +19,7 @@ import { MetricsTab } from './tabs/metrics/metrics'; import { LogsTab } from './tabs/logs'; import { ProcessesTab } from './tabs/processes'; import { PropertiesTab } from './tabs/properties/index'; +import { AnomaliesTab } from './tabs/anomalies/anomalies'; import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN } from './tabs/shared'; import { useLinkProps } from '../../../../../hooks/use_link_props'; import { getNodeDetailUrl } from '../../../../link_to'; @@ -44,7 +45,7 @@ export const NodeContextPopover = ({ openAlertFlyout, }: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps - const tabConfigs = [MetricsTab, LogsTab, ProcessesTab, PropertiesTab]; + const tabConfigs = [MetricsTab, LogsTab, ProcessesTab, PropertiesTab, AnomaliesTab]; const inventoryModel = findInventoryModel(nodeType); const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; const uiCapabilities = useKibana().services.application?.capabilities; @@ -58,11 +59,17 @@ export const NodeContextPopover = ({ return { ...m, content: ( - + ), }; }); - }, [tabConfigs, node, nodeType, currentTime, options]); + }, [tabConfigs, node, nodeType, currentTime, onClose, options]); const [selectedTab, setSelectedTab] = useState(0); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/anomalies/anomalies.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/anomalies/anomalies.tsx new file mode 100644 index 0000000000000..40dad03f73366 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/anomalies/anomalies.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { AnomaliesTable } from '../../../ml/anomaly_detection/anomalies_table/anomalies_table'; +import { TabContent, TabProps } from '../shared'; + +const TabComponent = (props: TabProps) => { + const { node, onClose } = props; + + return ( + + + + ); +}; + +export const AnomaliesTab = { + id: 'anomalies', + name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', { + defaultMessage: 'Anomalies', + }), + content: TabComponent, +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx index 979048ca685f8..f011f91a2e9df 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx @@ -14,6 +14,7 @@ export interface TabProps { currentTime: number; node: InfraWaffleMapNode; nodeType: InventoryItemType; + onClose(): void; } export const OVERLAY_Y_START = 266; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts index 5ef6c9429c4b3..b1401f268dc51 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts @@ -174,7 +174,7 @@ export const useMetricsHostsAnomaliesResults = ({ const [getMetricsHostsAnomaliesRequest, getMetricsHostsAnomalies] = useTrackedPromise( { cancelPreviousOn: 'creation', - createPromise: async (metric?: Metric, query?: string) => { + createPromise: async (metric?: Metric, query?: string, hostName?: string) => { const { timeRange: { start: queryStartTime, end: queryEndTime }, sortOptions, @@ -195,6 +195,7 @@ export const useMetricsHostsAnomaliesResults = ({ ...paginationOptions, cursor: paginationCursor, }, + hostName, }, services.http.fetch ); @@ -309,6 +310,7 @@ interface RequestArgs { endTime: number; metric?: Metric; query?: string; + hostName?: string; sort: Sort; pagination: Pagination; } @@ -326,6 +328,7 @@ export const callGetMetricHostsAnomaliesAPI = async ( sort, pagination, query, + hostName, } = requestArgs; const response = await fetch(INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH, { method: 'POST', @@ -342,6 +345,7 @@ export const callGetMetricHostsAnomaliesAPI = async ( metric, sort, pagination, + hostName, }, }) ), diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts index 15ff6c0834a75..1b5ed62a006be 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts @@ -40,6 +40,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { pagination: paginationParam, metric, query, + hostName, }, } = request.body; @@ -63,6 +64,12 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { query, sort, pagination, + influencerFilter: hostName + ? { + fieldName: 'host.name', + fieldValue: hostName, + } + : undefined, }); return response.ok({