mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Revert "[Metrics UI] Add Metrics Anomaly Alert Type (#89244)"
This reverts commit 0d94968df1
.
This commit is contained in:
parent
4bab95229d
commit
8166becc55
50 changed files with 481 additions and 1919 deletions
|
@ -4,15 +4,14 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as rt from 'io-ts';
|
import * as rt from 'io-ts';
|
||||||
import { ANOMALY_THRESHOLD } from '../../infra_ml';
|
|
||||||
import { ItemTypeRT } from '../../inventory_models/types';
|
import { ItemTypeRT } from '../../inventory_models/types';
|
||||||
|
|
||||||
// TODO: Have threshold and inventory alerts import these types from this file instead of from their
|
// TODO: Have threshold and inventory alerts import these types from this file instead of from their
|
||||||
// local directories
|
// local directories
|
||||||
export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold';
|
export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold';
|
||||||
export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';
|
export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';
|
||||||
export const METRIC_ANOMALY_ALERT_TYPE_ID = 'metrics.alert.anomaly';
|
|
||||||
|
|
||||||
export enum Comparator {
|
export enum Comparator {
|
||||||
GT = '>',
|
GT = '>',
|
||||||
|
@ -35,26 +34,6 @@ export enum Aggregators {
|
||||||
P99 = 'p99',
|
P99 = 'p99',
|
||||||
}
|
}
|
||||||
|
|
||||||
const metricAnomalyNodeTypeRT = rt.union([rt.literal('hosts'), rt.literal('k8s')]);
|
|
||||||
const metricAnomalyMetricRT = rt.union([
|
|
||||||
rt.literal('memory_usage'),
|
|
||||||
rt.literal('network_in'),
|
|
||||||
rt.literal('network_out'),
|
|
||||||
]);
|
|
||||||
const metricAnomalyInfluencerFilterRT = rt.type({
|
|
||||||
fieldName: rt.string,
|
|
||||||
fieldValue: rt.string,
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface MetricAnomalyParams {
|
|
||||||
nodeType: rt.TypeOf<typeof metricAnomalyNodeTypeRT>;
|
|
||||||
metric: rt.TypeOf<typeof metricAnomalyMetricRT>;
|
|
||||||
alertInterval?: string;
|
|
||||||
sourceId?: string;
|
|
||||||
threshold: Exclude<ANOMALY_THRESHOLD, ANOMALY_THRESHOLD.LOW>;
|
|
||||||
influencerFilter: rt.TypeOf<typeof metricAnomalyInfluencerFilterRT> | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alert Preview API
|
// Alert Preview API
|
||||||
const baseAlertRequestParamsRT = rt.intersection([
|
const baseAlertRequestParamsRT = rt.intersection([
|
||||||
rt.partial({
|
rt.partial({
|
||||||
|
@ -72,6 +51,7 @@ const baseAlertRequestParamsRT = rt.intersection([
|
||||||
rt.literal('M'),
|
rt.literal('M'),
|
||||||
rt.literal('y'),
|
rt.literal('y'),
|
||||||
]),
|
]),
|
||||||
|
criteria: rt.array(rt.any),
|
||||||
alertInterval: rt.string,
|
alertInterval: rt.string,
|
||||||
alertThrottle: rt.string,
|
alertThrottle: rt.string,
|
||||||
alertOnNoData: rt.boolean,
|
alertOnNoData: rt.boolean,
|
||||||
|
@ -85,7 +65,6 @@ const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([
|
||||||
}),
|
}),
|
||||||
rt.type({
|
rt.type({
|
||||||
alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID),
|
alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID),
|
||||||
criteria: rt.array(rt.any),
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf<
|
export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf<
|
||||||
|
@ -97,33 +76,15 @@ const inventoryAlertPreviewRequestParamsRT = rt.intersection([
|
||||||
rt.type({
|
rt.type({
|
||||||
nodeType: ItemTypeRT,
|
nodeType: ItemTypeRT,
|
||||||
alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID),
|
alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID),
|
||||||
criteria: rt.array(rt.any),
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type InventoryAlertPreviewRequestParams = rt.TypeOf<
|
export type InventoryAlertPreviewRequestParams = rt.TypeOf<
|
||||||
typeof inventoryAlertPreviewRequestParamsRT
|
typeof inventoryAlertPreviewRequestParamsRT
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const metricAnomalyAlertPreviewRequestParamsRT = rt.intersection([
|
|
||||||
baseAlertRequestParamsRT,
|
|
||||||
rt.type({
|
|
||||||
nodeType: metricAnomalyNodeTypeRT,
|
|
||||||
metric: metricAnomalyMetricRT,
|
|
||||||
threshold: rt.number,
|
|
||||||
alertType: rt.literal(METRIC_ANOMALY_ALERT_TYPE_ID),
|
|
||||||
}),
|
|
||||||
rt.partial({
|
|
||||||
influencerFilter: metricAnomalyInfluencerFilterRT,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
export type MetricAnomalyAlertPreviewRequestParams = rt.TypeOf<
|
|
||||||
typeof metricAnomalyAlertPreviewRequestParamsRT
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const alertPreviewRequestParamsRT = rt.union([
|
export const alertPreviewRequestParamsRT = rt.union([
|
||||||
metricThresholdAlertPreviewRequestParamsRT,
|
metricThresholdAlertPreviewRequestParamsRT,
|
||||||
inventoryAlertPreviewRequestParamsRT,
|
inventoryAlertPreviewRequestParamsRT,
|
||||||
metricAnomalyAlertPreviewRequestParamsRT,
|
|
||||||
]);
|
]);
|
||||||
export type AlertPreviewRequestParams = rt.TypeOf<typeof alertPreviewRequestParamsRT>;
|
export type AlertPreviewRequestParams = rt.TypeOf<typeof alertPreviewRequestParamsRT>;
|
||||||
|
|
||||||
|
|
|
@ -5,44 +5,36 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export enum ANOMALY_SEVERITY {
|
export const ML_SEVERITY_SCORES = {
|
||||||
CRITICAL = 'critical',
|
warning: 3,
|
||||||
MAJOR = 'major',
|
minor: 25,
|
||||||
MINOR = 'minor',
|
major: 50,
|
||||||
WARNING = 'warning',
|
critical: 75,
|
||||||
LOW = 'low',
|
|
||||||
UNKNOWN = 'unknown',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ANOMALY_THRESHOLD {
|
|
||||||
CRITICAL = 75,
|
|
||||||
MAJOR = 50,
|
|
||||||
MINOR = 25,
|
|
||||||
WARNING = 3,
|
|
||||||
LOW = 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SEVERITY_COLORS = {
|
|
||||||
CRITICAL: '#fe5050',
|
|
||||||
MAJOR: '#fba740',
|
|
||||||
MINOR: '#fdec25',
|
|
||||||
WARNING: '#8bc8fb',
|
|
||||||
LOW: '#d2e9f7',
|
|
||||||
BLANK: '#ffffff',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSeverityCategoryForScore = (score: number): ANOMALY_SEVERITY | undefined => {
|
export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES;
|
||||||
if (score >= ANOMALY_THRESHOLD.CRITICAL) {
|
|
||||||
return ANOMALY_SEVERITY.CRITICAL;
|
export const ML_SEVERITY_COLORS = {
|
||||||
} else if (score >= ANOMALY_THRESHOLD.MAJOR) {
|
critical: 'rgb(228, 72, 72)',
|
||||||
return ANOMALY_SEVERITY.MAJOR;
|
major: 'rgb(229, 113, 0)',
|
||||||
} else if (score >= ANOMALY_THRESHOLD.MINOR) {
|
minor: 'rgb(255, 221, 0)',
|
||||||
return ANOMALY_SEVERITY.MINOR;
|
warning: 'rgb(125, 180, 226)',
|
||||||
} else if (score >= ANOMALY_THRESHOLD.WARNING) {
|
};
|
||||||
return ANOMALY_SEVERITY.WARNING;
|
|
||||||
|
export const getSeverityCategoryForScore = (
|
||||||
|
score: number
|
||||||
|
): MLSeverityScoreCategories | undefined => {
|
||||||
|
if (score >= ML_SEVERITY_SCORES.critical) {
|
||||||
|
return 'critical';
|
||||||
|
} else if (score >= ML_SEVERITY_SCORES.major) {
|
||||||
|
return 'major';
|
||||||
|
} else if (score >= ML_SEVERITY_SCORES.minor) {
|
||||||
|
return 'minor';
|
||||||
|
} else if (score >= ML_SEVERITY_SCORES.warning) {
|
||||||
|
return 'warning';
|
||||||
} else {
|
} else {
|
||||||
// Category is too low to include
|
// Category is too low to include
|
||||||
return ANOMALY_SEVERITY.LOW;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ interface Props {
|
||||||
alertInterval: string;
|
alertInterval: string;
|
||||||
alertThrottle: string;
|
alertThrottle: string;
|
||||||
alertType: PreviewableAlertTypes;
|
alertType: PreviewableAlertTypes;
|
||||||
alertParams: { criteria?: any[]; sourceId: string } & Record<string, any>;
|
alertParams: { criteria: any[]; sourceId: string } & Record<string, any>;
|
||||||
validate: (params: any) => ValidationResult;
|
validate: (params: any) => ValidationResult;
|
||||||
showNoDataResults?: boolean;
|
showNoDataResults?: boolean;
|
||||||
groupByDisplayName?: string;
|
groupByDisplayName?: string;
|
||||||
|
@ -109,7 +109,6 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
||||||
}, [previewLookbackInterval, alertInterval]);
|
}, [previewLookbackInterval, alertInterval]);
|
||||||
|
|
||||||
const isPreviewDisabled = useMemo(() => {
|
const isPreviewDisabled = useMemo(() => {
|
||||||
if (!alertParams.criteria) return false;
|
|
||||||
const validationResult = validate({ criteria: alertParams.criteria } as any);
|
const validationResult = validate({ criteria: alertParams.criteria } as any);
|
||||||
const hasValidationErrors = Object.values(validationResult.errors).some((result) =>
|
const hasValidationErrors = Object.values(validationResult.errors).some((result) =>
|
||||||
Object.values(result).some((arr) => Array.isArray(arr) && arr.length)
|
Object.values(result).some((arr) => Array.isArray(arr) && arr.length)
|
||||||
|
|
|
@ -10,15 +10,13 @@ import {
|
||||||
INFRA_ALERT_PREVIEW_PATH,
|
INFRA_ALERT_PREVIEW_PATH,
|
||||||
METRIC_THRESHOLD_ALERT_TYPE_ID,
|
METRIC_THRESHOLD_ALERT_TYPE_ID,
|
||||||
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
|
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
|
||||||
METRIC_ANOMALY_ALERT_TYPE_ID,
|
|
||||||
AlertPreviewRequestParams,
|
AlertPreviewRequestParams,
|
||||||
AlertPreviewSuccessResponsePayload,
|
AlertPreviewSuccessResponsePayload,
|
||||||
} from '../../../../common/alerting/metrics';
|
} from '../../../../common/alerting/metrics';
|
||||||
|
|
||||||
export type PreviewableAlertTypes =
|
export type PreviewableAlertTypes =
|
||||||
| typeof METRIC_THRESHOLD_ALERT_TYPE_ID
|
| typeof METRIC_THRESHOLD_ALERT_TYPE_ID
|
||||||
| typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID
|
| typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID;
|
||||||
| typeof METRIC_ANOMALY_ALERT_TYPE_ID;
|
|
||||||
|
|
||||||
export async function getAlertPreview({
|
export async function getAlertPreview({
|
||||||
fetch,
|
fetch,
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import React, { useState, useCallback, useMemo } from 'react';
|
|
||||||
import {
|
|
||||||
EuiPopover,
|
|
||||||
EuiButtonEmpty,
|
|
||||||
EuiContextMenu,
|
|
||||||
EuiContextMenuPanelDescriptor,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
|
||||||
import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities';
|
|
||||||
import { PrefilledInventoryAlertFlyout } from '../../inventory/components/alert_flyout';
|
|
||||||
import { PrefilledThresholdAlertFlyout } from '../../metric_threshold/components/alert_flyout';
|
|
||||||
import { PrefilledAnomalyAlertFlyout } from '../../metric_anomaly/components/alert_flyout';
|
|
||||||
import { useLinkProps } from '../../../hooks/use_link_props';
|
|
||||||
|
|
||||||
type VisibleFlyoutType = 'inventory' | 'threshold' | 'anomaly' | null;
|
|
||||||
|
|
||||||
export const MetricsAlertDropdown = () => {
|
|
||||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
|
||||||
const [visibleFlyoutType, setVisibleFlyoutType] = useState<VisibleFlyoutType>(null);
|
|
||||||
const { hasInfraMLCapabilities } = useInfraMLCapabilities();
|
|
||||||
|
|
||||||
const closeFlyout = useCallback(() => setVisibleFlyoutType(null), [setVisibleFlyoutType]);
|
|
||||||
|
|
||||||
const manageAlertsLinkProps = useLinkProps({
|
|
||||||
app: 'management',
|
|
||||||
pathname: '/insightsAndAlerting/triggersActions/alerts',
|
|
||||||
});
|
|
||||||
|
|
||||||
const panels: EuiContextMenuPanelDescriptor[] = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', {
|
|
||||||
defaultMessage: 'Alerts',
|
|
||||||
}),
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: i18n.translate('xpack.infra.alerting.infrastructureDropdownMenu', {
|
|
||||||
defaultMessage: 'Infrastructure',
|
|
||||||
}),
|
|
||||||
panel: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n.translate('xpack.infra.alerting.metricsDropdownMenu', {
|
|
||||||
defaultMessage: 'Metrics',
|
|
||||||
}),
|
|
||||||
panel: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n.translate('xpack.infra.alerting.manageAlerts', {
|
|
||||||
defaultMessage: 'Manage alerts',
|
|
||||||
}),
|
|
||||||
icon: 'tableOfContents',
|
|
||||||
onClick: manageAlertsLinkProps.onClick,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', {
|
|
||||||
defaultMessage: 'Infrastructure alerts',
|
|
||||||
}),
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', {
|
|
||||||
defaultMessage: 'Create inventory alert',
|
|
||||||
}),
|
|
||||||
onClick: () => setVisibleFlyoutType('inventory'),
|
|
||||||
},
|
|
||||||
].concat(
|
|
||||||
hasInfraMLCapabilities
|
|
||||||
? {
|
|
||||||
name: i18n.translate('xpack.infra.alerting.createAnomalyAlertButton', {
|
|
||||||
defaultMessage: 'Create anomaly alert',
|
|
||||||
}),
|
|
||||||
onClick: () => setVisibleFlyoutType('anomaly'),
|
|
||||||
}
|
|
||||||
: []
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', {
|
|
||||||
defaultMessage: 'Metrics alerts',
|
|
||||||
}),
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', {
|
|
||||||
defaultMessage: 'Create threshold alert',
|
|
||||||
}),
|
|
||||||
onClick: () => setVisibleFlyoutType('threshold'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[manageAlertsLinkProps, setVisibleFlyoutType, hasInfraMLCapabilities]
|
|
||||||
);
|
|
||||||
|
|
||||||
const closePopover = useCallback(() => {
|
|
||||||
setPopoverOpen(false);
|
|
||||||
}, [setPopoverOpen]);
|
|
||||||
|
|
||||||
const openPopover = useCallback(() => {
|
|
||||||
setPopoverOpen(true);
|
|
||||||
}, [setPopoverOpen]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EuiPopover
|
|
||||||
panelPaddingSize="none"
|
|
||||||
anchorPosition="downLeft"
|
|
||||||
button={
|
|
||||||
<EuiButtonEmpty iconSide={'right'} iconType={'arrowDown'} onClick={openPopover}>
|
|
||||||
<FormattedMessage id="xpack.infra.alerting.alertsButton" defaultMessage="Alerts" />
|
|
||||||
</EuiButtonEmpty>
|
|
||||||
}
|
|
||||||
isOpen={popoverOpen}
|
|
||||||
closePopover={closePopover}
|
|
||||||
>
|
|
||||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
|
||||||
</EuiPopover>
|
|
||||||
<AlertFlyout visibleFlyoutType={visibleFlyoutType} onClose={closeFlyout} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AlertFlyoutProps {
|
|
||||||
visibleFlyoutType: VisibleFlyoutType;
|
|
||||||
onClose(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AlertFlyout = ({ visibleFlyoutType, onClose }: AlertFlyoutProps) => {
|
|
||||||
switch (visibleFlyoutType) {
|
|
||||||
case 'inventory':
|
|
||||||
return <PrefilledInventoryAlertFlyout onClose={onClose} />;
|
|
||||||
case 'threshold':
|
|
||||||
return <PrefilledThresholdAlertFlyout onClose={onClose} />;
|
|
||||||
case 'anomaly':
|
|
||||||
return <PrefilledAnomalyAlertFlyout onClose={onClose} />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* 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, { useState, useCallback } from 'react';
|
||||||
|
import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
|
||||||
|
import { AlertFlyout } from './alert_flyout';
|
||||||
|
import { ManageAlertsContextMenuItem } from './manage_alerts_context_menu_item';
|
||||||
|
|
||||||
|
export const InventoryAlertDropdown = () => {
|
||||||
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
const [flyoutVisible, setFlyoutVisible] = useState(false);
|
||||||
|
|
||||||
|
const { inventoryPrefill } = useAlertPrefillContext();
|
||||||
|
const { nodeType, metric, filterQuery } = inventoryPrefill;
|
||||||
|
|
||||||
|
const closePopover = useCallback(() => {
|
||||||
|
setPopoverOpen(false);
|
||||||
|
}, [setPopoverOpen]);
|
||||||
|
|
||||||
|
const openPopover = useCallback(() => {
|
||||||
|
setPopoverOpen(true);
|
||||||
|
}, [setPopoverOpen]);
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
<EuiContextMenuItem icon="bell" key="createLink" onClick={() => setFlyoutVisible(true)}>
|
||||||
|
<FormattedMessage id="xpack.infra.alerting.createAlertButton" defaultMessage="Create alert" />
|
||||||
|
</EuiContextMenuItem>,
|
||||||
|
<ManageAlertsContextMenuItem />,
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiPopover
|
||||||
|
button={
|
||||||
|
<EuiButtonEmpty iconSide={'right'} iconType={'arrowDown'} onClick={openPopover}>
|
||||||
|
<FormattedMessage id="xpack.infra.alerting.alertsButton" defaultMessage="Alerts" />
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
}
|
||||||
|
isOpen={popoverOpen}
|
||||||
|
closePopover={closePopover}
|
||||||
|
>
|
||||||
|
<EuiContextMenuPanel items={menuItems} />
|
||||||
|
</EuiPopover>
|
||||||
|
<AlertFlyout
|
||||||
|
setVisible={setFlyoutVisible}
|
||||||
|
visible={flyoutVisible}
|
||||||
|
nodeType={nodeType}
|
||||||
|
options={{ metric }}
|
||||||
|
filter={filterQuery}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,7 +8,8 @@
|
||||||
import React, { useCallback, useContext, useMemo } from 'react';
|
import React, { useCallback, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
|
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
|
||||||
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics';
|
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||||
|
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
|
||||||
import { InfraWaffleMapOptions } from '../../../lib/lib';
|
import { InfraWaffleMapOptions } from '../../../lib/lib';
|
||||||
import { InventoryItemType } from '../../../../common/inventory_models/types';
|
import { InventoryItemType } from '../../../../common/inventory_models/types';
|
||||||
import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
|
import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
|
||||||
|
@ -48,18 +49,3 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }:
|
||||||
|
|
||||||
return <>{visible && AddAlertFlyout}</>;
|
return <>{visible && AddAlertFlyout}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PrefilledInventoryAlertFlyout = ({ onClose }: { onClose(): void }) => {
|
|
||||||
const { inventoryPrefill } = useAlertPrefillContext();
|
|
||||||
const { nodeType, metric, filterQuery } = inventoryPrefill;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertFlyout
|
|
||||||
options={{ metric }}
|
|
||||||
nodeType={nodeType}
|
|
||||||
filter={filterQuery}
|
|
||||||
visible
|
|
||||||
setVisible={onClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ export const NodeTypeExpression = ({
|
||||||
<ClosablePopoverTitle onClose={() => setAggTypePopoverOpen(false)}>
|
<ClosablePopoverTitle onClose={() => setAggTypePopoverOpen(false)}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.infra.metrics.alertFlyout.expression.for.popoverTitle"
|
id="xpack.infra.metrics.alertFlyout.expression.for.popoverTitle"
|
||||||
defaultMessage="Node Type"
|
defaultMessage="Inventory Type"
|
||||||
/>
|
/>
|
||||||
</ClosablePopoverTitle>
|
</ClosablePopoverTitle>
|
||||||
<EuiSelect
|
<EuiSelect
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useCallback, useContext, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
|
|
||||||
import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../../common/alerting/metrics';
|
|
||||||
import { InfraWaffleMapOptions } from '../../../lib/lib';
|
|
||||||
import { InventoryItemType } from '../../../../common/inventory_models/types';
|
|
||||||
import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
visible?: boolean;
|
|
||||||
metric?: InfraWaffleMapOptions['metric'];
|
|
||||||
nodeType?: InventoryItemType;
|
|
||||||
filter?: string;
|
|
||||||
setVisible(val: boolean): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AlertFlyout = ({ metric, nodeType, visible, setVisible }: Props) => {
|
|
||||||
const { triggersActionsUI } = useContext(TriggerActionsContext);
|
|
||||||
|
|
||||||
const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]);
|
|
||||||
const AddAlertFlyout = useMemo(
|
|
||||||
() =>
|
|
||||||
triggersActionsUI &&
|
|
||||||
triggersActionsUI.getAddAlertFlyout({
|
|
||||||
consumer: 'infrastructure',
|
|
||||||
onClose: onCloseFlyout,
|
|
||||||
canChangeTrigger: false,
|
|
||||||
alertTypeId: METRIC_ANOMALY_ALERT_TYPE_ID,
|
|
||||||
metadata: {
|
|
||||||
metric,
|
|
||||||
nodeType,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[triggersActionsUI, visible]
|
|
||||||
);
|
|
||||||
|
|
||||||
return <>{visible && AddAlertFlyout}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PrefilledAnomalyAlertFlyout = ({ onClose }: { onClose(): void }) => {
|
|
||||||
const { inventoryPrefill } = useAlertPrefillContext();
|
|
||||||
const { nodeType, metric } = inventoryPrefill;
|
|
||||||
|
|
||||||
return <AlertFlyout metric={metric} nodeType={nodeType} visible setVisible={onClose} />;
|
|
||||||
};
|
|
|
@ -1,74 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { mountWithIntl, nextTick } from '@kbn/test/jest';
|
|
||||||
// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock`
|
|
||||||
import { coreMock as mockCoreMock } from 'src/core/public/mocks';
|
|
||||||
import React from 'react';
|
|
||||||
import { Expression, AlertContextMeta } from './expression';
|
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
|
|
||||||
jest.mock('../../../containers/source/use_source_via_http', () => ({
|
|
||||||
useSourceViaHttp: () => ({
|
|
||||||
source: { id: 'default' },
|
|
||||||
createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('../../../hooks/use_kibana', () => ({
|
|
||||||
useKibanaContextForPlugin: () => ({
|
|
||||||
services: mockCoreMock.createStart(),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('../../../containers/ml/infra_ml_capabilities', () => ({
|
|
||||||
useInfraMLCapabilities: () => ({
|
|
||||||
isLoading: false,
|
|
||||||
hasInfraMLCapabilities: true,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('Expression', () => {
|
|
||||||
async function setup(currentOptions: AlertContextMeta) {
|
|
||||||
const alertParams = {
|
|
||||||
metric: undefined,
|
|
||||||
nodeType: undefined,
|
|
||||||
threshold: 50,
|
|
||||||
};
|
|
||||||
const wrapper = mountWithIntl(
|
|
||||||
<Expression
|
|
||||||
alertInterval="1m"
|
|
||||||
alertThrottle="1m"
|
|
||||||
alertParams={alertParams as any}
|
|
||||||
errors={[]}
|
|
||||||
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
|
|
||||||
setAlertProperty={() => {}}
|
|
||||||
metadata={currentOptions}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const update = async () =>
|
|
||||||
await act(async () => {
|
|
||||||
await nextTick();
|
|
||||||
wrapper.update();
|
|
||||||
});
|
|
||||||
|
|
||||||
await update();
|
|
||||||
|
|
||||||
return { wrapper, update, alertParams };
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should prefill the alert using the context metadata', async () => {
|
|
||||||
const currentOptions = {
|
|
||||||
nodeType: 'pod',
|
|
||||||
metric: { type: 'tx' },
|
|
||||||
};
|
|
||||||
const { alertParams } = await setup(currentOptions as AlertContextMeta);
|
|
||||||
expect(alertParams.nodeType).toBe('k8s');
|
|
||||||
expect(alertParams.metric).toBe('network_out');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,320 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { pick } from 'lodash';
|
|
||||||
import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
|
||||||
import { EuiFlexGroup, EuiSpacer, EuiText, EuiLoadingContent } from '@elastic/eui';
|
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities';
|
|
||||||
import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
|
|
||||||
import { AlertPreview } from '../../common';
|
|
||||||
import {
|
|
||||||
METRIC_ANOMALY_ALERT_TYPE_ID,
|
|
||||||
MetricAnomalyParams,
|
|
||||||
} from '../../../../common/alerting/metrics';
|
|
||||||
import { euiStyled, EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common';
|
|
||||||
import {
|
|
||||||
WhenExpression,
|
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
|
||||||
} from '../../../../../triggers_actions_ui/public/common';
|
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
|
||||||
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
|
|
||||||
import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';
|
|
||||||
import { findInventoryModel } from '../../../../common/inventory_models';
|
|
||||||
import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
|
|
||||||
import { NodeTypeExpression } from './node_type';
|
|
||||||
import { SeverityThresholdExpression } from './severity_threshold';
|
|
||||||
import { InfraWaffleMapOptions } from '../../../lib/lib';
|
|
||||||
import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml';
|
|
||||||
|
|
||||||
import { validateMetricAnomaly } from './validation';
|
|
||||||
import { InfluencerFilter } from './influencer_filter';
|
|
||||||
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
|
|
||||||
|
|
||||||
export interface AlertContextMeta {
|
|
||||||
metric?: InfraWaffleMapOptions['metric'];
|
|
||||||
nodeType?: InventoryItemType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
errors: IErrorObject[];
|
|
||||||
alertParams: MetricAnomalyParams & {
|
|
||||||
sourceId: string;
|
|
||||||
};
|
|
||||||
alertInterval: string;
|
|
||||||
alertThrottle: string;
|
|
||||||
setAlertParams(key: string, value: any): void;
|
|
||||||
setAlertProperty(key: string, value: any): void;
|
|
||||||
metadata: AlertContextMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultExpression = {
|
|
||||||
metric: 'memory_usage' as MetricAnomalyParams['metric'],
|
|
||||||
threshold: ANOMALY_THRESHOLD.MAJOR,
|
|
||||||
nodeType: 'hosts',
|
|
||||||
influencerFilter: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Expression: React.FC<Props> = (props) => {
|
|
||||||
const { hasInfraMLCapabilities, isLoading: isLoadingMLCapabilities } = useInfraMLCapabilities();
|
|
||||||
const { http, notifications } = useKibanaContextForPlugin().services;
|
|
||||||
const { setAlertParams, alertParams, alertInterval, alertThrottle, metadata } = props;
|
|
||||||
const { source, createDerivedIndexPattern } = useSourceViaHttp({
|
|
||||||
sourceId: 'default',
|
|
||||||
type: 'metrics',
|
|
||||||
fetch: http.fetch,
|
|
||||||
toastWarning: notifications.toasts.addWarning,
|
|
||||||
});
|
|
||||||
|
|
||||||
const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
|
|
||||||
createDerivedIndexPattern,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [influencerFieldName, updateInfluencerFieldName] = useState(
|
|
||||||
alertParams.influencerFilter?.fieldName ?? 'host.name'
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAlertParams('hasInfraMLCapabilities', hasInfraMLCapabilities);
|
|
||||||
}, [setAlertParams, hasInfraMLCapabilities]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (alertParams.influencerFilter) {
|
|
||||||
setAlertParams('influencerFilter', {
|
|
||||||
...alertParams.influencerFilter,
|
|
||||||
fieldName: influencerFieldName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [influencerFieldName, alertParams, setAlertParams]);
|
|
||||||
const updateInfluencerFieldValue = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
if (value) {
|
|
||||||
setAlertParams('influencerFilter', {
|
|
||||||
...alertParams.influencerFilter,
|
|
||||||
fieldValue: value,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setAlertParams('influencerFilter', undefined);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setAlertParams, alertParams]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAlertParams('alertInterval', alertInterval);
|
|
||||||
}, [setAlertParams, alertInterval]);
|
|
||||||
|
|
||||||
const updateNodeType = useCallback(
|
|
||||||
(nt: any) => {
|
|
||||||
setAlertParams('nodeType', nt);
|
|
||||||
},
|
|
||||||
[setAlertParams]
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateMetric = useCallback(
|
|
||||||
(metric: string) => {
|
|
||||||
setAlertParams('metric', metric);
|
|
||||||
},
|
|
||||||
[setAlertParams]
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateSeverityThreshold = useCallback(
|
|
||||||
(threshold: any) => {
|
|
||||||
setAlertParams('threshold', threshold);
|
|
||||||
},
|
|
||||||
[setAlertParams]
|
|
||||||
);
|
|
||||||
|
|
||||||
const prefillNodeType = useCallback(() => {
|
|
||||||
const md = metadata;
|
|
||||||
if (md && md.nodeType) {
|
|
||||||
setAlertParams(
|
|
||||||
'nodeType',
|
|
||||||
getMLNodeTypeFromInventoryNodeType(md.nodeType) ?? defaultExpression.nodeType
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setAlertParams('nodeType', defaultExpression.nodeType);
|
|
||||||
}
|
|
||||||
}, [metadata, setAlertParams]);
|
|
||||||
|
|
||||||
const prefillMetric = useCallback(() => {
|
|
||||||
const md = metadata;
|
|
||||||
if (md && md.metric) {
|
|
||||||
setAlertParams(
|
|
||||||
'metric',
|
|
||||||
getMLMetricFromInventoryMetric(md.metric.type) ?? defaultExpression.metric
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setAlertParams('metric', defaultExpression.metric);
|
|
||||||
}
|
|
||||||
}, [metadata, setAlertParams]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!alertParams.nodeType) {
|
|
||||||
prefillNodeType();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!alertParams.threshold) {
|
|
||||||
setAlertParams('threshold', defaultExpression.threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!alertParams.metric) {
|
|
||||||
prefillMetric();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!alertParams.sourceId) {
|
|
||||||
setAlertParams('sourceId', source?.id || 'default');
|
|
||||||
}
|
|
||||||
}, [metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
if (isLoadingMLCapabilities) return <EuiLoadingContent lines={10} />;
|
|
||||||
if (!hasInfraMLCapabilities) return <SubscriptionSplashContent />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
// https://github.com/elastic/kibana/issues/89506
|
|
||||||
<EuiThemeProvider>
|
|
||||||
<EuiText size="xs">
|
|
||||||
<h4>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.infra.metrics.alertFlyout.conditions"
|
|
||||||
defaultMessage="Conditions"
|
|
||||||
/>
|
|
||||||
</h4>
|
|
||||||
</EuiText>
|
|
||||||
<StyledExpression>
|
|
||||||
<StyledExpressionRow>
|
|
||||||
<NodeTypeExpression
|
|
||||||
options={nodeTypes}
|
|
||||||
value={alertParams.nodeType ?? defaultExpression.nodeType}
|
|
||||||
onChange={updateNodeType}
|
|
||||||
/>
|
|
||||||
</StyledExpressionRow>
|
|
||||||
</StyledExpression>
|
|
||||||
<EuiSpacer size={'xs'} />
|
|
||||||
<StyledExpressionRow>
|
|
||||||
<StyledExpression>
|
|
||||||
<WhenExpression
|
|
||||||
aggType={alertParams.metric ?? defaultExpression.metric}
|
|
||||||
onChangeSelectedAggType={updateMetric}
|
|
||||||
customAggTypesOptions={{
|
|
||||||
memory_usage: {
|
|
||||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.anomalyJobs.memoryUsage', {
|
|
||||||
defaultMessage: 'Memory usage',
|
|
||||||
}),
|
|
||||||
fieldRequired: false,
|
|
||||||
value: 'memory_usage',
|
|
||||||
validNormalizedTypes: [],
|
|
||||||
},
|
|
||||||
network_in: {
|
|
||||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.anomalyJobs.networkIn', {
|
|
||||||
defaultMessage: 'Network in',
|
|
||||||
}),
|
|
||||||
fieldRequired: false,
|
|
||||||
validNormalizedTypes: [],
|
|
||||||
value: 'network_in',
|
|
||||||
},
|
|
||||||
network_out: {
|
|
||||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.anomalyJobs.networkOut', {
|
|
||||||
defaultMessage: 'Network out',
|
|
||||||
}),
|
|
||||||
fieldRequired: false,
|
|
||||||
validNormalizedTypes: [],
|
|
||||||
value: 'network_out',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</StyledExpression>
|
|
||||||
<StyledExpression>
|
|
||||||
<SeverityThresholdExpression
|
|
||||||
value={alertParams.threshold ?? ANOMALY_THRESHOLD.CRITICAL}
|
|
||||||
onChange={updateSeverityThreshold}
|
|
||||||
/>
|
|
||||||
</StyledExpression>
|
|
||||||
</StyledExpressionRow>
|
|
||||||
<EuiSpacer size={'m'} />
|
|
||||||
<InfluencerFilter
|
|
||||||
derivedIndexPattern={derivedIndexPattern}
|
|
||||||
nodeType={alertParams.nodeType}
|
|
||||||
fieldName={influencerFieldName}
|
|
||||||
fieldValue={alertParams.influencerFilter?.fieldValue ?? ''}
|
|
||||||
onChangeFieldName={updateInfluencerFieldName}
|
|
||||||
onChangeFieldValue={updateInfluencerFieldValue}
|
|
||||||
/>
|
|
||||||
<EuiSpacer size={'m'} />
|
|
||||||
<AlertPreview
|
|
||||||
alertInterval={alertInterval}
|
|
||||||
alertThrottle={alertThrottle}
|
|
||||||
alertType={METRIC_ANOMALY_ALERT_TYPE_ID}
|
|
||||||
alertParams={pick(
|
|
||||||
alertParams,
|
|
||||||
'metric',
|
|
||||||
'threshold',
|
|
||||||
'nodeType',
|
|
||||||
'sourceId',
|
|
||||||
'influencerFilter'
|
|
||||||
)}
|
|
||||||
validate={validateMetricAnomaly}
|
|
||||||
/>
|
|
||||||
<EuiSpacer size={'m'} />
|
|
||||||
</EuiThemeProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// required for dynamic import
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
|
||||||
export default Expression;
|
|
||||||
|
|
||||||
const StyledExpressionRow = euiStyled(EuiFlexGroup)`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin: 0 -4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledExpression = euiStyled.div`
|
|
||||||
padding: 0 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const getDisplayNameForType = (type: InventoryItemType) => {
|
|
||||||
const inventoryModel = findInventoryModel(type);
|
|
||||||
return inventoryModel.displayName;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const nodeTypes: { [key: string]: any } = {
|
|
||||||
hosts: {
|
|
||||||
text: getDisplayNameForType('host'),
|
|
||||||
value: 'hosts',
|
|
||||||
},
|
|
||||||
k8s: {
|
|
||||||
text: getDisplayNameForType('pod'),
|
|
||||||
value: 'k8s',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMLMetricFromInventoryMetric = (metric: SnapshotMetricType) => {
|
|
||||||
switch (metric) {
|
|
||||||
case 'memory':
|
|
||||||
return 'memory_usage';
|
|
||||||
case 'tx':
|
|
||||||
return 'network_out';
|
|
||||||
case 'rx':
|
|
||||||
return 'network_in';
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMLNodeTypeFromInventoryNodeType = (nodeType: InventoryItemType) => {
|
|
||||||
switch (nodeType) {
|
|
||||||
case 'host':
|
|
||||||
return 'hosts';
|
|
||||||
case 'pod':
|
|
||||||
return 'k8s';
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,193 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
|
||||||
import { first } from 'lodash';
|
|
||||||
import { EuiFlexGroup, EuiFormRow, EuiCheckbox, EuiFlexItem, EuiSelect } from '@elastic/eui';
|
|
||||||
import {
|
|
||||||
MetricsExplorerKueryBar,
|
|
||||||
CurryLoadSuggestionsType,
|
|
||||||
} from '../../../pages/metrics/metrics_explorer/components/kuery_bar';
|
|
||||||
import { MetricAnomalyParams } from '../../../../common/alerting/metrics';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
fieldName: string;
|
|
||||||
fieldValue: string;
|
|
||||||
nodeType: MetricAnomalyParams['nodeType'];
|
|
||||||
onChangeFieldName: (v: string) => void;
|
|
||||||
onChangeFieldValue: (v: string) => void;
|
|
||||||
derivedIndexPattern: Parameters<typeof MetricsExplorerKueryBar>[0]['derivedIndexPattern'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const FILTER_TYPING_DEBOUNCE_MS = 500;
|
|
||||||
|
|
||||||
export const InfluencerFilter = ({
|
|
||||||
fieldName,
|
|
||||||
fieldValue,
|
|
||||||
nodeType,
|
|
||||||
onChangeFieldName,
|
|
||||||
onChangeFieldValue,
|
|
||||||
derivedIndexPattern,
|
|
||||||
}: Props) => {
|
|
||||||
const fieldNameOptions = useMemo(() => (nodeType === 'k8s' ? k8sFieldNames : hostFieldNames), [
|
|
||||||
nodeType,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// If initial props contain a fieldValue, assume it was passed in from loaded alertParams,
|
|
||||||
// and enable the UI element
|
|
||||||
const [isEnabled, updateIsEnabled] = useState(fieldValue ? true : false);
|
|
||||||
const [storedFieldValue, updateStoredFieldValue] = useState(fieldValue);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() =>
|
|
||||||
nodeType === 'k8s'
|
|
||||||
? onChangeFieldName(first(k8sFieldNames)!.value)
|
|
||||||
: onChangeFieldName(first(hostFieldNames)!.value),
|
|
||||||
[nodeType, onChangeFieldName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSelectFieldName = useCallback((e) => onChangeFieldName(e.target.value), [
|
|
||||||
onChangeFieldName,
|
|
||||||
]);
|
|
||||||
const onUpdateFieldValue = useCallback(
|
|
||||||
(value) => {
|
|
||||||
updateStoredFieldValue(value);
|
|
||||||
onChangeFieldValue(value);
|
|
||||||
},
|
|
||||||
[onChangeFieldValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleEnabled = useCallback(() => {
|
|
||||||
const nextState = !isEnabled;
|
|
||||||
updateIsEnabled(nextState);
|
|
||||||
if (!nextState) {
|
|
||||||
onChangeFieldValue('');
|
|
||||||
} else {
|
|
||||||
onChangeFieldValue(storedFieldValue);
|
|
||||||
}
|
|
||||||
}, [isEnabled, updateIsEnabled, onChangeFieldValue, storedFieldValue]);
|
|
||||||
|
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
||||||
const debouncedOnUpdateFieldValue = useCallback(
|
|
||||||
debounce(onUpdateFieldValue, FILTER_TYPING_DEBOUNCE_MS),
|
|
||||||
[onUpdateFieldValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
const affixFieldNameToQuery: CurryLoadSuggestionsType = (fn) => (
|
|
||||||
expression,
|
|
||||||
cursorPosition,
|
|
||||||
maxSuggestions
|
|
||||||
) => {
|
|
||||||
// Add the field name to the front of the passed-in query
|
|
||||||
const prefix = `${fieldName}:`;
|
|
||||||
// Trim whitespace to prevent AND/OR suggestions
|
|
||||||
const modifiedExpression = `${prefix}${expression}`.trim();
|
|
||||||
// Move the cursor position forward by the length of the field name
|
|
||||||
const modifiedPosition = cursorPosition + prefix.length;
|
|
||||||
return fn(modifiedExpression, modifiedPosition, maxSuggestions, (suggestions) =>
|
|
||||||
suggestions
|
|
||||||
.map((s) => ({
|
|
||||||
...s,
|
|
||||||
// Remove quotes from suggestions
|
|
||||||
text: s.text.replace(/\"/g, '').trim(),
|
|
||||||
// Offset the returned suggestions' cursor positions so that they can be autocompleted accurately
|
|
||||||
start: s.start - prefix.length,
|
|
||||||
end: s.end - prefix.length,
|
|
||||||
}))
|
|
||||||
// Removing quotes can lead to an already-selected suggestion still coming up in the autocomplete list,
|
|
||||||
// so filter these out
|
|
||||||
.filter((s) => !expression.startsWith(s.text))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiFormRow
|
|
||||||
label={
|
|
||||||
<EuiCheckbox
|
|
||||||
label={filterByNodeLabel}
|
|
||||||
id="anomalyAlertFilterByNodeCheckbox"
|
|
||||||
onChange={toggleEnabled}
|
|
||||||
checked={isEnabled}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
helpText={
|
|
||||||
isEnabled ? (
|
|
||||||
<>
|
|
||||||
{i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpText', {
|
|
||||||
defaultMessage:
|
|
||||||
'Limit the scope of your alert trigger to anomalies influenced by certain node(s).',
|
|
||||||
})}
|
|
||||||
<br />
|
|
||||||
{i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpTextExample', {
|
|
||||||
defaultMessage: 'For example: "my-node-1" or "my-node-*"',
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
display="rowCompressed"
|
|
||||||
>
|
|
||||||
{isEnabled ? (
|
|
||||||
<EuiFlexGroup>
|
|
||||||
<EuiFlexItem>
|
|
||||||
<EuiSelect
|
|
||||||
id="selectInfluencerFieldName"
|
|
||||||
value={fieldName}
|
|
||||||
onChange={onSelectFieldName}
|
|
||||||
options={fieldNameOptions}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
<EuiFlexItem>
|
|
||||||
<MetricsExplorerKueryBar
|
|
||||||
derivedIndexPattern={derivedIndexPattern}
|
|
||||||
onChange={debouncedOnUpdateFieldValue}
|
|
||||||
onSubmit={onUpdateFieldValue}
|
|
||||||
value={storedFieldValue}
|
|
||||||
curryLoadSuggestions={affixFieldNameToQuery}
|
|
||||||
placeholder={i18n.translate(
|
|
||||||
'xpack.infra.metrics.alertFlyout.anomalyInfluencerFilterPlaceholder',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Everything',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</EuiFormRow>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hostFieldNames = [
|
|
||||||
{
|
|
||||||
value: 'host.name',
|
|
||||||
text: 'host.name',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const k8sFieldNames = [
|
|
||||||
{
|
|
||||||
value: 'kubernetes.pod.uid',
|
|
||||||
text: 'kubernetes.pod.uid',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'kubernetes.node.name',
|
|
||||||
text: 'kubernetes.node.name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'kubernetes.namespace',
|
|
||||||
text: 'kubernetes.namespace',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const filterByNodeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.filterByNodeLabel', {
|
|
||||||
defaultMessage: 'Filter by node',
|
|
||||||
});
|
|
|
@ -1,117 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
|
||||||
import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui';
|
|
||||||
import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui';
|
|
||||||
import { MetricAnomalyParams } from '../../../../common/alerting/metrics';
|
|
||||||
|
|
||||||
type Node = MetricAnomalyParams['nodeType'];
|
|
||||||
|
|
||||||
interface WhenExpressionProps {
|
|
||||||
value: Node;
|
|
||||||
options: { [key: string]: { text: string; value: Node } };
|
|
||||||
onChange: (value: Node) => void;
|
|
||||||
popupPosition?:
|
|
||||||
| 'upCenter'
|
|
||||||
| 'upLeft'
|
|
||||||
| 'upRight'
|
|
||||||
| 'downCenter'
|
|
||||||
| 'downLeft'
|
|
||||||
| 'downRight'
|
|
||||||
| 'leftCenter'
|
|
||||||
| 'leftUp'
|
|
||||||
| 'leftDown'
|
|
||||||
| 'rightCenter'
|
|
||||||
| 'rightUp'
|
|
||||||
| 'rightDown';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NodeTypeExpression = ({
|
|
||||||
value,
|
|
||||||
options,
|
|
||||||
onChange,
|
|
||||||
popupPosition,
|
|
||||||
}: WhenExpressionProps) => {
|
|
||||||
const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiPopover
|
|
||||||
button={
|
|
||||||
<EuiExpression
|
|
||||||
data-test-subj="nodeTypeExpression"
|
|
||||||
description={i18n.translate(
|
|
||||||
'xpack.infra.metrics.alertFlyout.expression.for.descriptionLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'For',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
value={options[value].text}
|
|
||||||
isActive={aggTypePopoverOpen}
|
|
||||||
onClick={() => {
|
|
||||||
setAggTypePopoverOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isOpen={aggTypePopoverOpen}
|
|
||||||
closePopover={() => {
|
|
||||||
setAggTypePopoverOpen(false);
|
|
||||||
}}
|
|
||||||
ownFocus
|
|
||||||
anchorPosition={popupPosition ?? 'downLeft'}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<ClosablePopoverTitle onClose={() => setAggTypePopoverOpen(false)}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.infra.metrics.alertFlyout.expression.for.popoverTitle"
|
|
||||||
defaultMessage="Node Type"
|
|
||||||
/>
|
|
||||||
</ClosablePopoverTitle>
|
|
||||||
<EuiSelect
|
|
||||||
data-test-subj="forExpressionSelect"
|
|
||||||
value={value}
|
|
||||||
fullWidth
|
|
||||||
onChange={(e) => {
|
|
||||||
onChange(e.target.value as Node);
|
|
||||||
setAggTypePopoverOpen(false);
|
|
||||||
}}
|
|
||||||
options={Object.values(options).map((o) => o)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</EuiPopover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ClosablePopoverTitleProps {
|
|
||||||
children: JSX.Element;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => {
|
|
||||||
return (
|
|
||||||
<EuiPopoverTitle>
|
|
||||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
|
||||||
<EuiFlexItem>{children}</EuiFlexItem>
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiButtonIcon
|
|
||||||
iconType="cross"
|
|
||||||
color="danger"
|
|
||||||
aria-label={i18n.translate(
|
|
||||||
'xpack.infra.metrics.expressionItems.components.closablePopoverTitle.closeLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Close',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onClick={() => onClose()}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
</EuiPopoverTitle>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,140 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
|
||||||
import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui';
|
|
||||||
import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui';
|
|
||||||
import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml';
|
|
||||||
|
|
||||||
interface WhenExpressionProps {
|
|
||||||
value: Exclude<ANOMALY_THRESHOLD, ANOMALY_THRESHOLD.LOW>;
|
|
||||||
onChange: (value: ANOMALY_THRESHOLD) => void;
|
|
||||||
popupPosition?:
|
|
||||||
| 'upCenter'
|
|
||||||
| 'upLeft'
|
|
||||||
| 'upRight'
|
|
||||||
| 'downCenter'
|
|
||||||
| 'downLeft'
|
|
||||||
| 'downRight'
|
|
||||||
| 'leftCenter'
|
|
||||||
| 'leftUp'
|
|
||||||
| 'leftDown'
|
|
||||||
| 'rightCenter'
|
|
||||||
| 'rightUp'
|
|
||||||
| 'rightDown';
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
[ANOMALY_THRESHOLD.CRITICAL]: {
|
|
||||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.criticalLabel', {
|
|
||||||
defaultMessage: 'Critical',
|
|
||||||
}),
|
|
||||||
value: ANOMALY_THRESHOLD.CRITICAL,
|
|
||||||
},
|
|
||||||
[ANOMALY_THRESHOLD.MAJOR]: {
|
|
||||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.majorLabel', {
|
|
||||||
defaultMessage: 'Major',
|
|
||||||
}),
|
|
||||||
value: ANOMALY_THRESHOLD.MAJOR,
|
|
||||||
},
|
|
||||||
[ANOMALY_THRESHOLD.MINOR]: {
|
|
||||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.minorLabel', {
|
|
||||||
defaultMessage: 'Minor',
|
|
||||||
}),
|
|
||||||
value: ANOMALY_THRESHOLD.MINOR,
|
|
||||||
},
|
|
||||||
[ANOMALY_THRESHOLD.WARNING]: {
|
|
||||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.warningLabel', {
|
|
||||||
defaultMessage: 'Warning',
|
|
||||||
}),
|
|
||||||
value: ANOMALY_THRESHOLD.WARNING,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SeverityThresholdExpression = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
popupPosition,
|
|
||||||
}: WhenExpressionProps) => {
|
|
||||||
const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiPopover
|
|
||||||
button={
|
|
||||||
<EuiExpression
|
|
||||||
data-test-subj="nodeTypeExpression"
|
|
||||||
description={i18n.translate(
|
|
||||||
'xpack.infra.metrics.alertFlyout.expression.severityScore.descriptionLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Severity score is above',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
value={options[value].text}
|
|
||||||
isActive={aggTypePopoverOpen}
|
|
||||||
onClick={() => {
|
|
||||||
setAggTypePopoverOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isOpen={aggTypePopoverOpen}
|
|
||||||
closePopover={() => {
|
|
||||||
setAggTypePopoverOpen(false);
|
|
||||||
}}
|
|
||||||
ownFocus
|
|
||||||
anchorPosition={popupPosition ?? 'downLeft'}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<ClosablePopoverTitle onClose={() => setAggTypePopoverOpen(false)}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.infra.metrics.alertFlyout.expression.severityScore.popoverTitle"
|
|
||||||
defaultMessage="Severity Score"
|
|
||||||
/>
|
|
||||||
</ClosablePopoverTitle>
|
|
||||||
<EuiSelect
|
|
||||||
data-test-subj="severityExpressionSelect"
|
|
||||||
value={value}
|
|
||||||
fullWidth
|
|
||||||
onChange={(e) => {
|
|
||||||
onChange(Number(e.target.value) as ANOMALY_THRESHOLD);
|
|
||||||
setAggTypePopoverOpen(false);
|
|
||||||
}}
|
|
||||||
options={Object.values(options).map((o) => o)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</EuiPopover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ClosablePopoverTitleProps {
|
|
||||||
children: JSX.Element;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => {
|
|
||||||
return (
|
|
||||||
<EuiPopoverTitle>
|
|
||||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
|
||||||
<EuiFlexItem>{children}</EuiFlexItem>
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiButtonIcon
|
|
||||||
iconType="cross"
|
|
||||||
color="danger"
|
|
||||||
aria-label={i18n.translate(
|
|
||||||
'xpack.infra.metrics.expressionItems.components.closablePopoverTitle.closeLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Close',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onClick={() => onClose()}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
</EuiPopoverTitle>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
|
||||||
import { ValidationResult } from '../../../../../triggers_actions_ui/public/types';
|
|
||||||
|
|
||||||
export function validateMetricAnomaly({
|
|
||||||
hasInfraMLCapabilities,
|
|
||||||
}: {
|
|
||||||
hasInfraMLCapabilities: boolean;
|
|
||||||
}): ValidationResult {
|
|
||||||
const validationResult = { errors: {} };
|
|
||||||
const errors: {
|
|
||||||
hasInfraMLCapabilities: string[];
|
|
||||||
} = {
|
|
||||||
hasInfraMLCapabilities: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
validationResult.errors = errors;
|
|
||||||
|
|
||||||
if (!hasInfraMLCapabilities) {
|
|
||||||
errors.hasInfraMLCapabilities.push(
|
|
||||||
i18n.translate('xpack.infra.metrics.alertFlyout.error.mlCapabilitiesRequired', {
|
|
||||||
defaultMessage: 'Cannot create an anomaly alert when machine learning is disabled.',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return validationResult;
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import React from 'react';
|
|
||||||
import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../common/alerting/metrics';
|
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
|
||||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
|
|
||||||
import { AlertTypeParams } from '../../../../alerts/common';
|
|
||||||
import { validateMetricAnomaly } from './components/validation';
|
|
||||||
|
|
||||||
interface MetricAnomalyAlertTypeParams extends AlertTypeParams {
|
|
||||||
hasInfraMLCapabilities: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMetricAnomalyAlertType(): AlertTypeModel<MetricAnomalyAlertTypeParams> {
|
|
||||||
return {
|
|
||||||
id: METRIC_ANOMALY_ALERT_TYPE_ID,
|
|
||||||
description: i18n.translate('xpack.infra.metrics.anomaly.alertFlyout.alertDescription', {
|
|
||||||
defaultMessage: 'Alert when the anomaly score exceeds a defined threshold.',
|
|
||||||
}),
|
|
||||||
iconClass: 'bell',
|
|
||||||
documentationUrl(docLinks) {
|
|
||||||
return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/metric-anomaly-alert.html`;
|
|
||||||
},
|
|
||||||
alertParamsExpression: React.lazy(() => import('./components/expression')),
|
|
||||||
validate: validateMetricAnomaly,
|
|
||||||
defaultActionMessage: i18n.translate(
|
|
||||||
'xpack.infra.metrics.alerting.anomaly.defaultActionMessage',
|
|
||||||
{
|
|
||||||
defaultMessage: `\\{\\{alertName\\}\\} is in a state of \\{\\{context.alertState\\}\\}
|
|
||||||
|
|
||||||
\\{\\{context.metric\\}\\} was \\{\\{context.summary\\}\\} than normal at \\{\\{context.timestamp\\}\\}
|
|
||||||
|
|
||||||
Typical value: \\{\\{context.typical\\}\\}
|
|
||||||
Actual value: \\{\\{context.actual\\}\\}
|
|
||||||
`,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
requiresAppContext: false,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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 React, { useState, useCallback } from 'react';
|
||||||
|
import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { useAlertPrefillContext } from '../../use_alert_prefill';
|
||||||
|
import { AlertFlyout } from './alert_flyout';
|
||||||
|
import { ManageAlertsContextMenuItem } from '../../inventory/components/manage_alerts_context_menu_item';
|
||||||
|
|
||||||
|
export const MetricsAlertDropdown = () => {
|
||||||
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
const [flyoutVisible, setFlyoutVisible] = useState(false);
|
||||||
|
|
||||||
|
const { metricThresholdPrefill } = useAlertPrefillContext();
|
||||||
|
const { groupBy, filterQuery, metrics } = metricThresholdPrefill;
|
||||||
|
|
||||||
|
const closePopover = useCallback(() => {
|
||||||
|
setPopoverOpen(false);
|
||||||
|
}, [setPopoverOpen]);
|
||||||
|
|
||||||
|
const openPopover = useCallback(() => {
|
||||||
|
setPopoverOpen(true);
|
||||||
|
}, [setPopoverOpen]);
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
<EuiContextMenuItem icon="bell" key="createLink" onClick={() => setFlyoutVisible(true)}>
|
||||||
|
<FormattedMessage id="xpack.infra.alerting.createAlertButton" defaultMessage="Create alert" />
|
||||||
|
</EuiContextMenuItem>,
|
||||||
|
<ManageAlertsContextMenuItem />,
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiPopover
|
||||||
|
button={
|
||||||
|
<EuiButtonEmpty iconSide={'right'} iconType={'arrowDown'} onClick={openPopover}>
|
||||||
|
<FormattedMessage id="xpack.infra.alerting.alertsButton" defaultMessage="Alerts" />
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
}
|
||||||
|
isOpen={popoverOpen}
|
||||||
|
closePopover={closePopover}
|
||||||
|
>
|
||||||
|
<EuiContextMenuPanel items={menuItems} />
|
||||||
|
</EuiPopover>
|
||||||
|
<AlertFlyout
|
||||||
|
setVisible={setFlyoutVisible}
|
||||||
|
visible={flyoutVisible}
|
||||||
|
options={{ groupBy, filterQuery, metrics }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -7,10 +7,10 @@
|
||||||
|
|
||||||
import React, { useCallback, useContext, useMemo } from 'react';
|
import React, { useCallback, useContext, useMemo } from 'react';
|
||||||
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
|
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
|
||||||
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics';
|
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||||
|
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types';
|
||||||
import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
|
import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
|
||||||
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
||||||
import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
|
@ -42,10 +42,3 @@ export const AlertFlyout = (props: Props) => {
|
||||||
|
|
||||||
return <>{visible && AddAlertFlyout}</>;
|
return <>{visible && AddAlertFlyout}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PrefilledThresholdAlertFlyout = ({ onClose }: { onClose(): void }) => {
|
|
||||||
const { metricThresholdPrefill } = useAlertPrefillContext();
|
|
||||||
const { groupBy, filterQuery, metrics } = metricThresholdPrefill;
|
|
||||||
|
|
||||||
return <AlertFlyout options={{ groupBy, filterQuery, metrics }} visible setVisible={onClose} />;
|
|
||||||
};
|
|
||||||
|
|
|
@ -14,3 +14,4 @@ export * from './missing_results_privileges_prompt';
|
||||||
export * from './missing_setup_privileges_prompt';
|
export * from './missing_setup_privileges_prompt';
|
||||||
export * from './ml_unavailable_prompt';
|
export * from './ml_unavailable_prompt';
|
||||||
export * from './setup_status_unknown_prompt';
|
export * from './setup_status_unknown_prompt';
|
||||||
|
export * from './subscription_splash_content';
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
/*
|
||||||
|
* 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, { useEffect } from 'react';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import {
|
||||||
|
EuiPage,
|
||||||
|
EuiPageBody,
|
||||||
|
EuiPageContent,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiSpacer,
|
||||||
|
EuiTitle,
|
||||||
|
EuiText,
|
||||||
|
EuiButton,
|
||||||
|
EuiButtonEmpty,
|
||||||
|
EuiImage,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { HttpStart } from 'src/core/public';
|
||||||
|
import { LoadingPage } from '../../loading_page';
|
||||||
|
|
||||||
|
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||||
|
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
|
||||||
|
import { useTrialStatus } from '../../../hooks/use_trial_status';
|
||||||
|
|
||||||
|
export const SubscriptionSplashContent: React.FC = () => {
|
||||||
|
const { services } = useKibana<{ http: HttpStart }>();
|
||||||
|
const { loadState, isTrialAvailable, checkTrialAvailability } = useTrialStatus();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkTrialAvailability();
|
||||||
|
}, [checkTrialAvailability]);
|
||||||
|
|
||||||
|
if (loadState === 'pending') {
|
||||||
|
return (
|
||||||
|
<LoadingPage
|
||||||
|
message={i18n.translate('xpack.infra.logs.logAnalysis.splash.loadingMessage', {
|
||||||
|
defaultMessage: 'Checking license...',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canStartTrial = isTrialAvailable && loadState === 'resolved';
|
||||||
|
|
||||||
|
let title;
|
||||||
|
let description;
|
||||||
|
let cta;
|
||||||
|
|
||||||
|
if (canStartTrial) {
|
||||||
|
title = (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.infra.logs.logAnalysis.splash.startTrialTitle"
|
||||||
|
defaultMessage="To access anomaly detection, start a free trial"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
description = (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.infra.logs.logAnalysis.splash.startTrialDescription"
|
||||||
|
defaultMessage="Our free trial includes machine learning features, which enable you to detect anomalies in your logs."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
cta = (
|
||||||
|
<EuiButton
|
||||||
|
fullWidth={false}
|
||||||
|
fill
|
||||||
|
href={services.http.basePath.prepend('/app/management/stack/license_management')}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.infra.logs.logAnalysis.splash.startTrialCta"
|
||||||
|
defaultMessage="Start trial"
|
||||||
|
/>
|
||||||
|
</EuiButton>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
title = (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle"
|
||||||
|
defaultMessage="To access anomaly detection, upgrade to a Platinum Subscription"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
description = (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription"
|
||||||
|
defaultMessage="You must have a Platinum Subscription to use machine learning features."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
cta = (
|
||||||
|
<EuiButton fullWidth={false} fill href="https://www.elastic.co/subscriptions">
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta"
|
||||||
|
defaultMessage="Upgrade subscription"
|
||||||
|
/>
|
||||||
|
</EuiButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SubscriptionPage>
|
||||||
|
<EuiPageBody>
|
||||||
|
<SubscriptionPageContent verticalPosition="center" horizontalPosition="center">
|
||||||
|
<EuiFlexGroup>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiTitle size="m">
|
||||||
|
<h2>{title}</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
<EuiSpacer size="xl" />
|
||||||
|
<EuiText>
|
||||||
|
<p>{description}</p>
|
||||||
|
</EuiText>
|
||||||
|
<EuiSpacer />
|
||||||
|
<div>{cta}</div>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiImage
|
||||||
|
alt={i18n.translate('xpack.infra.logs.logAnalysis.splash.splashImageAlt', {
|
||||||
|
defaultMessage: 'Placeholder image',
|
||||||
|
})}
|
||||||
|
url={services.http.basePath.prepend(
|
||||||
|
'/plugins/infra/assets/anomaly_chart_minified.svg'
|
||||||
|
)}
|
||||||
|
size="fullWidth"
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
<SubscriptionPageFooter>
|
||||||
|
<EuiTitle size="xs">
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.infra.logs.logAnalysis.splash.learnMoreTitle"
|
||||||
|
defaultMessage="Want to learn more?"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
</EuiTitle>
|
||||||
|
<EuiButtonEmpty
|
||||||
|
flush="left"
|
||||||
|
iconType="training"
|
||||||
|
target="_blank"
|
||||||
|
color="text"
|
||||||
|
href="https://www.elastic.co/guide/en/kibana/master/xpack-logs-analysis.html"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.infra.logs.logAnalysis.splash.learnMoreLink"
|
||||||
|
defaultMessage="Read documentation"
|
||||||
|
/>
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</SubscriptionPageFooter>
|
||||||
|
</SubscriptionPageContent>
|
||||||
|
</EuiPageBody>
|
||||||
|
</SubscriptionPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubscriptionPage = euiStyled(EuiPage)`
|
||||||
|
height: 100%
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SubscriptionPageContent = euiStyled(EuiPageContent)`
|
||||||
|
max-width: 768px !important;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SubscriptionPageFooter = euiStyled.div`
|
||||||
|
background: ${(props) => props.theme.eui.euiColorLightestShade};
|
||||||
|
margin: 0 -${(props) => props.theme.eui.paddingSizes.l} -${(props) =>
|
||||||
|
props.theme.eui.paddingSizes.l};
|
||||||
|
padding: ${(props) => props.theme.eui.paddingSizes.l};
|
||||||
|
`;
|
|
@ -75,7 +75,7 @@ export const SourceConfigurationSettings = ({
|
||||||
source,
|
source,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { hasInfraMLCapabilities } = useInfraMLCapabilitiesContext();
|
const { hasInfraMLCapabilites } = useInfraMLCapabilitiesContext();
|
||||||
|
|
||||||
if ((isLoading || isUninitialized) && !source) {
|
if ((isLoading || isUninitialized) && !source) {
|
||||||
return <SourceLoadingPage />;
|
return <SourceLoadingPage />;
|
||||||
|
@ -128,7 +128,7 @@ export const SourceConfigurationSettings = ({
|
||||||
/>
|
/>
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
{hasInfraMLCapabilities && (
|
{hasInfraMLCapabilites && (
|
||||||
<>
|
<>
|
||||||
<EuiPanel paddingSize="l">
|
<EuiPanel paddingSize="l">
|
||||||
<MLConfigurationPanel
|
<MLConfigurationPanel
|
||||||
|
|
|
@ -52,11 +52,11 @@ export const useInfraMLCapabilities = () => {
|
||||||
|
|
||||||
const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob;
|
const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob;
|
||||||
const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs;
|
const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs;
|
||||||
const hasInfraMLCapabilities =
|
const hasInfraMLCapabilites =
|
||||||
mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace;
|
mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasInfraMLCapabilities,
|
hasInfraMLCapabilites,
|
||||||
hasInfraMLReadCapabilities,
|
hasInfraMLReadCapabilities,
|
||||||
hasInfraMLSetupCapabilities,
|
hasInfraMLSetupCapabilities,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|
|
@ -56,8 +56,7 @@ class WithKueryAutocompletionComponent extends React.Component<
|
||||||
private loadSuggestions = async (
|
private loadSuggestions = async (
|
||||||
expression: string,
|
expression: string,
|
||||||
cursorPosition: number,
|
cursorPosition: number,
|
||||||
maxSuggestions?: number,
|
maxSuggestions?: number
|
||||||
transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[]
|
|
||||||
) => {
|
) => {
|
||||||
const { indexPattern } = this.props;
|
const { indexPattern } = this.props;
|
||||||
const language = 'kuery';
|
const language = 'kuery';
|
||||||
|
@ -87,10 +86,6 @@ class WithKueryAutocompletionComponent extends React.Component<
|
||||||
boolFilter: [],
|
boolFilter: [],
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const transformedSuggestions = transformSuggestions
|
|
||||||
? transformSuggestions(suggestions)
|
|
||||||
: suggestions;
|
|
||||||
|
|
||||||
this.setState((state) =>
|
this.setState((state) =>
|
||||||
state.currentRequest &&
|
state.currentRequest &&
|
||||||
state.currentRequest.expression !== expression &&
|
state.currentRequest.expression !== expression &&
|
||||||
|
@ -99,9 +94,7 @@ class WithKueryAutocompletionComponent extends React.Component<
|
||||||
: {
|
: {
|
||||||
...state,
|
...state,
|
||||||
currentRequest: null,
|
currentRequest: null,
|
||||||
suggestions: maxSuggestions
|
suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions,
|
||||||
? transformedSuggestions.slice(0, maxSuggestions)
|
|
||||||
: transformedSuggestions,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,13 +7,13 @@
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
|
|
||||||
import { isJobStatusWithResults } from '../../../../common/log_analysis';
|
import { isJobStatusWithResults } from '../../../../common/log_analysis';
|
||||||
import { LoadingPage } from '../../../components/loading_page';
|
import { LoadingPage } from '../../../components/loading_page';
|
||||||
import {
|
import {
|
||||||
LogAnalysisSetupStatusUnknownPrompt,
|
LogAnalysisSetupStatusUnknownPrompt,
|
||||||
MissingResultsPrivilegesPrompt,
|
MissingResultsPrivilegesPrompt,
|
||||||
MissingSetupPrivilegesPrompt,
|
MissingSetupPrivilegesPrompt,
|
||||||
|
SubscriptionSplashContent,
|
||||||
} from '../../../components/logging/log_analysis_setup';
|
} from '../../../components/logging/log_analysis_setup';
|
||||||
import {
|
import {
|
||||||
LogAnalysisSetupFlyout,
|
LogAnalysisSetupFlyout,
|
||||||
|
|
|
@ -7,13 +7,13 @@
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React, { memo, useEffect, useCallback } from 'react';
|
import React, { memo, useEffect, useCallback } from 'react';
|
||||||
import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
|
|
||||||
import { isJobStatusWithResults } from '../../../../common/log_analysis';
|
import { isJobStatusWithResults } from '../../../../common/log_analysis';
|
||||||
import { LoadingPage } from '../../../components/loading_page';
|
import { LoadingPage } from '../../../components/loading_page';
|
||||||
import {
|
import {
|
||||||
LogAnalysisSetupStatusUnknownPrompt,
|
LogAnalysisSetupStatusUnknownPrompt,
|
||||||
MissingResultsPrivilegesPrompt,
|
MissingResultsPrivilegesPrompt,
|
||||||
MissingSetupPrivilegesPrompt,
|
MissingSetupPrivilegesPrompt,
|
||||||
|
SubscriptionSplashContent,
|
||||||
} from '../../../components/logging/log_analysis_setup';
|
} from '../../../components/logging/log_analysis_setup';
|
||||||
import {
|
import {
|
||||||
LogAnalysisSetupFlyout,
|
LogAnalysisSetupFlyout,
|
||||||
|
|
|
@ -35,11 +35,12 @@ import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options
|
||||||
import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time';
|
import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time';
|
||||||
import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters';
|
import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters';
|
||||||
|
|
||||||
import { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown';
|
import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown';
|
||||||
|
import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown';
|
||||||
import { SavedView } from '../../containers/saved_view/saved_view';
|
import { SavedView } from '../../containers/saved_view/saved_view';
|
||||||
import { AlertPrefillProvider } from '../../alerting/use_alert_prefill';
|
import { AlertPrefillProvider } from '../../alerting/use_alert_prefill';
|
||||||
import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities';
|
import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities';
|
||||||
import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout';
|
import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout';
|
||||||
import { HeaderMenuPortal } from '../../../../observability/public';
|
import { HeaderMenuPortal } from '../../../../observability/public';
|
||||||
import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider';
|
import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider';
|
||||||
|
|
||||||
|
@ -82,7 +83,8 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
|
||||||
<Route path={'/inventory'} component={AnomalyDetectionFlyout} />
|
<Route path={'/inventory'} component={AnomalyDetectionFlyout} />
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<MetricsAlertDropdown />
|
<Route path={'/explorer'} component={MetricsAlertDropdown} />
|
||||||
|
<Route path={'/inventory'} component={InventoryAlertDropdown} />
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiButtonEmpty
|
<EuiButtonEmpty
|
||||||
|
|
|
@ -14,8 +14,8 @@ import { EuiCallOut } from '@elastic/eui';
|
||||||
import { EuiButton } from '@elastic/eui';
|
import { EuiButton } from '@elastic/eui';
|
||||||
import { EuiButtonEmpty } from '@elastic/eui';
|
import { EuiButtonEmpty } from '@elastic/eui';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { SubscriptionSplashContent } from '../../../../../../components/subscription_splash_content';
|
|
||||||
import { useInfraMLCapabilitiesContext } from '../../../../../../containers/ml/infra_ml_capabilities';
|
import { useInfraMLCapabilitiesContext } from '../../../../../../containers/ml/infra_ml_capabilities';
|
||||||
|
import { SubscriptionSplashContent } from './subscription_splash_content';
|
||||||
import {
|
import {
|
||||||
MissingResultsPrivilegesPrompt,
|
MissingResultsPrivilegesPrompt,
|
||||||
MissingSetupPrivilegesPrompt,
|
MissingSetupPrivilegesPrompt,
|
||||||
|
@ -44,7 +44,7 @@ export const FlyoutHome = (props: Props) => {
|
||||||
jobSummaries: k8sJobSummaries,
|
jobSummaries: k8sJobSummaries,
|
||||||
} = useMetricK8sModuleContext();
|
} = useMetricK8sModuleContext();
|
||||||
const {
|
const {
|
||||||
hasInfraMLCapabilities,
|
hasInfraMLCapabilites,
|
||||||
hasInfraMLReadCapabilities,
|
hasInfraMLReadCapabilities,
|
||||||
hasInfraMLSetupCapabilities,
|
hasInfraMLSetupCapabilities,
|
||||||
} = useInfraMLCapabilitiesContext();
|
} = useInfraMLCapabilitiesContext();
|
||||||
|
@ -69,7 +69,7 @@ export const FlyoutHome = (props: Props) => {
|
||||||
}
|
}
|
||||||
}, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]);
|
}, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]);
|
||||||
|
|
||||||
if (!hasInfraMLCapabilities) {
|
if (!hasInfraMLCapabilites) {
|
||||||
return <SubscriptionSplashContent />;
|
return <SubscriptionSplashContent />;
|
||||||
} else if (!hasInfraMLReadCapabilities) {
|
} else if (!hasInfraMLReadCapabilities) {
|
||||||
return <MissingResultsPrivilegesPrompt />;
|
return <MissingResultsPrivilegesPrompt />;
|
||||||
|
|
|
@ -22,11 +22,11 @@ import {
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
|
||||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
import { LoadingPage } from '../../../../../../components/loading_page';
|
||||||
import { euiStyled, EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
|
import { useTrialStatus } from '../../../../../../hooks/use_trial_status';
|
||||||
import { HttpStart } from '../../../../../src/core/public';
|
import { useKibana } from '../../../../../../../../../../src/plugins/kibana_react/public';
|
||||||
import { useTrialStatus } from '../hooks/use_trial_status';
|
import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
|
||||||
import { LoadingPage } from '../components/loading_page';
|
import { HttpStart } from '../../../../../../../../../../src/core/public';
|
||||||
|
|
||||||
export const SubscriptionSplashContent: React.FC = () => {
|
export const SubscriptionSplashContent: React.FC = () => {
|
||||||
const { services } = useKibana<{ http: HttpStart }>();
|
const { services } = useKibana<{ http: HttpStart }>();
|
||||||
|
@ -102,60 +102,58 @@ export const SubscriptionSplashContent: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiThemeProvider>
|
<SubscriptionPage>
|
||||||
<SubscriptionPage>
|
<EuiPageBody>
|
||||||
<EuiPageBody>
|
<SubscriptionPageContent verticalPosition="center" horizontalPosition="center">
|
||||||
<SubscriptionPageContent verticalPosition="center" horizontalPosition="center">
|
<EuiFlexGroup>
|
||||||
<EuiFlexGroup>
|
<EuiFlexItem>
|
||||||
<EuiFlexItem>
|
<EuiTitle size="m">
|
||||||
<EuiTitle size="m">
|
<h2>{title}</h2>
|
||||||
<h2>{title}</h2>
|
|
||||||
</EuiTitle>
|
|
||||||
<EuiSpacer size="xl" />
|
|
||||||
<EuiText>
|
|
||||||
<p>{description}</p>
|
|
||||||
</EuiText>
|
|
||||||
<EuiSpacer />
|
|
||||||
<div>{cta}</div>
|
|
||||||
</EuiFlexItem>
|
|
||||||
<EuiFlexItem>
|
|
||||||
<EuiImage
|
|
||||||
alt={i18n.translate('xpack.infra.ml.splash.splashImageAlt', {
|
|
||||||
defaultMessage: 'Placeholder image',
|
|
||||||
})}
|
|
||||||
url={services.http.basePath.prepend(
|
|
||||||
'/plugins/infra/assets/anomaly_chart_minified.svg'
|
|
||||||
)}
|
|
||||||
size="fullWidth"
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
<SubscriptionPageFooter>
|
|
||||||
<EuiTitle size="xs">
|
|
||||||
<h3>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.infra.ml.splash.learnMoreTitle"
|
|
||||||
defaultMessage="Want to learn more?"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
<EuiButtonEmpty
|
<EuiSpacer size="xl" />
|
||||||
flush="left"
|
<EuiText>
|
||||||
iconType="training"
|
<p>{description}</p>
|
||||||
target="_blank"
|
</EuiText>
|
||||||
color="text"
|
<EuiSpacer />
|
||||||
href="https://www.elastic.co/guide/en/kibana/master/xpack-logs-analysis.html"
|
<div>{cta}</div>
|
||||||
>
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiImage
|
||||||
|
alt={i18n.translate('xpack.infra.ml.splash.splashImageAlt', {
|
||||||
|
defaultMessage: 'Placeholder image',
|
||||||
|
})}
|
||||||
|
url={services.http.basePath.prepend(
|
||||||
|
'/plugins/infra/assets/anomaly_chart_minified.svg'
|
||||||
|
)}
|
||||||
|
size="fullWidth"
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
<SubscriptionPageFooter>
|
||||||
|
<EuiTitle size="xs">
|
||||||
|
<h3>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.infra.ml.splash.learnMoreLink"
|
id="xpack.infra.ml.splash.learnMoreTitle"
|
||||||
defaultMessage="Read documentation"
|
defaultMessage="Want to learn more?"
|
||||||
/>
|
/>
|
||||||
</EuiButtonEmpty>
|
</h3>
|
||||||
</SubscriptionPageFooter>
|
</EuiTitle>
|
||||||
</SubscriptionPageContent>
|
<EuiButtonEmpty
|
||||||
</EuiPageBody>
|
flush="left"
|
||||||
</SubscriptionPage>
|
iconType="training"
|
||||||
</EuiThemeProvider>
|
target="_blank"
|
||||||
|
color="text"
|
||||||
|
href="https://www.elastic.co/guide/en/kibana/master/xpack-logs-analysis.html"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.infra.ml.splash.learnMoreLink"
|
||||||
|
defaultMessage="Read documentation"
|
||||||
|
/>
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</SubscriptionPageFooter>
|
||||||
|
</SubscriptionPageContent>
|
||||||
|
</EuiPageBody>
|
||||||
|
</SubscriptionPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,19 +10,7 @@ import { i18n } from '@kbn/i18n';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion';
|
import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion';
|
||||||
import { AutocompleteField } from '../../../../components/autocomplete_field';
|
import { AutocompleteField } from '../../../../components/autocomplete_field';
|
||||||
import {
|
import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public';
|
||||||
esKuery,
|
|
||||||
IIndexPattern,
|
|
||||||
QuerySuggestion,
|
|
||||||
} from '../../../../../../../../src/plugins/data/public';
|
|
||||||
|
|
||||||
type LoadSuggestionsFn = (
|
|
||||||
e: string,
|
|
||||||
p: number,
|
|
||||||
m?: number,
|
|
||||||
transform?: (s: QuerySuggestion[]) => QuerySuggestion[]
|
|
||||||
) => void;
|
|
||||||
export type CurryLoadSuggestionsType = (loadSuggestions: LoadSuggestionsFn) => LoadSuggestionsFn;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
derivedIndexPattern: IIndexPattern;
|
derivedIndexPattern: IIndexPattern;
|
||||||
|
@ -30,7 +18,6 @@ interface Props {
|
||||||
onChange?: (query: string) => void;
|
onChange?: (query: string) => void;
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
curryLoadSuggestions?: CurryLoadSuggestionsType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateQuery(query: string) {
|
function validateQuery(query: string) {
|
||||||
|
@ -48,7 +35,6 @@ export const MetricsExplorerKueryBar = ({
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value,
|
||||||
placeholder,
|
placeholder,
|
||||||
curryLoadSuggestions = defaultCurryLoadSuggestions,
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [draftQuery, setDraftQuery] = useState<string>(value || '');
|
const [draftQuery, setDraftQuery] = useState<string>(value || '');
|
||||||
const [isValid, setValidation] = useState<boolean>(true);
|
const [isValid, setValidation] = useState<boolean>(true);
|
||||||
|
@ -87,7 +73,7 @@ export const MetricsExplorerKueryBar = ({
|
||||||
aria-label={placeholder}
|
aria-label={placeholder}
|
||||||
isLoadingSuggestions={isLoadingSuggestions}
|
isLoadingSuggestions={isLoadingSuggestions}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
loadSuggestions={curryLoadSuggestions(loadSuggestions)}
|
loadSuggestions={loadSuggestions}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
placeholder={placeholder || defaultPlaceholder}
|
placeholder={placeholder || defaultPlaceholder}
|
||||||
|
@ -98,6 +84,3 @@ export const MetricsExplorerKueryBar = ({
|
||||||
</WithKueryAutocompletion>
|
</WithKueryAutocompletion>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultCurryLoadSuggestions: CurryLoadSuggestionsType = (loadSuggestions) => (...args) =>
|
|
||||||
loadSuggestions(...args);
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public';
|
||||||
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
|
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
|
||||||
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
|
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
|
||||||
import { createInventoryMetricAlertType } from './alerting/inventory';
|
import { createInventoryMetricAlertType } from './alerting/inventory';
|
||||||
import { createMetricAnomalyAlertType } from './alerting/metric_anomaly';
|
|
||||||
import { getAlertType as getLogsAlertType } from './alerting/log_threshold';
|
import { getAlertType as getLogsAlertType } from './alerting/log_threshold';
|
||||||
import { registerFeatures } from './register_feature';
|
import { registerFeatures } from './register_feature';
|
||||||
import {
|
import {
|
||||||
|
@ -36,7 +35,6 @@ export class Plugin implements InfraClientPluginClass {
|
||||||
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType());
|
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType());
|
||||||
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(getLogsAlertType());
|
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(getLogsAlertType());
|
||||||
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType());
|
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType());
|
||||||
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricAnomalyAlertType());
|
|
||||||
|
|
||||||
if (pluginsSetup.observability) {
|
if (pluginsSetup.observability) {
|
||||||
pluginsSetup.observability.dashboard.register({
|
pluginsSetup.observability.dashboard.register({
|
||||||
|
|
|
@ -23,7 +23,7 @@ import type {
|
||||||
ObservabilityPluginStart,
|
ObservabilityPluginStart,
|
||||||
} from '../../observability/public';
|
} from '../../observability/public';
|
||||||
import type { SpacesPluginStart } from '../../spaces/public';
|
import type { SpacesPluginStart } from '../../spaces/public';
|
||||||
import { MlPluginStart, MlPluginSetup } from '../../ml/public';
|
import { MlPluginStart } from '../../ml/public';
|
||||||
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
|
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
|
||||||
|
|
||||||
// Our own setup and start contract values
|
// Our own setup and start contract values
|
||||||
|
@ -36,7 +36,6 @@ export interface InfraClientSetupDeps {
|
||||||
observability: ObservabilityPluginSetup;
|
observability: ObservabilityPluginSetup;
|
||||||
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
|
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
|
||||||
usageCollection: UsageCollectionSetup;
|
usageCollection: UsageCollectionSetup;
|
||||||
ml: MlPluginSetup;
|
|
||||||
embeddable: EmbeddableSetup;
|
embeddable: EmbeddableSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MetricAnomalyParams } from '../../../../common/alerting/metrics';
|
|
||||||
import { getMetricsHostsAnomalies, getMetricK8sAnomalies } from '../../infra_ml';
|
|
||||||
import { MlSystem, MlAnomalyDetectors } from '../../../types';
|
|
||||||
|
|
||||||
type ConditionParams = Omit<MetricAnomalyParams, 'alertInterval'> & {
|
|
||||||
spaceId: string;
|
|
||||||
startTime: number;
|
|
||||||
endTime: number;
|
|
||||||
mlSystem: MlSystem;
|
|
||||||
mlAnomalyDetectors: MlAnomalyDetectors;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const evaluateCondition = async ({
|
|
||||||
nodeType,
|
|
||||||
spaceId,
|
|
||||||
sourceId,
|
|
||||||
mlSystem,
|
|
||||||
mlAnomalyDetectors,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
metric,
|
|
||||||
threshold,
|
|
||||||
influencerFilter,
|
|
||||||
}: ConditionParams) => {
|
|
||||||
const getAnomalies = nodeType === 'k8s' ? getMetricK8sAnomalies : getMetricsHostsAnomalies;
|
|
||||||
|
|
||||||
const result = await getAnomalies(
|
|
||||||
{
|
|
||||||
spaceId,
|
|
||||||
mlSystem,
|
|
||||||
mlAnomalyDetectors,
|
|
||||||
},
|
|
||||||
sourceId ?? 'default',
|
|
||||||
threshold,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
metric,
|
|
||||||
{ field: 'anomalyScore', direction: 'desc' },
|
|
||||||
{ pageSize: 100 },
|
|
||||||
influencerFilter
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
|
@ -1,142 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { first } from 'lodash';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { stateToAlertMessage } from '../common/messages';
|
|
||||||
import { MetricAnomalyParams } from '../../../../common/alerting/metrics';
|
|
||||||
import { MappedAnomalyHit } from '../../infra_ml';
|
|
||||||
import { AlertStates } from '../common/types';
|
|
||||||
import {
|
|
||||||
ActionGroup,
|
|
||||||
AlertInstanceContext,
|
|
||||||
AlertInstanceState,
|
|
||||||
} from '../../../../../alerts/common';
|
|
||||||
import { AlertExecutorOptions } from '../../../../../alerts/server';
|
|
||||||
import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
|
|
||||||
import { MetricAnomalyAllowedActionGroups } from './register_metric_anomaly_alert_type';
|
|
||||||
import { MlPluginSetup } from '../../../../../ml/server';
|
|
||||||
import { KibanaRequest } from '../../../../../../../src/core/server';
|
|
||||||
import { InfraBackendLibs } from '../../infra_types';
|
|
||||||
import { evaluateCondition } from './evaluate_condition';
|
|
||||||
|
|
||||||
export const createMetricAnomalyExecutor = (libs: InfraBackendLibs, ml?: MlPluginSetup) => async ({
|
|
||||||
services,
|
|
||||||
params,
|
|
||||||
startedAt,
|
|
||||||
}: AlertExecutorOptions<
|
|
||||||
/**
|
|
||||||
* TODO: Remove this use of `any` by utilizing a proper type
|
|
||||||
*/
|
|
||||||
Record<string, any>,
|
|
||||||
Record<string, any>,
|
|
||||||
AlertInstanceState,
|
|
||||||
AlertInstanceContext,
|
|
||||||
MetricAnomalyAllowedActionGroups
|
|
||||||
>) => {
|
|
||||||
if (!ml) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const request = {} as KibanaRequest;
|
|
||||||
const mlSystem = ml.mlSystemProvider(request, services.savedObjectsClient);
|
|
||||||
const mlAnomalyDetectors = ml.anomalyDetectorsProvider(request, services.savedObjectsClient);
|
|
||||||
|
|
||||||
const {
|
|
||||||
metric,
|
|
||||||
alertInterval,
|
|
||||||
influencerFilter,
|
|
||||||
sourceId,
|
|
||||||
nodeType,
|
|
||||||
threshold,
|
|
||||||
} = params as MetricAnomalyParams;
|
|
||||||
|
|
||||||
const alertInstance = services.alertInstanceFactory(`${nodeType}-${metric}`);
|
|
||||||
|
|
||||||
const bucketInterval = getIntervalInSeconds('15m') * 1000;
|
|
||||||
const alertIntervalInMs = getIntervalInSeconds(alertInterval ?? '1m') * 1000;
|
|
||||||
|
|
||||||
const endTime = startedAt.getTime();
|
|
||||||
// Anomalies are bucketed at :00, :15, :30, :45 minutes every hour
|
|
||||||
const previousBucketStartTime = endTime - (endTime % bucketInterval);
|
|
||||||
|
|
||||||
// If the alert interval is less than 15m, make sure that it actually queries an anomaly bucket
|
|
||||||
const startTime = Math.min(endTime - alertIntervalInMs, previousBucketStartTime);
|
|
||||||
|
|
||||||
const { data } = await evaluateCondition({
|
|
||||||
sourceId: sourceId ?? 'default',
|
|
||||||
spaceId: 'default',
|
|
||||||
mlSystem,
|
|
||||||
mlAnomalyDetectors,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
metric,
|
|
||||||
threshold,
|
|
||||||
nodeType,
|
|
||||||
influencerFilter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const shouldAlertFire = data.length > 0;
|
|
||||||
|
|
||||||
if (shouldAlertFire) {
|
|
||||||
const { startTime: anomalyStartTime, anomalyScore, actual, typical, influencers } = first(
|
|
||||||
data as MappedAnomalyHit[]
|
|
||||||
)!;
|
|
||||||
|
|
||||||
alertInstance.scheduleActions(FIRED_ACTIONS_ID, {
|
|
||||||
alertState: stateToAlertMessage[AlertStates.ALERT],
|
|
||||||
timestamp: moment(anomalyStartTime).toISOString(),
|
|
||||||
anomalyScore,
|
|
||||||
actual,
|
|
||||||
typical,
|
|
||||||
metric: metricNameMap[metric],
|
|
||||||
summary: generateSummaryMessage(actual, typical),
|
|
||||||
influencers: influencers.join(', '),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FIRED_ACTIONS_ID = 'metrics.anomaly.fired';
|
|
||||||
export const FIRED_ACTIONS: ActionGroup<typeof FIRED_ACTIONS_ID> = {
|
|
||||||
id: FIRED_ACTIONS_ID,
|
|
||||||
name: i18n.translate('xpack.infra.metrics.alerting.anomaly.fired', {
|
|
||||||
defaultMessage: 'Fired',
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateSummaryMessage = (actual: number, typical: number) => {
|
|
||||||
const differential = (Math.max(actual, typical) / Math.min(actual, typical))
|
|
||||||
.toFixed(1)
|
|
||||||
.replace('.0', '');
|
|
||||||
if (actual > typical) {
|
|
||||||
return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryHigher', {
|
|
||||||
defaultMessage: '{differential}x higher',
|
|
||||||
values: {
|
|
||||||
differential,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryLower', {
|
|
||||||
defaultMessage: '{differential}x lower',
|
|
||||||
values: {
|
|
||||||
differential,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const metricNameMap = {
|
|
||||||
memory_usage: i18n.translate('xpack.infra.metrics.alerting.anomaly.memoryUsage', {
|
|
||||||
defaultMessage: 'Memory usage',
|
|
||||||
}),
|
|
||||||
network_in: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkIn', {
|
|
||||||
defaultMessage: 'Network in',
|
|
||||||
}),
|
|
||||||
network_out: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkOut', {
|
|
||||||
defaultMessage: 'Network out',
|
|
||||||
}),
|
|
||||||
};
|
|
|
@ -1,120 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Unit } from '@elastic/datemath';
|
|
||||||
import { countBy } from 'lodash';
|
|
||||||
import { MappedAnomalyHit } from '../../infra_ml';
|
|
||||||
import { MlSystem, MlAnomalyDetectors } from '../../../types';
|
|
||||||
import { MetricAnomalyParams } from '../../../../common/alerting/metrics';
|
|
||||||
import {
|
|
||||||
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
|
|
||||||
isTooManyBucketsPreviewException,
|
|
||||||
} from '../../../../common/alerting/metrics';
|
|
||||||
import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
|
|
||||||
import { evaluateCondition } from './evaluate_condition';
|
|
||||||
|
|
||||||
interface PreviewMetricAnomalyAlertParams {
|
|
||||||
mlSystem: MlSystem;
|
|
||||||
mlAnomalyDetectors: MlAnomalyDetectors;
|
|
||||||
spaceId: string;
|
|
||||||
params: MetricAnomalyParams;
|
|
||||||
sourceId: string;
|
|
||||||
lookback: Unit;
|
|
||||||
alertInterval: string;
|
|
||||||
alertThrottle: string;
|
|
||||||
alertOnNoData: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const previewMetricAnomalyAlert = async ({
|
|
||||||
mlSystem,
|
|
||||||
mlAnomalyDetectors,
|
|
||||||
spaceId,
|
|
||||||
params,
|
|
||||||
sourceId,
|
|
||||||
lookback,
|
|
||||||
alertInterval,
|
|
||||||
alertThrottle,
|
|
||||||
}: PreviewMetricAnomalyAlertParams) => {
|
|
||||||
const { metric, threshold, influencerFilter, nodeType } = params as MetricAnomalyParams;
|
|
||||||
|
|
||||||
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
|
|
||||||
const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle);
|
|
||||||
const executionsPerThrottle = Math.floor(throttleIntervalInSeconds / alertIntervalInSeconds);
|
|
||||||
|
|
||||||
const lookbackInterval = `1${lookback}`;
|
|
||||||
const lookbackIntervalInSeconds = getIntervalInSeconds(lookbackInterval);
|
|
||||||
const endTime = Date.now();
|
|
||||||
const startTime = endTime - lookbackIntervalInSeconds * 1000;
|
|
||||||
|
|
||||||
const numberOfExecutions = Math.floor(lookbackIntervalInSeconds / alertIntervalInSeconds);
|
|
||||||
const bucketIntervalInSeconds = getIntervalInSeconds('15m');
|
|
||||||
const bucketsPerExecution = Math.max(
|
|
||||||
1,
|
|
||||||
Math.floor(alertIntervalInSeconds / bucketIntervalInSeconds)
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let anomalies: MappedAnomalyHit[] = [];
|
|
||||||
const { data } = await evaluateCondition({
|
|
||||||
nodeType,
|
|
||||||
spaceId,
|
|
||||||
sourceId,
|
|
||||||
mlSystem,
|
|
||||||
mlAnomalyDetectors,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
metric,
|
|
||||||
threshold,
|
|
||||||
influencerFilter,
|
|
||||||
});
|
|
||||||
anomalies = [...anomalies, ...data];
|
|
||||||
|
|
||||||
const anomaliesByTime = countBy(anomalies, ({ startTime: anomStartTime }) => anomStartTime);
|
|
||||||
|
|
||||||
let numberOfTimesFired = 0;
|
|
||||||
let numberOfNotifications = 0;
|
|
||||||
let throttleTracker = 0;
|
|
||||||
const notifyWithThrottle = () => {
|
|
||||||
if (throttleTracker === 0) numberOfNotifications++;
|
|
||||||
throttleTracker++;
|
|
||||||
};
|
|
||||||
// Mock each alert evaluation
|
|
||||||
for (let i = 0; i < numberOfExecutions; i++) {
|
|
||||||
const executionTime = startTime + alertIntervalInSeconds * 1000 * i;
|
|
||||||
// Get an array of bucket times this mock alert evaluation will be looking at
|
|
||||||
// Anomalies are bucketed at :00, :15, :30, :45 minutes every hour,
|
|
||||||
// so this is an array of how many of those times occurred between this evaluation
|
|
||||||
// and the previous one
|
|
||||||
const bucketsLookedAt = Array.from(Array(bucketsPerExecution), (_, idx) => {
|
|
||||||
const previousBucketStartTime =
|
|
||||||
executionTime -
|
|
||||||
(executionTime % (bucketIntervalInSeconds * 1000)) -
|
|
||||||
idx * bucketIntervalInSeconds * 1000;
|
|
||||||
return previousBucketStartTime;
|
|
||||||
});
|
|
||||||
const anomaliesDetectedInBuckets = bucketsLookedAt.some((bucketTime) =>
|
|
||||||
Reflect.has(anomaliesByTime, bucketTime)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (anomaliesDetectedInBuckets) {
|
|
||||||
numberOfTimesFired++;
|
|
||||||
notifyWithThrottle();
|
|
||||||
} else if (throttleTracker > 0) {
|
|
||||||
throttleTracker++;
|
|
||||||
}
|
|
||||||
if (throttleTracker === executionsPerThrottle) {
|
|
||||||
throttleTracker = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { fired: numberOfTimesFired, notifications: numberOfNotifications };
|
|
||||||
} catch (e) {
|
|
||||||
if (!isTooManyBucketsPreviewException(e)) throw e;
|
|
||||||
const { maxBuckets } = e;
|
|
||||||
throw new Error(`${TOO_MANY_BUCKETS_PREVIEW_EXCEPTION}:${maxBuckets}`);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,110 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { schema } from '@kbn/config-schema';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { MlPluginSetup } from '../../../../../ml/server';
|
|
||||||
import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../../alerts/server';
|
|
||||||
import {
|
|
||||||
createMetricAnomalyExecutor,
|
|
||||||
FIRED_ACTIONS,
|
|
||||||
FIRED_ACTIONS_ID,
|
|
||||||
} from './metric_anomaly_executor';
|
|
||||||
import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../../common/alerting/metrics';
|
|
||||||
import { InfraBackendLibs } from '../../infra_types';
|
|
||||||
import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils';
|
|
||||||
import { alertStateActionVariableDescription } from '../common/messages';
|
|
||||||
import { RecoveredActionGroupId } from '../../../../../alerts/common';
|
|
||||||
|
|
||||||
export type MetricAnomalyAllowedActionGroups = typeof FIRED_ACTIONS_ID;
|
|
||||||
|
|
||||||
export const registerMetricAnomalyAlertType = (
|
|
||||||
libs: InfraBackendLibs,
|
|
||||||
ml?: MlPluginSetup
|
|
||||||
): AlertType<
|
|
||||||
/**
|
|
||||||
* TODO: Remove this use of `any` by utilizing a proper type
|
|
||||||
*/
|
|
||||||
Record<string, any>,
|
|
||||||
Record<string, any>,
|
|
||||||
AlertInstanceState,
|
|
||||||
AlertInstanceContext,
|
|
||||||
MetricAnomalyAllowedActionGroups,
|
|
||||||
RecoveredActionGroupId
|
|
||||||
> => ({
|
|
||||||
id: METRIC_ANOMALY_ALERT_TYPE_ID,
|
|
||||||
name: i18n.translate('xpack.infra.metrics.anomaly.alertName', {
|
|
||||||
defaultMessage: 'Infrastructure anomaly',
|
|
||||||
}),
|
|
||||||
validate: {
|
|
||||||
params: schema.object(
|
|
||||||
{
|
|
||||||
nodeType: oneOfLiterals(['hosts', 'k8s']),
|
|
||||||
alertInterval: schema.string(),
|
|
||||||
metric: oneOfLiterals(['memory_usage', 'network_in', 'network_out']),
|
|
||||||
threshold: schema.number(),
|
|
||||||
filterQuery: schema.maybe(
|
|
||||||
schema.string({ validate: validateIsStringElasticsearchJSONFilter })
|
|
||||||
),
|
|
||||||
sourceId: schema.string(),
|
|
||||||
},
|
|
||||||
{ unknowns: 'allow' }
|
|
||||||
),
|
|
||||||
},
|
|
||||||
defaultActionGroupId: FIRED_ACTIONS_ID,
|
|
||||||
actionGroups: [FIRED_ACTIONS],
|
|
||||||
producer: 'infrastructure',
|
|
||||||
minimumLicenseRequired: 'basic',
|
|
||||||
executor: createMetricAnomalyExecutor(libs, ml),
|
|
||||||
actionVariables: {
|
|
||||||
context: [
|
|
||||||
{ name: 'alertState', description: alertStateActionVariableDescription },
|
|
||||||
{
|
|
||||||
name: 'metric',
|
|
||||||
description: i18n.translate('xpack.infra.metrics.alerting.anomalyMetricDescription', {
|
|
||||||
defaultMessage: 'The metric name in the specified condition.',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'timestamp',
|
|
||||||
description: i18n.translate('xpack.infra.metrics.alerting.anomalyTimestampDescription', {
|
|
||||||
defaultMessage: 'A timestamp of when the anomaly was detected.',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'anomalyScore',
|
|
||||||
description: i18n.translate('xpack.infra.metrics.alerting.anomalyScoreDescription', {
|
|
||||||
defaultMessage: 'The exact severity score of the detected anomaly.',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actual',
|
|
||||||
description: i18n.translate('xpack.infra.metrics.alerting.anomalyActualDescription', {
|
|
||||||
defaultMessage: 'The actual value of the monitored metric at the time of the anomaly.',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'typical',
|
|
||||||
description: i18n.translate('xpack.infra.metrics.alerting.anomalyTypicalDescription', {
|
|
||||||
defaultMessage: 'The typical value of the monitored metric at the time of the anomaly.',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'summary',
|
|
||||||
description: i18n.translate('xpack.infra.metrics.alerting.anomalySummaryDescription', {
|
|
||||||
defaultMessage: 'A description of the anomaly, e.g. "2x higher."',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'influencers',
|
|
||||||
description: i18n.translate('xpack.infra.metrics.alerting.anomalyInfluencersDescription', {
|
|
||||||
defaultMessage: 'A list of node names that influenced the anomaly.',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -8,21 +8,13 @@
|
||||||
import { PluginSetupContract } from '../../../../alerts/server';
|
import { PluginSetupContract } from '../../../../alerts/server';
|
||||||
import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type';
|
import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type';
|
||||||
import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type';
|
import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type';
|
||||||
import { registerMetricAnomalyAlertType } from './metric_anomaly/register_metric_anomaly_alert_type';
|
|
||||||
|
|
||||||
import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type';
|
import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type';
|
||||||
import { InfraBackendLibs } from '../infra_types';
|
import { InfraBackendLibs } from '../infra_types';
|
||||||
import { MlPluginSetup } from '../../../../ml/server';
|
|
||||||
|
|
||||||
const registerAlertTypes = (
|
const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => {
|
||||||
alertingPlugin: PluginSetupContract,
|
|
||||||
libs: InfraBackendLibs,
|
|
||||||
ml?: MlPluginSetup
|
|
||||||
) => {
|
|
||||||
if (alertingPlugin) {
|
if (alertingPlugin) {
|
||||||
alertingPlugin.registerType(registerMetricThresholdAlertType(libs));
|
alertingPlugin.registerType(registerMetricThresholdAlertType(libs));
|
||||||
alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs));
|
alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs));
|
||||||
alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml));
|
|
||||||
|
|
||||||
const registerFns = [registerLogThresholdAlertType];
|
const registerFns = [registerLogThresholdAlertType];
|
||||||
registerFns.forEach((fn) => {
|
registerFns.forEach((fn) => {
|
||||||
|
|
|
@ -17,23 +17,6 @@ import {
|
||||||
import { decodeOrThrow } from '../../../common/runtime_types';
|
import { decodeOrThrow } from '../../../common/runtime_types';
|
||||||
import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing';
|
import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing';
|
||||||
|
|
||||||
export interface MappedAnomalyHit {
|
|
||||||
id: string;
|
|
||||||
anomalyScore: number;
|
|
||||||
typical: number;
|
|
||||||
actual: number;
|
|
||||||
jobId: string;
|
|
||||||
startTime: number;
|
|
||||||
duration: number;
|
|
||||||
influencers: string[];
|
|
||||||
categoryId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InfluencerFilter {
|
|
||||||
fieldName: string;
|
|
||||||
fieldValue: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) {
|
export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) {
|
||||||
const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES');
|
const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES');
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -8,4 +8,3 @@
|
||||||
export * from './errors';
|
export * from './errors';
|
||||||
export * from './metrics_hosts_anomalies';
|
export * from './metrics_hosts_anomalies';
|
||||||
export * from './metrics_k8s_anomalies';
|
export * from './metrics_k8s_anomalies';
|
||||||
export { MappedAnomalyHit } from './common';
|
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { InfraPluginRequestHandlerContext } from '../../types';
|
||||||
import { InfraRequestHandlerContext } from '../../types';
|
import { InfraRequestHandlerContext } from '../../types';
|
||||||
import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
|
import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
|
||||||
import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common';
|
import { fetchMlJob } from './common';
|
||||||
import { getJobId, metricsHostsJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml';
|
import { getJobId, metricsHostsJobTypes } from '../../../common/infra_ml';
|
||||||
import { Sort, Pagination } from '../../../common/http_api/infra_ml';
|
import { Sort, Pagination } from '../../../common/http_api/infra_ml';
|
||||||
import type { MlSystem, MlAnomalyDetectors } from '../../types';
|
import type { MlSystem, MlAnomalyDetectors } from '../../types';
|
||||||
import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors';
|
import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors';
|
||||||
|
@ -18,6 +19,18 @@ import {
|
||||||
createMetricsHostsAnomaliesQuery,
|
createMetricsHostsAnomaliesQuery,
|
||||||
} from './queries/metrics_hosts_anomalies';
|
} from './queries/metrics_hosts_anomalies';
|
||||||
|
|
||||||
|
interface MappedAnomalyHit {
|
||||||
|
id: string;
|
||||||
|
anomalyScore: number;
|
||||||
|
typical: number;
|
||||||
|
actual: number;
|
||||||
|
jobId: string;
|
||||||
|
startTime: number;
|
||||||
|
duration: number;
|
||||||
|
influencers: string[];
|
||||||
|
categoryId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
async function getCompatibleAnomaliesJobIds(
|
async function getCompatibleAnomaliesJobIds(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
|
@ -61,15 +74,14 @@ async function getCompatibleAnomaliesJobIds(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMetricsHostsAnomalies(
|
export async function getMetricsHostsAnomalies(
|
||||||
context: Required<InfraRequestHandlerContext>,
|
context: InfraPluginRequestHandlerContext & { infra: Required<InfraRequestHandlerContext> },
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
anomalyThreshold: ANOMALY_THRESHOLD,
|
anomalyThreshold: number,
|
||||||
startTime: number,
|
startTime: number,
|
||||||
endTime: number,
|
endTime: number,
|
||||||
metric: 'memory_usage' | 'network_in' | 'network_out' | undefined,
|
metric: 'memory_usage' | 'network_in' | 'network_out' | undefined,
|
||||||
sort: Sort,
|
sort: Sort,
|
||||||
pagination: Pagination,
|
pagination: Pagination
|
||||||
influencerFilter?: InfluencerFilter
|
|
||||||
) {
|
) {
|
||||||
const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies');
|
const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies');
|
||||||
|
|
||||||
|
@ -77,10 +89,10 @@ export async function getMetricsHostsAnomalies(
|
||||||
jobIds,
|
jobIds,
|
||||||
timing: { spans: jobSpans },
|
timing: { spans: jobSpans },
|
||||||
} = await getCompatibleAnomaliesJobIds(
|
} = await getCompatibleAnomaliesJobIds(
|
||||||
context.spaceId,
|
context.infra.spaceId,
|
||||||
sourceId,
|
sourceId,
|
||||||
metric,
|
metric,
|
||||||
context.mlAnomalyDetectors
|
context.infra.mlAnomalyDetectors
|
||||||
);
|
);
|
||||||
|
|
||||||
if (jobIds.length === 0) {
|
if (jobIds.length === 0) {
|
||||||
|
@ -96,14 +108,13 @@ export async function getMetricsHostsAnomalies(
|
||||||
hasMoreEntries,
|
hasMoreEntries,
|
||||||
timing: { spans: fetchLogEntryAnomaliesSpans },
|
timing: { spans: fetchLogEntryAnomaliesSpans },
|
||||||
} = await fetchMetricsHostsAnomalies(
|
} = await fetchMetricsHostsAnomalies(
|
||||||
context.mlSystem,
|
context.infra.mlSystem,
|
||||||
anomalyThreshold,
|
anomalyThreshold,
|
||||||
jobIds,
|
jobIds,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
sort,
|
sort,
|
||||||
pagination,
|
pagination
|
||||||
influencerFilter
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = anomalies.map((anomaly) => {
|
const data = anomalies.map((anomaly) => {
|
||||||
|
@ -153,13 +164,12 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => {
|
||||||
|
|
||||||
async function fetchMetricsHostsAnomalies(
|
async function fetchMetricsHostsAnomalies(
|
||||||
mlSystem: MlSystem,
|
mlSystem: MlSystem,
|
||||||
anomalyThreshold: ANOMALY_THRESHOLD,
|
anomalyThreshold: number,
|
||||||
jobIds: string[],
|
jobIds: string[],
|
||||||
startTime: number,
|
startTime: number,
|
||||||
endTime: number,
|
endTime: number,
|
||||||
sort: Sort,
|
sort: Sort,
|
||||||
pagination: Pagination,
|
pagination: Pagination
|
||||||
influencerFilter?: InfluencerFilter
|
|
||||||
) {
|
) {
|
||||||
// We'll request 1 extra entry on top of our pageSize to determine if there are
|
// We'll request 1 extra entry on top of our pageSize to determine if there are
|
||||||
// more entries to be fetched. This avoids scenarios where the client side can't
|
// more entries to be fetched. This avoids scenarios where the client side can't
|
||||||
|
@ -178,7 +188,6 @@ async function fetchMetricsHostsAnomalies(
|
||||||
endTime,
|
endTime,
|
||||||
sort,
|
sort,
|
||||||
pagination: expandedPagination,
|
pagination: expandedPagination,
|
||||||
influencerFilter,
|
|
||||||
}),
|
}),
|
||||||
jobIds
|
jobIds
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { InfraPluginRequestHandlerContext } from '../../types';
|
||||||
import { InfraRequestHandlerContext } from '../../types';
|
import { InfraRequestHandlerContext } from '../../types';
|
||||||
import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
|
import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
|
||||||
import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common';
|
import { fetchMlJob } from './common';
|
||||||
import { getJobId, metricsK8SJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml';
|
import { getJobId, metricsK8SJobTypes } from '../../../common/infra_ml';
|
||||||
import { Sort, Pagination } from '../../../common/http_api/infra_ml';
|
import { Sort, Pagination } from '../../../common/http_api/infra_ml';
|
||||||
import type { MlSystem, MlAnomalyDetectors } from '../../types';
|
import type { MlSystem, MlAnomalyDetectors } from '../../types';
|
||||||
import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors';
|
import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors';
|
||||||
|
@ -18,6 +19,18 @@ import {
|
||||||
createMetricsK8sAnomaliesQuery,
|
createMetricsK8sAnomaliesQuery,
|
||||||
} from './queries/metrics_k8s_anomalies';
|
} from './queries/metrics_k8s_anomalies';
|
||||||
|
|
||||||
|
interface MappedAnomalyHit {
|
||||||
|
id: string;
|
||||||
|
anomalyScore: number;
|
||||||
|
typical: number;
|
||||||
|
actual: number;
|
||||||
|
jobId: string;
|
||||||
|
startTime: number;
|
||||||
|
influencers: string[];
|
||||||
|
duration: number;
|
||||||
|
categoryId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
async function getCompatibleAnomaliesJobIds(
|
async function getCompatibleAnomaliesJobIds(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
|
@ -61,15 +74,14 @@ async function getCompatibleAnomaliesJobIds(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMetricK8sAnomalies(
|
export async function getMetricK8sAnomalies(
|
||||||
context: Required<InfraRequestHandlerContext>,
|
context: InfraPluginRequestHandlerContext & { infra: Required<InfraRequestHandlerContext> },
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
anomalyThreshold: ANOMALY_THRESHOLD,
|
anomalyThreshold: number,
|
||||||
startTime: number,
|
startTime: number,
|
||||||
endTime: number,
|
endTime: number,
|
||||||
metric: 'memory_usage' | 'network_in' | 'network_out' | undefined,
|
metric: 'memory_usage' | 'network_in' | 'network_out' | undefined,
|
||||||
sort: Sort,
|
sort: Sort,
|
||||||
pagination: Pagination,
|
pagination: Pagination
|
||||||
influencerFilter?: InfluencerFilter
|
|
||||||
) {
|
) {
|
||||||
const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies');
|
const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies');
|
||||||
|
|
||||||
|
@ -77,10 +89,10 @@ export async function getMetricK8sAnomalies(
|
||||||
jobIds,
|
jobIds,
|
||||||
timing: { spans: jobSpans },
|
timing: { spans: jobSpans },
|
||||||
} = await getCompatibleAnomaliesJobIds(
|
} = await getCompatibleAnomaliesJobIds(
|
||||||
context.spaceId,
|
context.infra.spaceId,
|
||||||
sourceId,
|
sourceId,
|
||||||
metric,
|
metric,
|
||||||
context.mlAnomalyDetectors
|
context.infra.mlAnomalyDetectors
|
||||||
);
|
);
|
||||||
|
|
||||||
if (jobIds.length === 0) {
|
if (jobIds.length === 0) {
|
||||||
|
@ -95,14 +107,13 @@ export async function getMetricK8sAnomalies(
|
||||||
hasMoreEntries,
|
hasMoreEntries,
|
||||||
timing: { spans: fetchLogEntryAnomaliesSpans },
|
timing: { spans: fetchLogEntryAnomaliesSpans },
|
||||||
} = await fetchMetricK8sAnomalies(
|
} = await fetchMetricK8sAnomalies(
|
||||||
context.mlSystem,
|
context.infra.mlSystem,
|
||||||
anomalyThreshold,
|
anomalyThreshold,
|
||||||
jobIds,
|
jobIds,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
sort,
|
sort,
|
||||||
pagination,
|
pagination
|
||||||
influencerFilter
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = anomalies.map((anomaly) => {
|
const data = anomalies.map((anomaly) => {
|
||||||
|
@ -149,13 +160,12 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => {
|
||||||
|
|
||||||
async function fetchMetricK8sAnomalies(
|
async function fetchMetricK8sAnomalies(
|
||||||
mlSystem: MlSystem,
|
mlSystem: MlSystem,
|
||||||
anomalyThreshold: ANOMALY_THRESHOLD,
|
anomalyThreshold: number,
|
||||||
jobIds: string[],
|
jobIds: string[],
|
||||||
startTime: number,
|
startTime: number,
|
||||||
endTime: number,
|
endTime: number,
|
||||||
sort: Sort,
|
sort: Sort,
|
||||||
pagination: Pagination,
|
pagination: Pagination
|
||||||
influencerFilter?: InfluencerFilter | undefined
|
|
||||||
) {
|
) {
|
||||||
// We'll request 1 extra entry on top of our pageSize to determine if there are
|
// We'll request 1 extra entry on top of our pageSize to determine if there are
|
||||||
// more entries to be fetched. This avoids scenarios where the client side can't
|
// more entries to be fetched. This avoids scenarios where the client side can't
|
||||||
|
@ -174,7 +184,6 @@ async function fetchMetricK8sAnomalies(
|
||||||
endTime,
|
endTime,
|
||||||
sort,
|
sort,
|
||||||
pagination: expandedPagination,
|
pagination: expandedPagination,
|
||||||
influencerFilter,
|
|
||||||
}),
|
}),
|
||||||
jobIds
|
jobIds
|
||||||
)
|
)
|
||||||
|
|
|
@ -77,35 +77,3 @@ export const createDatasetsFilters = (datasets?: string[]) =>
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
export const createInfluencerFilter = ({
|
|
||||||
fieldName,
|
|
||||||
fieldValue,
|
|
||||||
}: {
|
|
||||||
fieldName: string;
|
|
||||||
fieldValue: string;
|
|
||||||
}) => [
|
|
||||||
{
|
|
||||||
nested: {
|
|
||||||
path: 'influencers',
|
|
||||||
query: {
|
|
||||||
bool: {
|
|
||||||
must: [
|
|
||||||
{
|
|
||||||
match: {
|
|
||||||
'influencers.influencer_field_name': fieldName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
query_string: {
|
|
||||||
fields: ['influencers.influencer_field_values'],
|
|
||||||
query: fieldValue,
|
|
||||||
minimum_should_match: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as rt from 'io-ts';
|
import * as rt from 'io-ts';
|
||||||
import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml';
|
|
||||||
import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
|
import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
|
||||||
import {
|
import {
|
||||||
createJobIdsFilters,
|
createJobIdsFilters,
|
||||||
|
@ -14,9 +13,7 @@ import {
|
||||||
createResultTypeFilters,
|
createResultTypeFilters,
|
||||||
defaultRequestParameters,
|
defaultRequestParameters,
|
||||||
createAnomalyScoreFilter,
|
createAnomalyScoreFilter,
|
||||||
createInfluencerFilter,
|
|
||||||
} from './common';
|
} from './common';
|
||||||
import { InfluencerFilter } from '../common';
|
|
||||||
import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
|
import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
|
||||||
|
|
||||||
// TODO: Reassess validity of this against ML docs
|
// TODO: Reassess validity of this against ML docs
|
||||||
|
@ -35,15 +32,13 @@ export const createMetricsHostsAnomaliesQuery = ({
|
||||||
endTime,
|
endTime,
|
||||||
sort,
|
sort,
|
||||||
pagination,
|
pagination,
|
||||||
influencerFilter,
|
|
||||||
}: {
|
}: {
|
||||||
jobIds: string[];
|
jobIds: string[];
|
||||||
anomalyThreshold: ANOMALY_THRESHOLD;
|
anomalyThreshold: number;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
sort: Sort;
|
sort: Sort;
|
||||||
pagination: Pagination;
|
pagination: Pagination;
|
||||||
influencerFilter?: InfluencerFilter;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { field } = sort;
|
const { field } = sort;
|
||||||
const { pageSize } = pagination;
|
const { pageSize } = pagination;
|
||||||
|
@ -55,10 +50,6 @@ export const createMetricsHostsAnomaliesQuery = ({
|
||||||
...createResultTypeFilters(['record']),
|
...createResultTypeFilters(['record']),
|
||||||
];
|
];
|
||||||
|
|
||||||
const influencerQuery = influencerFilter
|
|
||||||
? { must: createInfluencerFilter(influencerFilter) }
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const sourceFields = [
|
const sourceFields = [
|
||||||
'job_id',
|
'job_id',
|
||||||
'record_score',
|
'record_score',
|
||||||
|
@ -86,7 +77,6 @@ export const createMetricsHostsAnomaliesQuery = ({
|
||||||
query: {
|
query: {
|
||||||
bool: {
|
bool: {
|
||||||
filter: filters,
|
filter: filters,
|
||||||
...influencerQuery,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
search_after: queryCursor,
|
search_after: queryCursor,
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as rt from 'io-ts';
|
import * as rt from 'io-ts';
|
||||||
import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml';
|
|
||||||
import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
|
import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
|
||||||
import {
|
import {
|
||||||
createJobIdsFilters,
|
createJobIdsFilters,
|
||||||
|
@ -14,9 +13,7 @@ import {
|
||||||
createResultTypeFilters,
|
createResultTypeFilters,
|
||||||
defaultRequestParameters,
|
defaultRequestParameters,
|
||||||
createAnomalyScoreFilter,
|
createAnomalyScoreFilter,
|
||||||
createInfluencerFilter,
|
|
||||||
} from './common';
|
} from './common';
|
||||||
import { InfluencerFilter } from '../common';
|
|
||||||
import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
|
import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
|
||||||
|
|
||||||
// TODO: Reassess validity of this against ML docs
|
// TODO: Reassess validity of this against ML docs
|
||||||
|
@ -35,15 +32,13 @@ export const createMetricsK8sAnomaliesQuery = ({
|
||||||
endTime,
|
endTime,
|
||||||
sort,
|
sort,
|
||||||
pagination,
|
pagination,
|
||||||
influencerFilter,
|
|
||||||
}: {
|
}: {
|
||||||
jobIds: string[];
|
jobIds: string[];
|
||||||
anomalyThreshold: ANOMALY_THRESHOLD;
|
anomalyThreshold: number;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
sort: Sort;
|
sort: Sort;
|
||||||
pagination: Pagination;
|
pagination: Pagination;
|
||||||
influencerFilter?: InfluencerFilter;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { field } = sort;
|
const { field } = sort;
|
||||||
const { pageSize } = pagination;
|
const { pageSize } = pagination;
|
||||||
|
@ -55,10 +50,6 @@ export const createMetricsK8sAnomaliesQuery = ({
|
||||||
...createResultTypeFilters(['record']),
|
...createResultTypeFilters(['record']),
|
||||||
];
|
];
|
||||||
|
|
||||||
const influencerQuery = influencerFilter
|
|
||||||
? { must: createInfluencerFilter(influencerFilter) }
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const sourceFields = [
|
const sourceFields = [
|
||||||
'job_id',
|
'job_id',
|
||||||
'record_score',
|
'record_score',
|
||||||
|
@ -85,7 +76,6 @@ export const createMetricsK8sAnomaliesQuery = ({
|
||||||
query: {
|
query: {
|
||||||
bool: {
|
bool: {
|
||||||
filter: filters,
|
filter: filters,
|
||||||
...influencerQuery,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
search_after: queryCursor,
|
search_after: queryCursor,
|
||||||
|
|
|
@ -137,7 +137,7 @@ export class InfraServerPlugin implements Plugin<InfraPluginSetup> {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
initInfraServer(this.libs);
|
initInfraServer(this.libs);
|
||||||
registerAlertTypes(plugins.alerts, this.libs, plugins.ml);
|
registerAlertTypes(plugins.alerts, this.libs);
|
||||||
|
|
||||||
core.http.registerRouteHandlerContext<InfraPluginRequestHandlerContext, 'infra'>(
|
core.http.registerRouteHandlerContext<InfraPluginRequestHandlerContext, 'infra'>(
|
||||||
'infra',
|
'infra',
|
||||||
|
|
|
@ -9,21 +9,17 @@ import { PreviewResult } from '../../lib/alerting/common/types';
|
||||||
import {
|
import {
|
||||||
METRIC_THRESHOLD_ALERT_TYPE_ID,
|
METRIC_THRESHOLD_ALERT_TYPE_ID,
|
||||||
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
|
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
|
||||||
METRIC_ANOMALY_ALERT_TYPE_ID,
|
|
||||||
INFRA_ALERT_PREVIEW_PATH,
|
INFRA_ALERT_PREVIEW_PATH,
|
||||||
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
|
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
|
||||||
alertPreviewRequestParamsRT,
|
alertPreviewRequestParamsRT,
|
||||||
alertPreviewSuccessResponsePayloadRT,
|
alertPreviewSuccessResponsePayloadRT,
|
||||||
MetricThresholdAlertPreviewRequestParams,
|
MetricThresholdAlertPreviewRequestParams,
|
||||||
InventoryAlertPreviewRequestParams,
|
InventoryAlertPreviewRequestParams,
|
||||||
MetricAnomalyAlertPreviewRequestParams,
|
|
||||||
} from '../../../common/alerting/metrics';
|
} from '../../../common/alerting/metrics';
|
||||||
import { createValidationFunction } from '../../../common/runtime_types';
|
import { createValidationFunction } from '../../../common/runtime_types';
|
||||||
import { previewInventoryMetricThresholdAlert } from '../../lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert';
|
import { previewInventoryMetricThresholdAlert } from '../../lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert';
|
||||||
import { previewMetricThresholdAlert } from '../../lib/alerting/metric_threshold/preview_metric_threshold_alert';
|
import { previewMetricThresholdAlert } from '../../lib/alerting/metric_threshold/preview_metric_threshold_alert';
|
||||||
import { previewMetricAnomalyAlert } from '../../lib/alerting/metric_anomaly/preview_metric_anomaly_alert';
|
|
||||||
import { InfraBackendLibs } from '../../lib/infra_types';
|
import { InfraBackendLibs } from '../../lib/infra_types';
|
||||||
import { assertHasInfraMlPlugins } from '../../utils/request_context';
|
|
||||||
|
|
||||||
export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => {
|
export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => {
|
||||||
const { callWithRequest } = framework;
|
const { callWithRequest } = framework;
|
||||||
|
@ -37,6 +33,8 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
||||||
},
|
},
|
||||||
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
|
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
|
||||||
const {
|
const {
|
||||||
|
criteria,
|
||||||
|
filterQuery,
|
||||||
lookback,
|
lookback,
|
||||||
sourceId,
|
sourceId,
|
||||||
alertType,
|
alertType,
|
||||||
|
@ -57,11 +55,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
||||||
try {
|
try {
|
||||||
switch (alertType) {
|
switch (alertType) {
|
||||||
case METRIC_THRESHOLD_ALERT_TYPE_ID: {
|
case METRIC_THRESHOLD_ALERT_TYPE_ID: {
|
||||||
const {
|
const { groupBy } = request.body as MetricThresholdAlertPreviewRequestParams;
|
||||||
groupBy,
|
|
||||||
criteria,
|
|
||||||
filterQuery,
|
|
||||||
} = request.body as MetricThresholdAlertPreviewRequestParams;
|
|
||||||
const previewResult = await previewMetricThresholdAlert({
|
const previewResult = await previewMetricThresholdAlert({
|
||||||
callCluster,
|
callCluster,
|
||||||
params: { criteria, filterQuery, groupBy },
|
params: { criteria, filterQuery, groupBy },
|
||||||
|
@ -78,11 +72,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: {
|
case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: {
|
||||||
const {
|
const { nodeType } = request.body as InventoryAlertPreviewRequestParams;
|
||||||
nodeType,
|
|
||||||
criteria,
|
|
||||||
filterQuery,
|
|
||||||
} = request.body as InventoryAlertPreviewRequestParams;
|
|
||||||
const previewResult = await previewInventoryMetricThresholdAlert({
|
const previewResult = await previewInventoryMetricThresholdAlert({
|
||||||
callCluster,
|
callCluster,
|
||||||
params: { criteria, filterQuery, nodeType },
|
params: { criteria, filterQuery, nodeType },
|
||||||
|
@ -99,39 +89,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
||||||
body: alertPreviewSuccessResponsePayloadRT.encode(payload),
|
body: alertPreviewSuccessResponsePayloadRT.encode(payload),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case METRIC_ANOMALY_ALERT_TYPE_ID: {
|
|
||||||
assertHasInfraMlPlugins(requestContext);
|
|
||||||
const {
|
|
||||||
nodeType,
|
|
||||||
metric,
|
|
||||||
threshold,
|
|
||||||
influencerFilter,
|
|
||||||
} = request.body as MetricAnomalyAlertPreviewRequestParams;
|
|
||||||
const { mlAnomalyDetectors, mlSystem, spaceId } = requestContext.infra;
|
|
||||||
|
|
||||||
const previewResult = await previewMetricAnomalyAlert({
|
|
||||||
mlAnomalyDetectors,
|
|
||||||
mlSystem,
|
|
||||||
spaceId,
|
|
||||||
params: { nodeType, metric, threshold, influencerFilter },
|
|
||||||
lookback,
|
|
||||||
sourceId: source.id,
|
|
||||||
alertInterval,
|
|
||||||
alertThrottle,
|
|
||||||
alertOnNoData,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.ok({
|
|
||||||
body: alertPreviewSuccessResponsePayloadRT.encode({
|
|
||||||
numberOfGroups: 1,
|
|
||||||
resultTotals: {
|
|
||||||
...previewResult,
|
|
||||||
error: 0,
|
|
||||||
noData: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
throw new Error('Unknown alert type');
|
throw new Error('Unknown alert type');
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
|
||||||
hasMoreEntries,
|
hasMoreEntries,
|
||||||
timing,
|
timing,
|
||||||
} = await getMetricsHostsAnomalies(
|
} = await getMetricsHostsAnomalies(
|
||||||
requestContext.infra,
|
requestContext,
|
||||||
sourceId,
|
sourceId,
|
||||||
anomalyThreshold,
|
anomalyThreshold,
|
||||||
startTime,
|
startTime,
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
|
||||||
hasMoreEntries,
|
hasMoreEntries,
|
||||||
timing,
|
timing,
|
||||||
} = await getMetricK8sAnomalies(
|
} = await getMetricK8sAnomalies(
|
||||||
requestContext.infra,
|
requestContext,
|
||||||
sourceId,
|
sourceId,
|
||||||
anomalyThreshold,
|
anomalyThreshold,
|
||||||
startTime,
|
startTime,
|
||||||
|
|
|
@ -9676,6 +9676,7 @@
|
||||||
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)",
|
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)",
|
||||||
"xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件",
|
"xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件",
|
||||||
"xpack.infra.alerting.alertsButton": "アラート",
|
"xpack.infra.alerting.alertsButton": "アラート",
|
||||||
|
"xpack.infra.alerting.createAlertButton": "アラートの作成",
|
||||||
"xpack.infra.alerting.logs.alertsButton": "アラート",
|
"xpack.infra.alerting.logs.alertsButton": "アラート",
|
||||||
"xpack.infra.alerting.logs.createAlertButton": "アラートの作成",
|
"xpack.infra.alerting.logs.createAlertButton": "アラートの作成",
|
||||||
"xpack.infra.alerting.logs.manageAlerts": "アラートを管理",
|
"xpack.infra.alerting.logs.manageAlerts": "アラートを管理",
|
||||||
|
@ -9969,6 +9970,16 @@
|
||||||
"xpack.infra.logs.jumpToTailText": "最も新しいエントリーに移動",
|
"xpack.infra.logs.jumpToTailText": "最も新しいエントリーに移動",
|
||||||
"xpack.infra.logs.lastUpdate": "前回の更新 {timestamp}",
|
"xpack.infra.logs.lastUpdate": "前回の更新 {timestamp}",
|
||||||
"xpack.infra.logs.loadingNewEntriesText": "新しいエントリーを読み込み中",
|
"xpack.infra.logs.loadingNewEntriesText": "新しいエントリーを読み込み中",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.learnMoreLink": "ドキュメンテーションを表示",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "詳細について",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.loadingMessage": "ライセンスを確認しています...",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.splashImageAlt": "プレースホルダー画像",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.startTrialCta": "トライアルを開始",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.startTrialDescription": "無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.startTrialTitle": "異常検知を利用するには、無料の試用版を開始してください",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "サブスクリプションのアップグレード",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "機械学習機能を使用するには、プラチナサブスクリプションが必要です。",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "異常検知を利用するには、プラチナサブスクリプションにアップグレードしてください",
|
||||||
"xpack.infra.logs.logEntryActionsDetailsButton": "詳細を表示",
|
"xpack.infra.logs.logEntryActionsDetailsButton": "詳細を表示",
|
||||||
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析",
|
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析",
|
||||||
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。",
|
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。",
|
||||||
|
|
|
@ -9702,6 +9702,7 @@
|
||||||
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)",
|
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)",
|
||||||
"xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据",
|
"xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据",
|
||||||
"xpack.infra.alerting.alertsButton": "告警",
|
"xpack.infra.alerting.alertsButton": "告警",
|
||||||
|
"xpack.infra.alerting.createAlertButton": "创建告警",
|
||||||
"xpack.infra.alerting.logs.alertsButton": "告警",
|
"xpack.infra.alerting.logs.alertsButton": "告警",
|
||||||
"xpack.infra.alerting.logs.createAlertButton": "创建告警",
|
"xpack.infra.alerting.logs.createAlertButton": "创建告警",
|
||||||
"xpack.infra.alerting.logs.manageAlerts": "管理告警",
|
"xpack.infra.alerting.logs.manageAlerts": "管理告警",
|
||||||
|
@ -9996,6 +9997,16 @@
|
||||||
"xpack.infra.logs.jumpToTailText": "跳到最近的条目",
|
"xpack.infra.logs.jumpToTailText": "跳到最近的条目",
|
||||||
"xpack.infra.logs.lastUpdate": "上次更新时间 {timestamp}",
|
"xpack.infra.logs.lastUpdate": "上次更新时间 {timestamp}",
|
||||||
"xpack.infra.logs.loadingNewEntriesText": "正在加载新条目",
|
"xpack.infra.logs.loadingNewEntriesText": "正在加载新条目",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.learnMoreLink": "阅读文档",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "希望了解详情?",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.loadingMessage": "正在检查许可证......",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.splashImageAlt": "占位符图像",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.startTrialCta": "开始试用",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.startTrialDescription": "我们的免费试用版包含 Machine Learning 功能,可用于检测日志中的异常。",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.startTrialTitle": "要访问异常检测,请启动免费试用版",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "升级订阅",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "必须具有白金级订阅,才能使用 Machine Learning 功能。",
|
||||||
|
"xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "要访问异常检测,请升级到白金级订阅",
|
||||||
"xpack.infra.logs.logEntryActionsDetailsButton": "查看详情",
|
"xpack.infra.logs.logEntryActionsDetailsButton": "查看详情",
|
||||||
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析",
|
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析",
|
||||||
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。",
|
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue