Revert "Revert "[Metrics UI] Add Metrics Anomaly Alert Type (#89244)"" (#90889)

* Revert "Revert "[Metrics UI] Add Metrics Anomaly Alert Type (#89244)""

This reverts commit 8166becc55.

* Fix type error
This commit is contained in:
Zacqary Adam Xeper 2021-02-09 20:30:25 -06:00 committed by GitHub
parent 59cc39434e
commit 38a6475396
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1922 additions and 484 deletions

View file

@ -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>;

View file

@ -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;
}
};

View file

@ -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]
);

View file

@ -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,

View file

@ -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;
}
};

View file

@ -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}
/>
</>
);
};

View file

@ -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}
/>
);
};

View file

@ -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

View file

@ -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} />;
};

View file

@ -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');
});
});

View file

@ -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;
}
};

View file

@ -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',
});

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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;
}

View 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,
};
}

View file

@ -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 }}
/>
</>
);
};

View file

@ -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} />;
};

View file

@ -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';

View file

@ -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};
`;

View file

@ -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

View file

@ -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>
);
};

View file

@ -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,

View file

@ -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,
}
);
};

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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 />;

View file

@ -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);

View file

@ -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({

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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',
}),
};

View file

@ -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}`);
}
};

View file

@ -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.',
}),
},
],
},
});

View file

@ -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) => {

View file

@ -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 {

View file

@ -8,3 +8,4 @@
export * from './errors';
export * from './metrics_hosts_anomalies';
export * from './metrics_k8s_anomalies';
export { MappedAnomalyHit } from './common';

View file

@ -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
)

View file

@ -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
)

View file

@ -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,
},
},
],
},
},
},
},
];

View file

@ -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,

View file

@ -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,

View file

@ -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',

View file

@ -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');
}

View file

@ -53,7 +53,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
hasMoreEntries,
timing,
} = await getMetricsHostsAnomalies(
requestContext,
requestContext.infra,
sourceId,
anomalyThreshold,
startTime,

View file

@ -52,7 +52,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
hasMoreEntries,
timing,
} = await getMetricK8sAnomalies(
requestContext,
requestContext.infra,
sourceId,
anomalyThreshold,
startTime,

View file

@ -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 アプリでこのカテゴリーを分析します。",

View file

@ -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 应用中分析此类别。",