* Revert "Revert "[Metrics UI] Add Metrics Anomaly Alert Type (#89244)""
This reverts commit 8166becc55
.
* Fix type error
This commit is contained in:
parent
59cc39434e
commit
38a6475396
|
@ -4,14 +4,15 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { ANOMALY_THRESHOLD } from '../../infra_ml';
|
||||
import { ItemTypeRT } from '../../inventory_models/types';
|
||||
|
||||
// TODO: Have threshold and inventory alerts import these types from this file instead of from their
|
||||
// local directories
|
||||
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_ANOMALY_ALERT_TYPE_ID = 'metrics.alert.anomaly';
|
||||
|
||||
export enum Comparator {
|
||||
GT = '>',
|
||||
|
@ -34,6 +35,26 @@ export enum Aggregators {
|
|||
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
|
||||
const baseAlertRequestParamsRT = rt.intersection([
|
||||
rt.partial({
|
||||
|
@ -51,7 +72,6 @@ const baseAlertRequestParamsRT = rt.intersection([
|
|||
rt.literal('M'),
|
||||
rt.literal('y'),
|
||||
]),
|
||||
criteria: rt.array(rt.any),
|
||||
alertInterval: rt.string,
|
||||
alertThrottle: rt.string,
|
||||
alertOnNoData: rt.boolean,
|
||||
|
@ -65,6 +85,7 @@ const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([
|
|||
}),
|
||||
rt.type({
|
||||
alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID),
|
||||
criteria: rt.array(rt.any),
|
||||
}),
|
||||
]);
|
||||
export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf<
|
||||
|
@ -76,15 +97,33 @@ const inventoryAlertPreviewRequestParamsRT = rt.intersection([
|
|||
rt.type({
|
||||
nodeType: ItemTypeRT,
|
||||
alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID),
|
||||
criteria: rt.array(rt.any),
|
||||
}),
|
||||
]);
|
||||
export type InventoryAlertPreviewRequestParams = rt.TypeOf<
|
||||
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([
|
||||
metricThresholdAlertPreviewRequestParamsRT,
|
||||
inventoryAlertPreviewRequestParamsRT,
|
||||
metricAnomalyAlertPreviewRequestParamsRT,
|
||||
]);
|
||||
export type AlertPreviewRequestParams = rt.TypeOf<typeof alertPreviewRequestParamsRT>;
|
||||
|
||||
|
|
|
@ -5,36 +5,44 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const ML_SEVERITY_SCORES = {
|
||||
warning: 3,
|
||||
minor: 25,
|
||||
major: 50,
|
||||
critical: 75,
|
||||
export enum ANOMALY_SEVERITY {
|
||||
CRITICAL = 'critical',
|
||||
MAJOR = 'major',
|
||||
MINOR = 'minor',
|
||||
WARNING = 'warning',
|
||||
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 type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES;
|
||||
|
||||
export const ML_SEVERITY_COLORS = {
|
||||
critical: 'rgb(228, 72, 72)',
|
||||
major: 'rgb(229, 113, 0)',
|
||||
minor: 'rgb(255, 221, 0)',
|
||||
warning: 'rgb(125, 180, 226)',
|
||||
};
|
||||
|
||||
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';
|
||||
export const getSeverityCategoryForScore = (score: number): ANOMALY_SEVERITY | undefined => {
|
||||
if (score >= ANOMALY_THRESHOLD.CRITICAL) {
|
||||
return ANOMALY_SEVERITY.CRITICAL;
|
||||
} else if (score >= ANOMALY_THRESHOLD.MAJOR) {
|
||||
return ANOMALY_SEVERITY.MAJOR;
|
||||
} else if (score >= ANOMALY_THRESHOLD.MINOR) {
|
||||
return ANOMALY_SEVERITY.MINOR;
|
||||
} else if (score >= ANOMALY_THRESHOLD.WARNING) {
|
||||
return ANOMALY_SEVERITY.WARNING;
|
||||
} else {
|
||||
// Category is too low to include
|
||||
return undefined;
|
||||
return ANOMALY_SEVERITY.LOW;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ interface Props {
|
|||
alertInterval: string;
|
||||
alertThrottle: string;
|
||||
alertType: PreviewableAlertTypes;
|
||||
alertParams: { criteria: any[]; sourceId: string } & Record<string, any>;
|
||||
alertParams: { criteria?: any[]; sourceId: string } & Record<string, any>;
|
||||
validate: (params: any) => ValidationResult;
|
||||
showNoDataResults?: boolean;
|
||||
groupByDisplayName?: string;
|
||||
|
@ -109,6 +109,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
|||
}, [previewLookbackInterval, alertInterval]);
|
||||
|
||||
const isPreviewDisabled = useMemo(() => {
|
||||
if (!alertParams.criteria) return false;
|
||||
const validationResult = validate({ criteria: alertParams.criteria } as any);
|
||||
const hasValidationErrors = Object.values(validationResult.errors).some((result) =>
|
||||
Object.values(result).some((arr) => Array.isArray(arr) && arr.length)
|
||||
|
@ -124,7 +125,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
|||
}, [previewResult, showNoDataResults]);
|
||||
|
||||
const hasWarningThreshold = useMemo(
|
||||
() => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')),
|
||||
() => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')) ?? false,
|
||||
[alertParams]
|
||||
);
|
||||
|
||||
|
|
|
@ -10,13 +10,15 @@ import {
|
|||
INFRA_ALERT_PREVIEW_PATH,
|
||||
METRIC_THRESHOLD_ALERT_TYPE_ID,
|
||||
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
|
||||
METRIC_ANOMALY_ALERT_TYPE_ID,
|
||||
AlertPreviewRequestParams,
|
||||
AlertPreviewSuccessResponsePayload,
|
||||
} from '../../../../common/alerting/metrics';
|
||||
|
||||
export type PreviewableAlertTypes =
|
||||
| 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({
|
||||
fetch,
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
};
|
|
@ -1,59 +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, 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,8 +8,7 @@
|
|||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
|
||||
// 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 { METRIC_INVENTORY_THRESHOLD_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';
|
||||
|
@ -49,3 +48,18 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }:
|
|||
|
||||
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)}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metrics.alertFlyout.expression.for.popoverTitle"
|
||||
defaultMessage="Inventory Type"
|
||||
defaultMessage="Node Type"
|
||||
/>
|
||||
</ClosablePopoverTitle>
|
||||
<EuiSelect
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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} />;
|
||||
};
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,320 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* 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',
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
46
x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts
Normal file
46
x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { 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,
|
||||
};
|
||||
}
|
|
@ -1,57 +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, 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 { TriggerActionsContext } from '../../../utils/triggers_actions_context';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types';
|
||||
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics';
|
||||
import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
|
||||
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
|
||||
|
||||
interface Props {
|
||||
visible?: boolean;
|
||||
|
@ -42,3 +42,10 @@ export const AlertFlyout = (props: Props) => {
|
|||
|
||||
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,4 +14,3 @@ export * from './missing_results_privileges_prompt';
|
|||
export * from './missing_setup_privileges_prompt';
|
||||
export * from './ml_unavailable_prompt';
|
||||
export * from './setup_status_unknown_prompt';
|
||||
export * from './subscription_splash_content';
|
||||
|
|
|
@ -1,176 +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, { 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,
|
||||
]);
|
||||
|
||||
const { hasInfraMLCapabilites } = useInfraMLCapabilitiesContext();
|
||||
const { hasInfraMLCapabilities } = useInfraMLCapabilitiesContext();
|
||||
|
||||
if ((isLoading || isUninitialized) && !source) {
|
||||
return <SourceLoadingPage />;
|
||||
|
@ -128,7 +128,7 @@ export const SourceConfigurationSettings = ({
|
|||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
{hasInfraMLCapabilites && (
|
||||
{hasInfraMLCapabilities && (
|
||||
<>
|
||||
<EuiPanel paddingSize="l">
|
||||
<MLConfigurationPanel
|
||||
|
|
|
@ -22,11 +22,11 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { LoadingPage } from '../../../../../../components/loading_page';
|
||||
import { useTrialStatus } from '../../../../../../hooks/use_trial_status';
|
||||
import { useKibana } from '../../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { HttpStart } from '../../../../../../../../../../src/core/public';
|
||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { euiStyled, EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
|
||||
import { HttpStart } from '../../../../../src/core/public';
|
||||
import { useTrialStatus } from '../hooks/use_trial_status';
|
||||
import { LoadingPage } from '../components/loading_page';
|
||||
|
||||
export const SubscriptionSplashContent: React.FC = () => {
|
||||
const { services } = useKibana<{ http: HttpStart }>();
|
||||
|
@ -102,58 +102,60 @@ export const SubscriptionSplashContent: React.FC = () => {
|
|||
}
|
||||
|
||||
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.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?"
|
||||
<EuiThemeProvider>
|
||||
<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.ml.splash.splashImageAlt', {
|
||||
defaultMessage: 'Placeholder image',
|
||||
})}
|
||||
url={services.http.basePath.prepend(
|
||||
'/plugins/infra/assets/anomaly_chart_minified.svg'
|
||||
)}
|
||||
size="fullWidth"
|
||||
/>
|
||||
</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.ml.splash.learnMoreLink"
|
||||
defaultMessage="Read documentation"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</SubscriptionPageFooter>
|
||||
</SubscriptionPageContent>
|
||||
</EuiPageBody>
|
||||
</SubscriptionPage>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<SubscriptionPageFooter>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.ml.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.ml.splash.learnMoreLink"
|
||||
defaultMessage="Read documentation"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</SubscriptionPageFooter>
|
||||
</SubscriptionPageContent>
|
||||
</EuiPageBody>
|
||||
</SubscriptionPage>
|
||||
</EuiThemeProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -52,11 +52,11 @@ export const useInfraMLCapabilities = () => {
|
|||
|
||||
const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob;
|
||||
const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs;
|
||||
const hasInfraMLCapabilites =
|
||||
const hasInfraMLCapabilities =
|
||||
mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace;
|
||||
|
||||
return {
|
||||
hasInfraMLCapabilites,
|
||||
hasInfraMLCapabilities,
|
||||
hasInfraMLReadCapabilities,
|
||||
hasInfraMLSetupCapabilities,
|
||||
isLoading,
|
||||
|
|
|
@ -56,7 +56,8 @@ class WithKueryAutocompletionComponent extends React.Component<
|
|||
private loadSuggestions = async (
|
||||
expression: string,
|
||||
cursorPosition: number,
|
||||
maxSuggestions?: number
|
||||
maxSuggestions?: number,
|
||||
transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[]
|
||||
) => {
|
||||
const { indexPattern } = this.props;
|
||||
const language = 'kuery';
|
||||
|
@ -86,6 +87,10 @@ class WithKueryAutocompletionComponent extends React.Component<
|
|||
boolFilter: [],
|
||||
})) || [];
|
||||
|
||||
const transformedSuggestions = transformSuggestions
|
||||
? transformSuggestions(suggestions)
|
||||
: suggestions;
|
||||
|
||||
this.setState((state) =>
|
||||
state.currentRequest &&
|
||||
state.currentRequest.expression !== expression &&
|
||||
|
@ -94,7 +99,9 @@ class WithKueryAutocompletionComponent extends React.Component<
|
|||
: {
|
||||
...state,
|
||||
currentRequest: null,
|
||||
suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions,
|
||||
suggestions: maxSuggestions
|
||||
? transformedSuggestions.slice(0, maxSuggestions)
|
||||
: transformedSuggestions,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
|
||||
import { isJobStatusWithResults } from '../../../../common/log_analysis';
|
||||
import { LoadingPage } from '../../../components/loading_page';
|
||||
import {
|
||||
LogAnalysisSetupStatusUnknownPrompt,
|
||||
MissingResultsPrivilegesPrompt,
|
||||
MissingSetupPrivilegesPrompt,
|
||||
SubscriptionSplashContent,
|
||||
} from '../../../components/logging/log_analysis_setup';
|
||||
import {
|
||||
LogAnalysisSetupFlyout,
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { memo, useEffect, useCallback } from 'react';
|
||||
import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
|
||||
import { isJobStatusWithResults } from '../../../../common/log_analysis';
|
||||
import { LoadingPage } from '../../../components/loading_page';
|
||||
import {
|
||||
LogAnalysisSetupStatusUnknownPrompt,
|
||||
MissingResultsPrivilegesPrompt,
|
||||
MissingSetupPrivilegesPrompt,
|
||||
SubscriptionSplashContent,
|
||||
} from '../../../components/logging/log_analysis_setup';
|
||||
import {
|
||||
LogAnalysisSetupFlyout,
|
||||
|
|
|
@ -35,12 +35,11 @@ import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options
|
|||
import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time';
|
||||
import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters';
|
||||
|
||||
import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown';
|
||||
import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown';
|
||||
import { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown';
|
||||
import { SavedView } from '../../containers/saved_view/saved_view';
|
||||
import { AlertPrefillProvider } from '../../alerting/use_alert_prefill';
|
||||
import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities';
|
||||
import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout';
|
||||
import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout';
|
||||
import { HeaderMenuPortal } from '../../../../observability/public';
|
||||
import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider';
|
||||
|
||||
|
@ -83,8 +82,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
|
|||
<Route path={'/inventory'} component={AnomalyDetectionFlyout} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Route path={'/explorer'} component={MetricsAlertDropdown} />
|
||||
<Route path={'/inventory'} component={InventoryAlertDropdown} />
|
||||
<MetricsAlertDropdown />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
|
|
|
@ -14,8 +14,8 @@ import { EuiCallOut } from '@elastic/eui';
|
|||
import { EuiButton } from '@elastic/eui';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import { SubscriptionSplashContent } from '../../../../../../components/subscription_splash_content';
|
||||
import { useInfraMLCapabilitiesContext } from '../../../../../../containers/ml/infra_ml_capabilities';
|
||||
import { SubscriptionSplashContent } from './subscription_splash_content';
|
||||
import {
|
||||
MissingResultsPrivilegesPrompt,
|
||||
MissingSetupPrivilegesPrompt,
|
||||
|
@ -44,7 +44,7 @@ export const FlyoutHome = (props: Props) => {
|
|||
jobSummaries: k8sJobSummaries,
|
||||
} = useMetricK8sModuleContext();
|
||||
const {
|
||||
hasInfraMLCapabilites,
|
||||
hasInfraMLCapabilities,
|
||||
hasInfraMLReadCapabilities,
|
||||
hasInfraMLSetupCapabilities,
|
||||
} = useInfraMLCapabilitiesContext();
|
||||
|
@ -69,7 +69,7 @@ export const FlyoutHome = (props: Props) => {
|
|||
}
|
||||
}, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]);
|
||||
|
||||
if (!hasInfraMLCapabilites) {
|
||||
if (!hasInfraMLCapabilities) {
|
||||
return <SubscriptionSplashContent />;
|
||||
} else if (!hasInfraMLReadCapabilities) {
|
||||
return <MissingResultsPrivilegesPrompt />;
|
||||
|
|
|
@ -10,7 +10,19 @@ import { i18n } from '@kbn/i18n';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion';
|
||||
import { AutocompleteField } from '../../../../components/autocomplete_field';
|
||||
import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public';
|
||||
import {
|
||||
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 {
|
||||
derivedIndexPattern: IIndexPattern;
|
||||
|
@ -18,6 +30,7 @@ interface Props {
|
|||
onChange?: (query: string) => void;
|
||||
value?: string | null;
|
||||
placeholder?: string;
|
||||
curryLoadSuggestions?: CurryLoadSuggestionsType;
|
||||
}
|
||||
|
||||
function validateQuery(query: string) {
|
||||
|
@ -35,6 +48,7 @@ export const MetricsExplorerKueryBar = ({
|
|||
onChange,
|
||||
value,
|
||||
placeholder,
|
||||
curryLoadSuggestions = defaultCurryLoadSuggestions,
|
||||
}: Props) => {
|
||||
const [draftQuery, setDraftQuery] = useState<string>(value || '');
|
||||
const [isValid, setValidation] = useState<boolean>(true);
|
||||
|
@ -73,7 +87,7 @@ export const MetricsExplorerKueryBar = ({
|
|||
aria-label={placeholder}
|
||||
isLoadingSuggestions={isLoadingSuggestions}
|
||||
isValid={isValid}
|
||||
loadSuggestions={loadSuggestions}
|
||||
loadSuggestions={curryLoadSuggestions(loadSuggestions)}
|
||||
onChange={handleChange}
|
||||
onSubmit={onSubmit}
|
||||
placeholder={placeholder || defaultPlaceholder}
|
||||
|
@ -84,3 +98,6 @@ export const MetricsExplorerKueryBar = ({
|
|||
</WithKueryAutocompletion>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultCurryLoadSuggestions: CurryLoadSuggestionsType = (loadSuggestions) => (...args) =>
|
||||
loadSuggestions(...args);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public';
|
|||
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
|
||||
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
|
||||
import { createInventoryMetricAlertType } from './alerting/inventory';
|
||||
import { createMetricAnomalyAlertType } from './alerting/metric_anomaly';
|
||||
import { getAlertType as getLogsAlertType } from './alerting/log_threshold';
|
||||
import { registerFeatures } from './register_feature';
|
||||
import {
|
||||
|
@ -35,6 +36,7 @@ export class Plugin implements InfraClientPluginClass {
|
|||
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType());
|
||||
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(getLogsAlertType());
|
||||
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType());
|
||||
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricAnomalyAlertType());
|
||||
|
||||
if (pluginsSetup.observability) {
|
||||
pluginsSetup.observability.dashboard.register({
|
||||
|
|
|
@ -23,7 +23,7 @@ import type {
|
|||
ObservabilityPluginStart,
|
||||
} from '../../observability/public';
|
||||
import type { SpacesPluginStart } from '../../spaces/public';
|
||||
import { MlPluginStart } from '../../ml/public';
|
||||
import { MlPluginStart, MlPluginSetup } from '../../ml/public';
|
||||
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
|
||||
|
||||
// Our own setup and start contract values
|
||||
|
@ -36,6 +36,7 @@ export interface InfraClientSetupDeps {
|
|||
observability: ObservabilityPluginSetup;
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
ml: MlPluginSetup;
|
||||
embeddable: EmbeddableSetup;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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;
|
||||
};
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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',
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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,13 +8,21 @@
|
|||
import { PluginSetupContract } from '../../../../alerts/server';
|
||||
import { registerMetricThresholdAlertType } from './metric_threshold/register_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 { InfraBackendLibs } from '../infra_types';
|
||||
import { MlPluginSetup } from '../../../../ml/server';
|
||||
|
||||
const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => {
|
||||
const registerAlertTypes = (
|
||||
alertingPlugin: PluginSetupContract,
|
||||
libs: InfraBackendLibs,
|
||||
ml?: MlPluginSetup
|
||||
) => {
|
||||
if (alertingPlugin) {
|
||||
alertingPlugin.registerType(registerMetricThresholdAlertType(libs));
|
||||
alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs));
|
||||
alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml));
|
||||
|
||||
const registerFns = [registerLogThresholdAlertType];
|
||||
registerFns.forEach((fn) => {
|
||||
|
|
|
@ -17,6 +17,23 @@ import {
|
|||
import { decodeOrThrow } from '../../../common/runtime_types';
|
||||
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) {
|
||||
const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES');
|
||||
const {
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
export * from './errors';
|
||||
export * from './metrics_hosts_anomalies';
|
||||
export * from './metrics_k8s_anomalies';
|
||||
export { MappedAnomalyHit } from './common';
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { InfraPluginRequestHandlerContext } from '../../types';
|
||||
import { InfraRequestHandlerContext } from '../../types';
|
||||
import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
|
||||
import { fetchMlJob } from './common';
|
||||
import { getJobId, metricsHostsJobTypes } from '../../../common/infra_ml';
|
||||
import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common';
|
||||
import { getJobId, metricsHostsJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml';
|
||||
import { Sort, Pagination } from '../../../common/http_api/infra_ml';
|
||||
import type { MlSystem, MlAnomalyDetectors } from '../../types';
|
||||
import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors';
|
||||
|
@ -19,18 +18,6 @@ import {
|
|||
createMetricsHostsAnomaliesQuery,
|
||||
} 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(
|
||||
spaceId: string,
|
||||
sourceId: string,
|
||||
|
@ -74,14 +61,15 @@ async function getCompatibleAnomaliesJobIds(
|
|||
}
|
||||
|
||||
export async function getMetricsHostsAnomalies(
|
||||
context: InfraPluginRequestHandlerContext & { infra: Required<InfraRequestHandlerContext> },
|
||||
context: Required<InfraRequestHandlerContext>,
|
||||
sourceId: string,
|
||||
anomalyThreshold: number,
|
||||
anomalyThreshold: ANOMALY_THRESHOLD,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
metric: 'memory_usage' | 'network_in' | 'network_out' | undefined,
|
||||
sort: Sort,
|
||||
pagination: Pagination
|
||||
pagination: Pagination,
|
||||
influencerFilter?: InfluencerFilter
|
||||
) {
|
||||
const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies');
|
||||
|
||||
|
@ -89,10 +77,10 @@ export async function getMetricsHostsAnomalies(
|
|||
jobIds,
|
||||
timing: { spans: jobSpans },
|
||||
} = await getCompatibleAnomaliesJobIds(
|
||||
context.infra.spaceId,
|
||||
context.spaceId,
|
||||
sourceId,
|
||||
metric,
|
||||
context.infra.mlAnomalyDetectors
|
||||
context.mlAnomalyDetectors
|
||||
);
|
||||
|
||||
if (jobIds.length === 0) {
|
||||
|
@ -108,13 +96,14 @@ export async function getMetricsHostsAnomalies(
|
|||
hasMoreEntries,
|
||||
timing: { spans: fetchLogEntryAnomaliesSpans },
|
||||
} = await fetchMetricsHostsAnomalies(
|
||||
context.infra.mlSystem,
|
||||
context.mlSystem,
|
||||
anomalyThreshold,
|
||||
jobIds,
|
||||
startTime,
|
||||
endTime,
|
||||
sort,
|
||||
pagination
|
||||
pagination,
|
||||
influencerFilter
|
||||
);
|
||||
|
||||
const data = anomalies.map((anomaly) => {
|
||||
|
@ -164,12 +153,13 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => {
|
|||
|
||||
async function fetchMetricsHostsAnomalies(
|
||||
mlSystem: MlSystem,
|
||||
anomalyThreshold: number,
|
||||
anomalyThreshold: ANOMALY_THRESHOLD,
|
||||
jobIds: string[],
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
sort: Sort,
|
||||
pagination: Pagination
|
||||
pagination: Pagination,
|
||||
influencerFilter?: InfluencerFilter
|
||||
) {
|
||||
// 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
|
||||
|
@ -188,6 +178,7 @@ async function fetchMetricsHostsAnomalies(
|
|||
endTime,
|
||||
sort,
|
||||
pagination: expandedPagination,
|
||||
influencerFilter,
|
||||
}),
|
||||
jobIds
|
||||
)
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { InfraPluginRequestHandlerContext } from '../../types';
|
||||
import { InfraRequestHandlerContext } from '../../types';
|
||||
import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
|
||||
import { fetchMlJob } from './common';
|
||||
import { getJobId, metricsK8SJobTypes } from '../../../common/infra_ml';
|
||||
import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common';
|
||||
import { getJobId, metricsK8SJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml';
|
||||
import { Sort, Pagination } from '../../../common/http_api/infra_ml';
|
||||
import type { MlSystem, MlAnomalyDetectors } from '../../types';
|
||||
import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors';
|
||||
|
@ -19,18 +18,6 @@ import {
|
|||
createMetricsK8sAnomaliesQuery,
|
||||
} 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(
|
||||
spaceId: string,
|
||||
sourceId: string,
|
||||
|
@ -74,14 +61,15 @@ async function getCompatibleAnomaliesJobIds(
|
|||
}
|
||||
|
||||
export async function getMetricK8sAnomalies(
|
||||
context: InfraPluginRequestHandlerContext & { infra: Required<InfraRequestHandlerContext> },
|
||||
context: Required<InfraRequestHandlerContext>,
|
||||
sourceId: string,
|
||||
anomalyThreshold: number,
|
||||
anomalyThreshold: ANOMALY_THRESHOLD,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
metric: 'memory_usage' | 'network_in' | 'network_out' | undefined,
|
||||
sort: Sort,
|
||||
pagination: Pagination
|
||||
pagination: Pagination,
|
||||
influencerFilter?: InfluencerFilter
|
||||
) {
|
||||
const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies');
|
||||
|
||||
|
@ -89,10 +77,10 @@ export async function getMetricK8sAnomalies(
|
|||
jobIds,
|
||||
timing: { spans: jobSpans },
|
||||
} = await getCompatibleAnomaliesJobIds(
|
||||
context.infra.spaceId,
|
||||
context.spaceId,
|
||||
sourceId,
|
||||
metric,
|
||||
context.infra.mlAnomalyDetectors
|
||||
context.mlAnomalyDetectors
|
||||
);
|
||||
|
||||
if (jobIds.length === 0) {
|
||||
|
@ -107,13 +95,14 @@ export async function getMetricK8sAnomalies(
|
|||
hasMoreEntries,
|
||||
timing: { spans: fetchLogEntryAnomaliesSpans },
|
||||
} = await fetchMetricK8sAnomalies(
|
||||
context.infra.mlSystem,
|
||||
context.mlSystem,
|
||||
anomalyThreshold,
|
||||
jobIds,
|
||||
startTime,
|
||||
endTime,
|
||||
sort,
|
||||
pagination
|
||||
pagination,
|
||||
influencerFilter
|
||||
);
|
||||
|
||||
const data = anomalies.map((anomaly) => {
|
||||
|
@ -160,12 +149,13 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => {
|
|||
|
||||
async function fetchMetricK8sAnomalies(
|
||||
mlSystem: MlSystem,
|
||||
anomalyThreshold: number,
|
||||
anomalyThreshold: ANOMALY_THRESHOLD,
|
||||
jobIds: string[],
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
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
|
||||
// more entries to be fetched. This avoids scenarios where the client side can't
|
||||
|
@ -184,6 +174,7 @@ async function fetchMetricK8sAnomalies(
|
|||
endTime,
|
||||
sort,
|
||||
pagination: expandedPagination,
|
||||
influencerFilter,
|
||||
}),
|
||||
jobIds
|
||||
)
|
||||
|
|
|
@ -77,3 +77,35 @@ 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,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml';
|
||||
import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
|
||||
import {
|
||||
createJobIdsFilters,
|
||||
|
@ -13,7 +14,9 @@ import {
|
|||
createResultTypeFilters,
|
||||
defaultRequestParameters,
|
||||
createAnomalyScoreFilter,
|
||||
createInfluencerFilter,
|
||||
} from './common';
|
||||
import { InfluencerFilter } from '../common';
|
||||
import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
|
||||
|
||||
// TODO: Reassess validity of this against ML docs
|
||||
|
@ -32,13 +35,15 @@ export const createMetricsHostsAnomaliesQuery = ({
|
|||
endTime,
|
||||
sort,
|
||||
pagination,
|
||||
influencerFilter,
|
||||
}: {
|
||||
jobIds: string[];
|
||||
anomalyThreshold: number;
|
||||
anomalyThreshold: ANOMALY_THRESHOLD;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
sort: Sort;
|
||||
pagination: Pagination;
|
||||
influencerFilter?: InfluencerFilter;
|
||||
}) => {
|
||||
const { field } = sort;
|
||||
const { pageSize } = pagination;
|
||||
|
@ -50,6 +55,10 @@ export const createMetricsHostsAnomaliesQuery = ({
|
|||
...createResultTypeFilters(['record']),
|
||||
];
|
||||
|
||||
const influencerQuery = influencerFilter
|
||||
? { must: createInfluencerFilter(influencerFilter) }
|
||||
: {};
|
||||
|
||||
const sourceFields = [
|
||||
'job_id',
|
||||
'record_score',
|
||||
|
@ -77,6 +86,7 @@ export const createMetricsHostsAnomaliesQuery = ({
|
|||
query: {
|
||||
bool: {
|
||||
filter: filters,
|
||||
...influencerQuery,
|
||||
},
|
||||
},
|
||||
search_after: queryCursor,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml';
|
||||
import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
|
||||
import {
|
||||
createJobIdsFilters,
|
||||
|
@ -13,7 +14,9 @@ import {
|
|||
createResultTypeFilters,
|
||||
defaultRequestParameters,
|
||||
createAnomalyScoreFilter,
|
||||
createInfluencerFilter,
|
||||
} from './common';
|
||||
import { InfluencerFilter } from '../common';
|
||||
import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
|
||||
|
||||
// TODO: Reassess validity of this against ML docs
|
||||
|
@ -32,13 +35,15 @@ export const createMetricsK8sAnomaliesQuery = ({
|
|||
endTime,
|
||||
sort,
|
||||
pagination,
|
||||
influencerFilter,
|
||||
}: {
|
||||
jobIds: string[];
|
||||
anomalyThreshold: number;
|
||||
anomalyThreshold: ANOMALY_THRESHOLD;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
sort: Sort;
|
||||
pagination: Pagination;
|
||||
influencerFilter?: InfluencerFilter;
|
||||
}) => {
|
||||
const { field } = sort;
|
||||
const { pageSize } = pagination;
|
||||
|
@ -50,6 +55,10 @@ export const createMetricsK8sAnomaliesQuery = ({
|
|||
...createResultTypeFilters(['record']),
|
||||
];
|
||||
|
||||
const influencerQuery = influencerFilter
|
||||
? { must: createInfluencerFilter(influencerFilter) }
|
||||
: {};
|
||||
|
||||
const sourceFields = [
|
||||
'job_id',
|
||||
'record_score',
|
||||
|
@ -76,6 +85,7 @@ export const createMetricsK8sAnomaliesQuery = ({
|
|||
query: {
|
||||
bool: {
|
||||
filter: filters,
|
||||
...influencerQuery,
|
||||
},
|
||||
},
|
||||
search_after: queryCursor,
|
||||
|
|
|
@ -137,7 +137,7 @@ export class InfraServerPlugin implements Plugin<InfraPluginSetup> {
|
|||
]);
|
||||
|
||||
initInfraServer(this.libs);
|
||||
registerAlertTypes(plugins.alerts, this.libs);
|
||||
registerAlertTypes(plugins.alerts, this.libs, plugins.ml);
|
||||
|
||||
core.http.registerRouteHandlerContext<InfraPluginRequestHandlerContext, 'infra'>(
|
||||
'infra',
|
||||
|
|
|
@ -9,17 +9,21 @@ import { PreviewResult } from '../../lib/alerting/common/types';
|
|||
import {
|
||||
METRIC_THRESHOLD_ALERT_TYPE_ID,
|
||||
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
|
||||
METRIC_ANOMALY_ALERT_TYPE_ID,
|
||||
INFRA_ALERT_PREVIEW_PATH,
|
||||
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
|
||||
alertPreviewRequestParamsRT,
|
||||
alertPreviewSuccessResponsePayloadRT,
|
||||
MetricThresholdAlertPreviewRequestParams,
|
||||
InventoryAlertPreviewRequestParams,
|
||||
MetricAnomalyAlertPreviewRequestParams,
|
||||
} from '../../../common/alerting/metrics';
|
||||
import { createValidationFunction } from '../../../common/runtime_types';
|
||||
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 { previewMetricAnomalyAlert } from '../../lib/alerting/metric_anomaly/preview_metric_anomaly_alert';
|
||||
import { InfraBackendLibs } from '../../lib/infra_types';
|
||||
import { assertHasInfraMlPlugins } from '../../utils/request_context';
|
||||
|
||||
export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => {
|
||||
const { callWithRequest } = framework;
|
||||
|
@ -33,8 +37,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
|||
},
|
||||
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
|
||||
const {
|
||||
criteria,
|
||||
filterQuery,
|
||||
lookback,
|
||||
sourceId,
|
||||
alertType,
|
||||
|
@ -55,7 +57,11 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
|||
try {
|
||||
switch (alertType) {
|
||||
case METRIC_THRESHOLD_ALERT_TYPE_ID: {
|
||||
const { groupBy } = request.body as MetricThresholdAlertPreviewRequestParams;
|
||||
const {
|
||||
groupBy,
|
||||
criteria,
|
||||
filterQuery,
|
||||
} = request.body as MetricThresholdAlertPreviewRequestParams;
|
||||
const previewResult = await previewMetricThresholdAlert({
|
||||
callCluster,
|
||||
params: { criteria, filterQuery, groupBy },
|
||||
|
@ -72,7 +78,11 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
|||
});
|
||||
}
|
||||
case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: {
|
||||
const { nodeType } = request.body as InventoryAlertPreviewRequestParams;
|
||||
const {
|
||||
nodeType,
|
||||
criteria,
|
||||
filterQuery,
|
||||
} = request.body as InventoryAlertPreviewRequestParams;
|
||||
const previewResult = await previewInventoryMetricThresholdAlert({
|
||||
callCluster,
|
||||
params: { criteria, filterQuery, nodeType },
|
||||
|
@ -89,6 +99,39 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
|||
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:
|
||||
throw new Error('Unknown alert type');
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
|
|||
hasMoreEntries,
|
||||
timing,
|
||||
} = await getMetricsHostsAnomalies(
|
||||
requestContext,
|
||||
requestContext.infra,
|
||||
sourceId,
|
||||
anomalyThreshold,
|
||||
startTime,
|
||||
|
|
|
@ -52,7 +52,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
|
|||
hasMoreEntries,
|
||||
timing,
|
||||
} = await getMetricK8sAnomalies(
|
||||
requestContext,
|
||||
requestContext.infra,
|
||||
sourceId,
|
||||
anomalyThreshold,
|
||||
startTime,
|
||||
|
|
|
@ -9676,7 +9676,6 @@
|
|||
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)",
|
||||
"xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件",
|
||||
"xpack.infra.alerting.alertsButton": "アラート",
|
||||
"xpack.infra.alerting.createAlertButton": "アラートの作成",
|
||||
"xpack.infra.alerting.logs.alertsButton": "アラート",
|
||||
"xpack.infra.alerting.logs.createAlertButton": "アラートの作成",
|
||||
"xpack.infra.alerting.logs.manageAlerts": "アラートを管理",
|
||||
|
@ -9970,16 +9969,6 @@
|
|||
"xpack.infra.logs.jumpToTailText": "最も新しいエントリーに移動",
|
||||
"xpack.infra.logs.lastUpdate": "前回の更新 {timestamp}",
|
||||
"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.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析",
|
||||
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。",
|
||||
|
|
|
@ -9702,7 +9702,6 @@
|
|||
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)",
|
||||
"xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据",
|
||||
"xpack.infra.alerting.alertsButton": "告警",
|
||||
"xpack.infra.alerting.createAlertButton": "创建告警",
|
||||
"xpack.infra.alerting.logs.alertsButton": "告警",
|
||||
"xpack.infra.alerting.logs.createAlertButton": "创建告警",
|
||||
"xpack.infra.alerting.logs.manageAlerts": "管理告警",
|
||||
|
@ -9997,16 +9996,6 @@
|
|||
"xpack.infra.logs.jumpToTailText": "跳到最近的条目",
|
||||
"xpack.infra.logs.lastUpdate": "上次更新时间 {timestamp}",
|
||||
"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.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析",
|
||||
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。",
|
||||
|
|
Loading…
Reference in a new issue