[APM] Alerting: Add global option to create all alert types (#78151)
* adding alert to service page * sending on alert per service environment and transaction type * addressing PR comment * addressing PR comment Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
88df93bed5
commit
966f00ac59
|
@ -4,8 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { AlertType } from '../../../../../../common/alert_types';
|
||||
import { AlertAdd } from '../../../../../../../triggers_actions_ui/public';
|
||||
import { AlertType } from '../../../../common/alert_types';
|
||||
import { AlertAdd } from '../../../../../triggers_actions_ui/public';
|
||||
|
||||
type AlertAddProps = React.ComponentProps<typeof AlertAdd>;
|
||||
|
|
@ -34,11 +34,17 @@ export function ServiceAlertTrigger(props: Props) {
|
|||
|
||||
useEffect(() => {
|
||||
// we only want to run this on mount to set default values
|
||||
setAlertProperty('name', `${alertTypeName} | ${params.serviceName}`);
|
||||
setAlertProperty('tags', [
|
||||
'apm',
|
||||
`service.name:${params.serviceName}`.toLowerCase(),
|
||||
]);
|
||||
|
||||
const alertName = params.serviceName
|
||||
? `${alertTypeName} | ${params.serviceName}`
|
||||
: alertTypeName;
|
||||
setAlertProperty('name', alertName);
|
||||
|
||||
const tags = ['apm'];
|
||||
if (params.serviceName) {
|
||||
tags.push(`service.name:${params.serviceName}`.toLowerCase());
|
||||
}
|
||||
setAlertProperty('tags', tags);
|
||||
Object.keys(params).forEach((key) => {
|
||||
setAlertParams(key, params[key]);
|
||||
});
|
||||
|
|
|
@ -90,16 +90,16 @@ export function TransactionDurationAlertTrigger(props: Props) {
|
|||
|
||||
const fields = [
|
||||
<ServiceField value={serviceName} />,
|
||||
<EnvironmentField
|
||||
currentValue={params.environment}
|
||||
options={environmentOptions}
|
||||
onChange={(e) => setAlertParams('environment', e.target.value)}
|
||||
/>,
|
||||
<TransactionTypeField
|
||||
currentValue={params.transactionType}
|
||||
options={transactionTypes.map((key) => ({ text: key, value: key }))}
|
||||
onChange={(e) => setAlertParams('transactionType', e.target.value)}
|
||||
/>,
|
||||
<EnvironmentField
|
||||
currentValue={params.environment}
|
||||
options={environmentOptions}
|
||||
onChange={(e) => setAlertParams('environment', e.target.value)}
|
||||
/>,
|
||||
<PopoverExpression
|
||||
value={params.aggregationType}
|
||||
title={i18n.translate('xpack.apm.transactionDurationAlertTrigger.when', {
|
||||
|
|
|
@ -19,10 +19,6 @@ import {
|
|||
SelectAnomalySeverity,
|
||||
} from './SelectAnomalySeverity';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import {
|
||||
TRANSACTION_PAGE_LOAD,
|
||||
TRANSACTION_REQUEST,
|
||||
} from '../../../../common/transaction_types';
|
||||
import {
|
||||
EnvironmentField,
|
||||
ServiceField,
|
||||
|
@ -32,8 +28,8 @@ import {
|
|||
interface Params {
|
||||
windowSize: number;
|
||||
windowUnit: string;
|
||||
serviceName: string;
|
||||
transactionType: string;
|
||||
serviceName?: string;
|
||||
transactionType?: string;
|
||||
environment: string;
|
||||
anomalySeverityType:
|
||||
| ANOMALY_SEVERITY.CRITICAL
|
||||
|
@ -53,23 +49,17 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
|
|||
const { urlParams } = useUrlParams();
|
||||
const transactionTypes = useServiceTransactionTypes(urlParams);
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
const { start, end } = urlParams;
|
||||
const { start, end, transactionType } = urlParams;
|
||||
const { environmentOptions } = useEnvironments({ serviceName, start, end });
|
||||
const supportedTransactionTypes = transactionTypes.filter((transactionType) =>
|
||||
[TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST].includes(transactionType)
|
||||
);
|
||||
|
||||
if (!supportedTransactionTypes.length || !serviceName) {
|
||||
if (serviceName && !transactionTypes.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 'page-load' for RUM, 'request' otherwise
|
||||
const transactionType = supportedTransactionTypes[0];
|
||||
|
||||
const defaults: Params = {
|
||||
windowSize: 15,
|
||||
windowUnit: 'm',
|
||||
transactionType,
|
||||
transactionType: transactionType || transactionTypes[0],
|
||||
serviceName,
|
||||
environment: urlParams.environment || ENVIRONMENT_ALL.value,
|
||||
anomalySeverityType: ANOMALY_SEVERITY.CRITICAL,
|
||||
|
@ -82,7 +72,11 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
|
|||
|
||||
const fields = [
|
||||
<ServiceField value={serviceName} />,
|
||||
<TransactionTypeField currentValue={transactionType} />,
|
||||
<TransactionTypeField
|
||||
currentValue={params.transactionType}
|
||||
options={transactionTypes.map((key) => ({ text: key, value: key }))}
|
||||
onChange={(e) => setAlertParams('transactionType', e.target.value)}
|
||||
/>,
|
||||
<EnvironmentField
|
||||
currentValue={params.environment}
|
||||
options={environmentOptions}
|
||||
|
|
|
@ -43,7 +43,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) {
|
|||
const { start, end, transactionType } = urlParams;
|
||||
const { environmentOptions } = useEnvironments({ serviceName, start, end });
|
||||
|
||||
if (!transactionTypes.length || !serviceName) {
|
||||
if (serviceName && !transactionTypes.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -62,16 +62,16 @@ export function TransactionErrorRateAlertTrigger(props: Props) {
|
|||
|
||||
const fields = [
|
||||
<ServiceField value={serviceName} />,
|
||||
<EnvironmentField
|
||||
currentValue={params.environment}
|
||||
options={environmentOptions}
|
||||
onChange={(e) => setAlertParams('environment', e.target.value)}
|
||||
/>,
|
||||
<TransactionTypeField
|
||||
currentValue={params.transactionType}
|
||||
options={transactionTypes.map((key) => ({ text: key, value: key }))}
|
||||
onChange={(e) => setAlertParams('transactionType', e.target.value)}
|
||||
/>,
|
||||
<EnvironmentField
|
||||
currentValue={params.environment}
|
||||
options={environmentOptions}
|
||||
onChange={(e) => setAlertParams('environment', e.target.value)}
|
||||
/>,
|
||||
<IsAboveField
|
||||
value={params.threshold}
|
||||
unit="%"
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { ServiceField, TransactionTypeField } from './fields';
|
||||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
import { expectTextsInDocument } from '../../utils/testHelpers';
|
||||
|
||||
describe('alerting fields', () => {
|
||||
describe('Service Fiels', () => {
|
||||
it('renders with value', () => {
|
||||
const component = render(<ServiceField value="foo" />);
|
||||
expectTextsInDocument(component, ['foo']);
|
||||
});
|
||||
it('renders with All when value is not defined', () => {
|
||||
const component = render(<ServiceField />);
|
||||
expectTextsInDocument(component, ['All']);
|
||||
});
|
||||
});
|
||||
describe('Transaction Type Field', () => {
|
||||
it('renders select field when multiple options available', () => {
|
||||
const options = [
|
||||
{ text: 'Foo', value: 'foo' },
|
||||
{ text: 'Bar', value: 'bar' },
|
||||
];
|
||||
const { getByText, getByTestId } = render(
|
||||
<TransactionTypeField currentValue="Foo" options={options} />
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByText('Foo'));
|
||||
});
|
||||
|
||||
const selectBar = getByTestId('transactionTypeField');
|
||||
expect(selectBar instanceof HTMLSelectElement).toBeTruthy();
|
||||
const selectOptions = (selectBar as HTMLSelectElement).options;
|
||||
expect(selectOptions.length).toEqual(2);
|
||||
expect(
|
||||
Object.values(selectOptions).map((option) => option.value)
|
||||
).toEqual(['foo', 'bar']);
|
||||
});
|
||||
it('renders read-only field when single option available', () => {
|
||||
const options = [{ text: 'Bar', value: 'bar' }];
|
||||
const component = render(
|
||||
<TransactionTypeField currentValue="Bar" options={options} />
|
||||
);
|
||||
expectTextsInDocument(component, ['Bar']);
|
||||
});
|
||||
it('renders read-only All option when no option available', () => {
|
||||
const component = render(<TransactionTypeField currentValue="" />);
|
||||
expectTextsInDocument(component, ['All']);
|
||||
});
|
||||
|
||||
it('renders current value when available', () => {
|
||||
const component = render(<TransactionTypeField currentValue="foo" />);
|
||||
expectTextsInDocument(component, ['foo']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11,13 +11,17 @@ import { EuiSelectOption } from '@elastic/eui';
|
|||
import { getEnvironmentLabel } from '../../../common/environment_filter_values';
|
||||
import { PopoverExpression } from './ServiceAlertTrigger/PopoverExpression';
|
||||
|
||||
const ALL_OPTION = i18n.translate('xpack.apm.alerting.fields.all_option', {
|
||||
defaultMessage: 'All',
|
||||
});
|
||||
|
||||
export function ServiceField({ value }: { value?: string }) {
|
||||
return (
|
||||
<EuiExpression
|
||||
description={i18n.translate('xpack.apm.alerting.fields.service', {
|
||||
defaultMessage: 'Service',
|
||||
})}
|
||||
value={value}
|
||||
value={value || ALL_OPTION}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -53,7 +57,7 @@ export function TransactionTypeField({
|
|||
options,
|
||||
onChange,
|
||||
}: {
|
||||
currentValue: string;
|
||||
currentValue?: string;
|
||||
options?: EuiSelectOption[];
|
||||
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
}) {
|
||||
|
@ -61,13 +65,16 @@ export function TransactionTypeField({
|
|||
defaultMessage: 'Type',
|
||||
});
|
||||
|
||||
if (!options || options.length === 1) {
|
||||
return <EuiExpression description={label} value={currentValue} />;
|
||||
if (!options || options.length <= 1) {
|
||||
return (
|
||||
<EuiExpression description={label} value={currentValue || ALL_OPTION} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverExpression value={currentValue} title={label}>
|
||||
<EuiSelect
|
||||
data-test-subj="transactionTypeField"
|
||||
value={currentValue}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Capabilities } from 'kibana/public';
|
||||
import { ApmPluginSetupDeps } from '../../plugin';
|
||||
|
||||
export const getAlertingCapabilities = (
|
||||
plugins: ApmPluginSetupDeps,
|
||||
capabilities: Capabilities
|
||||
) => {
|
||||
const canReadAlerts = !!capabilities.apm['alerting:show'];
|
||||
const canSaveAlerts = !!capabilities.apm['alerting:save'];
|
||||
const isAlertingPluginEnabled = 'alerts' in plugins;
|
||||
const isAlertingAvailable =
|
||||
isAlertingPluginEnabled && (canReadAlerts || canSaveAlerts);
|
||||
const isMlPluginEnabled = 'ml' in plugins;
|
||||
const canReadAnomalies = !!(
|
||||
isMlPluginEnabled &&
|
||||
capabilities.ml.canAccessML &&
|
||||
capabilities.ml.canGetJobs
|
||||
);
|
||||
|
||||
return {
|
||||
isAlertingAvailable,
|
||||
canReadAlerts,
|
||||
canSaveAlerts,
|
||||
canReadAnomalies,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenu,
|
||||
EuiContextMenuPanelDescriptor,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { AlertType } from '../../../../../common/alert_types';
|
||||
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
|
||||
import { AlertingFlyout } from '../../../alerting/AlertingFlyout';
|
||||
|
||||
const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', {
|
||||
defaultMessage: 'Alerts',
|
||||
});
|
||||
const transactionDurationLabel = i18n.translate(
|
||||
'xpack.apm.home.alertsMenu.transactionDuration',
|
||||
{ defaultMessage: 'Transaction duration' }
|
||||
);
|
||||
const transactionErrorRateLabel = i18n.translate(
|
||||
'xpack.apm.home.alertsMenu.transactionErrorRate',
|
||||
{ defaultMessage: 'Transaction error rate' }
|
||||
);
|
||||
const errorCountLabel = i18n.translate('xpack.apm.home.alertsMenu.errorCount', {
|
||||
defaultMessage: 'Error count',
|
||||
});
|
||||
const createThresholdAlertLabel = i18n.translate(
|
||||
'xpack.apm.home.alertsMenu.createThresholdAlert',
|
||||
{ defaultMessage: 'Create threshold alert' }
|
||||
);
|
||||
const createAnomalyAlertAlertLabel = i18n.translate(
|
||||
'xpack.apm.home.alertsMenu.createAnomalyAlert',
|
||||
{ defaultMessage: 'Create anomaly alert' }
|
||||
);
|
||||
|
||||
const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID =
|
||||
'create_transaction_duration_panel';
|
||||
const CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID =
|
||||
'create_transaction_error_rate_panel';
|
||||
const CREATE_ERROR_COUNT_ALERT_PANEL_ID = 'create_error_count_panel';
|
||||
|
||||
interface Props {
|
||||
canReadAlerts: boolean;
|
||||
canSaveAlerts: boolean;
|
||||
canReadAnomalies: boolean;
|
||||
}
|
||||
|
||||
export function AlertingPopoverAndFlyout(props: Props) {
|
||||
const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props;
|
||||
|
||||
const plugin = useApmPluginContext();
|
||||
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
|
||||
const [alertType, setAlertType] = useState<AlertType | null>(null);
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={() => setPopoverOpen(true)}
|
||||
>
|
||||
{alertLabel}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const panels: EuiContextMenuPanelDescriptor[] = [
|
||||
{
|
||||
id: 0,
|
||||
title: alertLabel,
|
||||
items: [
|
||||
...(canSaveAlerts
|
||||
? [
|
||||
{
|
||||
name: transactionDurationLabel,
|
||||
panel: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID,
|
||||
},
|
||||
{
|
||||
name: transactionErrorRateLabel,
|
||||
panel: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID,
|
||||
},
|
||||
{
|
||||
name: errorCountLabel,
|
||||
panel: CREATE_ERROR_COUNT_ALERT_PANEL_ID,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(canReadAlerts
|
||||
? [
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.apm.home.alertsMenu.viewActiveAlerts',
|
||||
{ defaultMessage: 'View active alerts' }
|
||||
),
|
||||
href: plugin.core.http.basePath.prepend(
|
||||
'/app/management/insightsAndAlerting/triggersActions/alerts'
|
||||
),
|
||||
icon: 'tableOfContents',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
|
||||
// transaction duration panel
|
||||
{
|
||||
id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID,
|
||||
title: transactionDurationLabel,
|
||||
items: [
|
||||
// anomaly alerts
|
||||
...(canReadAnomalies
|
||||
? [
|
||||
{
|
||||
name: createAnomalyAlertAlertLabel,
|
||||
onClick: () => {
|
||||
setAlertType(AlertType.TransactionDurationAnomaly);
|
||||
setPopoverOpen(false);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
|
||||
// transaction error rate panel
|
||||
{
|
||||
id: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID,
|
||||
title: transactionErrorRateLabel,
|
||||
items: [
|
||||
// threshold alerts
|
||||
{
|
||||
name: createThresholdAlertLabel,
|
||||
onClick: () => {
|
||||
setAlertType(AlertType.TransactionErrorRate);
|
||||
setPopoverOpen(false);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// error alerts panel
|
||||
{
|
||||
id: CREATE_ERROR_COUNT_ALERT_PANEL_ID,
|
||||
title: errorCountLabel,
|
||||
items: [
|
||||
{
|
||||
name: createThresholdAlertLabel,
|
||||
onClick: () => {
|
||||
setAlertType(AlertType.ErrorCount);
|
||||
setPopoverOpen(false);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
id="integrations-menu"
|
||||
button={button}
|
||||
isOpen={popoverOpen}
|
||||
closePopover={() => setPopoverOpen(false)}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
<AlertingFlyout
|
||||
alertType={alertType}
|
||||
addFlyoutVisible={!!alertType}
|
||||
setAddFlyoutVisibility={(visible) => {
|
||||
if (!visible) {
|
||||
setAlertType(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -15,17 +15,19 @@ import { i18n } from '@kbn/i18n';
|
|||
import React from 'react';
|
||||
import { $ElementType } from 'utility-types';
|
||||
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
|
||||
import { getAlertingCapabilities } from '../../alerting/get_alert_capabilities';
|
||||
import { ApmHeader } from '../../shared/ApmHeader';
|
||||
import { EuiTabLink } from '../../shared/EuiTabLink';
|
||||
import { AnomalyDetectionSetupLink } from '../../shared/Links/apm/AnomalyDetectionSetupLink';
|
||||
import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink';
|
||||
import { ServiceOverviewLink } from '../../shared/Links/apm/ServiceOverviewLink';
|
||||
import { SettingsLink } from '../../shared/Links/apm/SettingsLink';
|
||||
import { AnomalyDetectionSetupLink } from '../../shared/Links/apm/AnomalyDetectionSetupLink';
|
||||
import { TraceOverviewLink } from '../../shared/Links/apm/TraceOverviewLink';
|
||||
import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink';
|
||||
import { ServiceMap } from '../ServiceMap';
|
||||
import { ServiceOverview } from '../ServiceOverview';
|
||||
import { TraceOverview } from '../TraceOverview';
|
||||
import { AlertingPopoverAndFlyout } from './alerting_popover_flyout';
|
||||
|
||||
function getHomeTabs({
|
||||
serviceMapEnabled = true,
|
||||
|
@ -83,13 +85,21 @@ interface Props {
|
|||
}
|
||||
|
||||
export function Home({ tab }: Props) {
|
||||
const { config, core } = useApmPluginContext();
|
||||
const canAccessML = !!core.application.capabilities.ml?.canAccessML;
|
||||
const { config, core, plugins } = useApmPluginContext();
|
||||
const capabilities = core.application.capabilities;
|
||||
const canAccessML = !!capabilities.ml?.canAccessML;
|
||||
const homeTabs = getHomeTabs(config);
|
||||
const selectedTab = homeTabs.find(
|
||||
(homeTab) => homeTab.name === tab
|
||||
) as $ElementType<typeof homeTabs, number>;
|
||||
|
||||
const {
|
||||
isAlertingAvailable,
|
||||
canReadAlerts,
|
||||
canSaveAlerts,
|
||||
canReadAnomalies,
|
||||
} = getAlertingCapabilities(plugins, core.application.capabilities);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApmHeader>
|
||||
|
@ -106,6 +116,15 @@ export function Home({ tab }: Props) {
|
|||
</EuiButtonEmpty>
|
||||
</SettingsLink>
|
||||
</EuiFlexItem>
|
||||
{isAlertingAvailable && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertingPopoverAndFlyout
|
||||
canReadAlerts={canReadAlerts}
|
||||
canSaveAlerts={canSaveAlerts}
|
||||
canReadAnomalies={canReadAnomalies}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{canAccessML && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AnomalyDetectionSetupLink />
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenu,
|
||||
EuiPopover,
|
||||
EuiContextMenuPanelDescriptor,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { AlertType } from '../../../../../common/alert_types';
|
||||
import { AlertingFlyout } from './AlertingFlyout';
|
||||
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
|
||||
import { AlertingFlyout } from '../../../alerting/AlertingFlyout';
|
||||
|
||||
const alertLabel = i18n.translate(
|
||||
'xpack.apm.serviceDetails.alertsMenu.alerts',
|
||||
|
@ -53,7 +53,7 @@ interface Props {
|
|||
canReadAnomalies: boolean;
|
||||
}
|
||||
|
||||
export function AlertIntegrations(props: Props) {
|
||||
export function AlertingPopoverAndFlyout(props: Props) {
|
||||
const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props;
|
||||
|
||||
const plugin = useApmPluginContext();
|
|
@ -14,8 +14,9 @@ import { i18n } from '@kbn/i18n';
|
|||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
|
||||
import { getAlertingCapabilities } from '../../alerting/get_alert_capabilities';
|
||||
import { ApmHeader } from '../../shared/ApmHeader';
|
||||
import { AlertIntegrations } from './AlertIntegrations';
|
||||
import { AlertingPopoverAndFlyout } from './alerting_popover_flyout';
|
||||
import { ServiceDetailTabs } from './ServiceDetailTabs';
|
||||
|
||||
interface Props extends RouteComponentProps<{ serviceName: string }> {
|
||||
|
@ -23,20 +24,15 @@ interface Props extends RouteComponentProps<{ serviceName: string }> {
|
|||
}
|
||||
|
||||
export function ServiceDetails({ match, tab }: Props) {
|
||||
const plugin = useApmPluginContext();
|
||||
const { core, plugins } = useApmPluginContext();
|
||||
const { serviceName } = match.params;
|
||||
const capabilities = plugin.core.application.capabilities;
|
||||
const canReadAlerts = !!capabilities.apm['alerting:show'];
|
||||
const canSaveAlerts = !!capabilities.apm['alerting:save'];
|
||||
const isAlertingPluginEnabled = 'alerts' in plugin.plugins;
|
||||
const isAlertingAvailable =
|
||||
isAlertingPluginEnabled && (canReadAlerts || canSaveAlerts);
|
||||
const isMlPluginEnabled = 'ml' in plugin.plugins;
|
||||
const canReadAnomalies = !!(
|
||||
isMlPluginEnabled &&
|
||||
capabilities.ml.canAccessML &&
|
||||
capabilities.ml.canGetJobs
|
||||
);
|
||||
|
||||
const {
|
||||
isAlertingAvailable,
|
||||
canReadAlerts,
|
||||
canSaveAlerts,
|
||||
canReadAnomalies,
|
||||
} = getAlertingCapabilities(plugins, core.application.capabilities);
|
||||
|
||||
const ADD_DATA_LABEL = i18n.translate('xpack.apm.addDataButtonLabel', {
|
||||
defaultMessage: 'Add data',
|
||||
|
@ -53,7 +49,7 @@ export function ServiceDetails({ match, tab }: Props) {
|
|||
</EuiFlexItem>
|
||||
{isAlertingAvailable && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertIntegrations
|
||||
<AlertingPopoverAndFlyout
|
||||
canReadAlerts={canReadAlerts}
|
||||
canSaveAlerts={canSaveAlerts}
|
||||
canReadAnomalies={canReadAnomalies}
|
||||
|
@ -62,9 +58,7 @@ export function ServiceDetails({ match, tab }: Props) {
|
|||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
href={plugin.core.http.basePath.prepend(
|
||||
'/app/home#/tutorial/apm'
|
||||
)}
|
||||
href={core.http.basePath.prepend('/app/home#/tutorial/apm')}
|
||||
size="s"
|
||||
color="primary"
|
||||
iconType="plusInCircle"
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import * as Rx from 'rxjs';
|
||||
import { toArray, map } from 'rxjs/operators';
|
||||
|
||||
import { AlertingPlugin } from '../../../../alerts/server';
|
||||
import { APMConfig } from '../..';
|
||||
|
||||
import { registerErrorCountAlertType } from './register_error_count_alert_type';
|
||||
|
||||
type Operator<T1, T2> = (source: Rx.Observable<T1>) => Rx.Observable<T2>;
|
||||
const pipeClosure = <T1, T2>(fn: Operator<T1, T2>): Operator<T1, T2> => {
|
||||
return (source: Rx.Observable<T1>) => {
|
||||
return Rx.defer(() => fn(source));
|
||||
};
|
||||
};
|
||||
const mockedConfig$ = (Rx.of('apm_oss.errorIndices').pipe(
|
||||
pipeClosure((source$) => {
|
||||
return source$.pipe(map((i) => i));
|
||||
}),
|
||||
toArray()
|
||||
) as unknown) as Observable<APMConfig>;
|
||||
|
||||
describe('Error count alert', () => {
|
||||
it("doesn't send an alert when error count is less than threshold", async () => {
|
||||
let alertExecutor: any;
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
registerErrorCountAlertType({
|
||||
alerts,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const services = {
|
||||
callCluster: jest.fn(() => ({
|
||||
hits: {
|
||||
total: {
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
})),
|
||||
alertInstanceFactory: jest.fn(),
|
||||
};
|
||||
const params = { threshold: 1 };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
expect(services.alertInstanceFactory).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('sends alerts with service name and environment', async () => {
|
||||
let alertExecutor: any;
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
registerErrorCountAlertType({
|
||||
alerts,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
const services = {
|
||||
callCluster: jest.fn(() => ({
|
||||
hits: {
|
||||
total: {
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
services: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'foo',
|
||||
environments: {
|
||||
buckets: [{ key: 'env-foo' }, { key: 'env-foo-2' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'bar',
|
||||
environments: {
|
||||
buckets: [{ key: 'env-bar' }, { key: 'env-bar-2' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})),
|
||||
alertInstanceFactory: jest.fn(() => ({ scheduleActions })),
|
||||
};
|
||||
const params = { threshold: 1 };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
[
|
||||
'apm.error_rate_foo_env-foo',
|
||||
'apm.error_rate_foo_env-foo-2',
|
||||
'apm.error_rate_bar_env-bar',
|
||||
'apm.error_rate_bar_env-bar-2',
|
||||
].forEach((instanceName) =>
|
||||
expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName)
|
||||
);
|
||||
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'foo',
|
||||
environment: 'env-foo',
|
||||
threshold: 1,
|
||||
triggerValue: 2,
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'foo',
|
||||
environment: 'env-foo-2',
|
||||
threshold: 1,
|
||||
triggerValue: 2,
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'bar',
|
||||
environment: 'env-bar',
|
||||
threshold: 1,
|
||||
triggerValue: 2,
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'bar',
|
||||
environment: 'env-bar-2',
|
||||
threshold: 1,
|
||||
triggerValue: 2,
|
||||
});
|
||||
});
|
||||
it('sends alerts with service name', async () => {
|
||||
let alertExecutor: any;
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
registerErrorCountAlertType({
|
||||
alerts,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
const services = {
|
||||
callCluster: jest.fn(() => ({
|
||||
hits: {
|
||||
total: {
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
services: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'foo',
|
||||
},
|
||||
{
|
||||
key: 'bar',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})),
|
||||
alertInstanceFactory: jest.fn(() => ({ scheduleActions })),
|
||||
};
|
||||
const params = { threshold: 1 };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
['apm.error_rate_foo', 'apm.error_rate_bar'].forEach((instanceName) =>
|
||||
expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName)
|
||||
);
|
||||
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'foo',
|
||||
environment: undefined,
|
||||
threshold: 1,
|
||||
triggerValue: 2,
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'bar',
|
||||
environment: undefined,
|
||||
threshold: 1,
|
||||
triggerValue: 2,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,22 +5,21 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es';
|
||||
import { APMConfig } from '../..';
|
||||
import { AlertingPlugin } from '../../../../alerts/server';
|
||||
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
|
||||
import {
|
||||
ESSearchResponse,
|
||||
ESSearchRequest,
|
||||
} from '../../../typings/elasticsearch';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { AlertingPlugin } from '../../../../alerts/server';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { ESSearchResponse } from '../../../typings/elasticsearch';
|
||||
import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es';
|
||||
import { getApmIndices } from '../settings/apm_indices/get_apm_indices';
|
||||
import { APMConfig } from '../..';
|
||||
import { apmActionVariables } from './action_variables';
|
||||
|
||||
interface RegisterAlertParams {
|
||||
|
@ -32,7 +31,7 @@ const paramsSchema = schema.object({
|
|||
windowSize: schema.number(),
|
||||
windowUnit: schema.string(),
|
||||
threshold: schema.number(),
|
||||
serviceName: schema.string(),
|
||||
serviceName: schema.maybe(schema.string()),
|
||||
environment: schema.string(),
|
||||
});
|
||||
|
||||
|
@ -83,30 +82,74 @@ export function registerErrorCountAlertType({
|
|||
},
|
||||
},
|
||||
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.error } },
|
||||
{ term: { [SERVICE_NAME]: alertParams.serviceName } },
|
||||
...(alertParams.serviceName
|
||||
? [{ term: { [SERVICE_NAME]: alertParams.serviceName } }]
|
||||
: []),
|
||||
...getEnvironmentUiFilterES(alertParams.environment),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
services: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: 50,
|
||||
},
|
||||
aggs: {
|
||||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response: ESSearchResponse<
|
||||
unknown,
|
||||
ESSearchRequest
|
||||
typeof searchParams
|
||||
> = await services.callCluster('search', searchParams);
|
||||
|
||||
const errorCount = response.hits.total.value;
|
||||
|
||||
if (errorCount > alertParams.threshold) {
|
||||
const alertInstance = services.alertInstanceFactory(
|
||||
AlertType.ErrorCount
|
||||
);
|
||||
alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
|
||||
serviceName: alertParams.serviceName,
|
||||
environment: alertParams.environment,
|
||||
threshold: alertParams.threshold,
|
||||
triggerValue: errorCount,
|
||||
function scheduleAction({
|
||||
serviceName,
|
||||
environment,
|
||||
}: {
|
||||
serviceName: string;
|
||||
environment?: string;
|
||||
}) {
|
||||
const alertInstanceName = [
|
||||
AlertType.ErrorCount,
|
||||
serviceName,
|
||||
environment,
|
||||
]
|
||||
.filter((name) => name)
|
||||
.join('_');
|
||||
|
||||
const alertInstance = services.alertInstanceFactory(
|
||||
alertInstanceName
|
||||
);
|
||||
alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
|
||||
serviceName,
|
||||
environment,
|
||||
threshold: alertParams.threshold,
|
||||
triggerValue: errorCount,
|
||||
});
|
||||
}
|
||||
response.aggregations?.services.buckets.forEach((serviceBucket) => {
|
||||
const serviceName = serviceBucket.key as string;
|
||||
if (isEmpty(serviceBucket.environments?.buckets)) {
|
||||
scheduleAction({ serviceName });
|
||||
} else {
|
||||
serviceBucket.environments.buckets.forEach((envBucket) => {
|
||||
const environment = envBucket.key as string;
|
||||
scheduleAction({ serviceName, environment });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,326 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import * as Rx from 'rxjs';
|
||||
import { toArray, map } from 'rxjs/operators';
|
||||
import { AlertingPlugin } from '../../../../alerts/server';
|
||||
import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type';
|
||||
import { APMConfig } from '../..';
|
||||
import { ANOMALY_SEVERITY } from '../../../../ml/common';
|
||||
import { Job, MlPluginSetup } from '../../../../ml/server';
|
||||
import * as GetServiceAnomalies from '../service_map/get_service_anomalies';
|
||||
|
||||
type Operator<T1, T2> = (source: Rx.Observable<T1>) => Rx.Observable<T2>;
|
||||
const pipeClosure = <T1, T2>(fn: Operator<T1, T2>): Operator<T1, T2> => {
|
||||
return (source: Rx.Observable<T1>) => {
|
||||
return Rx.defer(() => fn(source));
|
||||
};
|
||||
};
|
||||
const mockedConfig$ = (Rx.of('apm_oss.errorIndices').pipe(
|
||||
pipeClosure((source$) => {
|
||||
return source$.pipe(map((i) => i));
|
||||
}),
|
||||
toArray()
|
||||
) as unknown) as Observable<APMConfig>;
|
||||
|
||||
describe('Transaction duration anomaly alert', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe("doesn't send alert", () => {
|
||||
it('ml is not defined', async () => {
|
||||
let alertExecutor: any;
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
registerTransactionDurationAnomalyAlertType({
|
||||
alerts,
|
||||
ml: undefined,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const services = {
|
||||
callCluster: jest.fn(),
|
||||
alertInstanceFactory: jest.fn(),
|
||||
};
|
||||
const params = { anomalySeverityType: ANOMALY_SEVERITY.MINOR };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
expect(services.callCluster).not.toHaveBeenCalled();
|
||||
expect(services.alertInstanceFactory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ml jobs are not available', async () => {
|
||||
jest
|
||||
.spyOn(GetServiceAnomalies, 'getMLJobs')
|
||||
.mockReturnValue(Promise.resolve([]));
|
||||
|
||||
let alertExecutor: any;
|
||||
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
const ml = ({
|
||||
mlSystemProvider: () => ({ mlAnomalySearch: jest.fn() }),
|
||||
anomalyDetectorsProvider: jest.fn(),
|
||||
} as unknown) as MlPluginSetup;
|
||||
|
||||
registerTransactionDurationAnomalyAlertType({
|
||||
alerts,
|
||||
ml,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const services = {
|
||||
callCluster: jest.fn(),
|
||||
alertInstanceFactory: jest.fn(),
|
||||
};
|
||||
const params = { anomalySeverityType: ANOMALY_SEVERITY.MINOR };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
expect(services.callCluster).not.toHaveBeenCalled();
|
||||
expect(services.alertInstanceFactory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('anomaly is less than threshold', async () => {
|
||||
jest
|
||||
.spyOn(GetServiceAnomalies, 'getMLJobs')
|
||||
.mockReturnValue(
|
||||
Promise.resolve([{ job_id: '1' }, { job_id: '2' }] as Job[])
|
||||
);
|
||||
|
||||
let alertExecutor: any;
|
||||
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
const ml = ({
|
||||
mlSystemProvider: () => ({
|
||||
mlAnomalySearch: () => ({
|
||||
hits: { total: { value: 0 } },
|
||||
}),
|
||||
}),
|
||||
anomalyDetectorsProvider: jest.fn(),
|
||||
} as unknown) as MlPluginSetup;
|
||||
|
||||
registerTransactionDurationAnomalyAlertType({
|
||||
alerts,
|
||||
ml,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const services = {
|
||||
callCluster: jest.fn(),
|
||||
alertInstanceFactory: jest.fn(),
|
||||
};
|
||||
const params = { anomalySeverityType: ANOMALY_SEVERITY.MINOR };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
expect(services.callCluster).not.toHaveBeenCalled();
|
||||
expect(services.alertInstanceFactory).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sends alert', () => {
|
||||
it('with service name, environment and transaction type', async () => {
|
||||
jest.spyOn(GetServiceAnomalies, 'getMLJobs').mockReturnValue(
|
||||
Promise.resolve([
|
||||
{
|
||||
job_id: '1',
|
||||
custom_settings: {
|
||||
job_tags: {
|
||||
environment: 'production',
|
||||
},
|
||||
},
|
||||
} as unknown,
|
||||
{
|
||||
job_id: '2',
|
||||
custom_settings: {
|
||||
job_tags: {
|
||||
environment: 'production',
|
||||
},
|
||||
},
|
||||
} as unknown,
|
||||
] as Job[])
|
||||
);
|
||||
|
||||
let alertExecutor: any;
|
||||
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
const ml = ({
|
||||
mlSystemProvider: () => ({
|
||||
mlAnomalySearch: () => ({
|
||||
hits: { total: { value: 2 } },
|
||||
aggregations: {
|
||||
services: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'foo',
|
||||
transaction_types: {
|
||||
buckets: [{ key: 'type-foo' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'bar',
|
||||
transaction_types: {
|
||||
buckets: [{ key: 'type-bar' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
anomalyDetectorsProvider: jest.fn(),
|
||||
} as unknown) as MlPluginSetup;
|
||||
|
||||
registerTransactionDurationAnomalyAlertType({
|
||||
alerts,
|
||||
ml,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
const services = {
|
||||
callCluster: jest.fn(),
|
||||
alertInstanceFactory: jest.fn(() => ({ scheduleActions })),
|
||||
};
|
||||
const params = { anomalySeverityType: ANOMALY_SEVERITY.MINOR };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
[
|
||||
'apm.transaction_duration_anomaly_foo_production_type-foo',
|
||||
'apm.transaction_duration_anomaly_bar_production_type-bar',
|
||||
].forEach((instanceName) =>
|
||||
expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName)
|
||||
);
|
||||
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'foo',
|
||||
transactionType: 'type-foo',
|
||||
environment: 'production',
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'bar',
|
||||
transactionType: 'type-bar',
|
||||
environment: 'production',
|
||||
});
|
||||
});
|
||||
|
||||
it('with service name', async () => {
|
||||
jest.spyOn(GetServiceAnomalies, 'getMLJobs').mockReturnValue(
|
||||
Promise.resolve([
|
||||
{
|
||||
job_id: '1',
|
||||
custom_settings: {
|
||||
job_tags: {
|
||||
environment: 'production',
|
||||
},
|
||||
},
|
||||
} as unknown,
|
||||
{
|
||||
job_id: '2',
|
||||
custom_settings: {
|
||||
job_tags: {
|
||||
environment: 'testing',
|
||||
},
|
||||
},
|
||||
} as unknown,
|
||||
] as Job[])
|
||||
);
|
||||
|
||||
let alertExecutor: any;
|
||||
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
const ml = ({
|
||||
mlSystemProvider: () => ({
|
||||
mlAnomalySearch: () => ({
|
||||
hits: { total: { value: 2 } },
|
||||
aggregations: {
|
||||
services: {
|
||||
buckets: [{ key: 'foo' }, { key: 'bar' }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
anomalyDetectorsProvider: jest.fn(),
|
||||
} as unknown) as MlPluginSetup;
|
||||
|
||||
registerTransactionDurationAnomalyAlertType({
|
||||
alerts,
|
||||
ml,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
const services = {
|
||||
callCluster: jest.fn(),
|
||||
alertInstanceFactory: jest.fn(() => ({ scheduleActions })),
|
||||
};
|
||||
const params = { anomalySeverityType: ANOMALY_SEVERITY.MINOR };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
[
|
||||
'apm.transaction_duration_anomaly_foo_production',
|
||||
'apm.transaction_duration_anomaly_foo_testing',
|
||||
'apm.transaction_duration_anomaly_bar_production',
|
||||
'apm.transaction_duration_anomaly_bar_testing',
|
||||
].forEach((instanceName) =>
|
||||
expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName)
|
||||
);
|
||||
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'foo',
|
||||
transactionType: undefined,
|
||||
environment: 'production',
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'bar',
|
||||
transactionType: undefined,
|
||||
environment: 'production',
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'foo',
|
||||
transactionType: undefined,
|
||||
environment: 'testing',
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'bar',
|
||||
transactionType: undefined,
|
||||
environment: 'testing',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { Observable } from 'rxjs';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ANOMALY_SEVERITY } from '../../../../ml/common';
|
||||
import { KibanaRequest } from '../../../../../../src/core/server';
|
||||
import {
|
||||
|
@ -16,7 +17,7 @@ import {
|
|||
import { AlertingPlugin } from '../../../../alerts/server';
|
||||
import { APMConfig } from '../..';
|
||||
import { MlPluginSetup } from '../../../../ml/server';
|
||||
import { getMLJobIds } from '../service_map/get_service_anomalies';
|
||||
import { getMLJobs } from '../service_map/get_service_anomalies';
|
||||
import { apmActionVariables } from './action_variables';
|
||||
|
||||
interface RegisterAlertParams {
|
||||
|
@ -26,8 +27,8 @@ interface RegisterAlertParams {
|
|||
}
|
||||
|
||||
const paramsSchema = schema.object({
|
||||
serviceName: schema.string(),
|
||||
transactionType: schema.string(),
|
||||
serviceName: schema.maybe(schema.string()),
|
||||
transactionType: schema.maybe(schema.string()),
|
||||
windowSize: schema.number(),
|
||||
windowUnit: schema.string(),
|
||||
environment: schema.string(),
|
||||
|
@ -72,10 +73,7 @@ export function registerTransactionDurationAnomalyAlertType({
|
|||
const { mlAnomalySearch } = ml.mlSystemProvider(request);
|
||||
const anomalyDetectors = ml.anomalyDetectorsProvider(request);
|
||||
|
||||
const mlJobIds = await getMLJobIds(
|
||||
anomalyDetectors,
|
||||
alertParams.environment
|
||||
);
|
||||
const mlJobs = await getMLJobs(anomalyDetectors, alertParams.environment);
|
||||
|
||||
const selectedOption = ANOMALY_ALERT_SEVERITY_TYPES.find(
|
||||
(option) => option.type === alertParams.anomalySeverityType
|
||||
|
@ -89,19 +87,19 @@ export function registerTransactionDurationAnomalyAlertType({
|
|||
|
||||
const threshold = selectedOption.threshold;
|
||||
|
||||
if (mlJobIds.length === 0) {
|
||||
if (mlJobs.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const anomalySearchParams = {
|
||||
terminateAfter: 1,
|
||||
body: {
|
||||
terminateAfter: 1,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { result_type: 'record' } },
|
||||
{ terms: { job_id: mlJobIds } },
|
||||
{ terms: { job_id: mlJobs.map((job) => job.job_id) } },
|
||||
{
|
||||
range: {
|
||||
timestamp: {
|
||||
|
@ -110,11 +108,24 @@ export function registerTransactionDurationAnomalyAlertType({
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
partition_field_value: alertParams.serviceName,
|
||||
},
|
||||
},
|
||||
...(alertParams.serviceName
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
partition_field_value: alertParams.serviceName,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(alertParams.transactionType
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
by_field_value: alertParams.transactionType,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
range: {
|
||||
record_score: {
|
||||
|
@ -125,22 +136,82 @@ export function registerTransactionDurationAnomalyAlertType({
|
|||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
services: {
|
||||
terms: {
|
||||
field: 'partition_field_value',
|
||||
size: 50,
|
||||
},
|
||||
aggs: {
|
||||
transaction_types: {
|
||||
terms: {
|
||||
field: 'by_field_value',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = ((await mlAnomalySearch(
|
||||
anomalySearchParams
|
||||
)) as unknown) as { hits: { total: { value: number } } };
|
||||
)) as unknown) as {
|
||||
hits: { total: { value: number } };
|
||||
aggregations?: {
|
||||
services: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
transaction_types: { buckets: Array<{ key: string }> };
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const hitCount = response.hits.total.value;
|
||||
|
||||
if (hitCount > 0) {
|
||||
const alertInstance = services.alertInstanceFactory(
|
||||
AlertType.TransactionDurationAnomaly
|
||||
);
|
||||
alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
|
||||
serviceName: alertParams.serviceName,
|
||||
transactionType: alertParams.transactionType,
|
||||
environment: alertParams.environment,
|
||||
function scheduleAction({
|
||||
serviceName,
|
||||
environment,
|
||||
transactionType,
|
||||
}: {
|
||||
serviceName: string;
|
||||
environment?: string;
|
||||
transactionType?: string;
|
||||
}) {
|
||||
const alertInstanceName = [
|
||||
AlertType.TransactionDurationAnomaly,
|
||||
serviceName,
|
||||
environment,
|
||||
transactionType,
|
||||
]
|
||||
.filter((name) => name)
|
||||
.join('_');
|
||||
|
||||
const alertInstance = services.alertInstanceFactory(
|
||||
alertInstanceName
|
||||
);
|
||||
alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
|
||||
serviceName,
|
||||
environment,
|
||||
transactionType,
|
||||
});
|
||||
}
|
||||
|
||||
mlJobs.map((job) => {
|
||||
const environment = job.custom_settings?.job_tags?.environment;
|
||||
response.aggregations?.services.buckets.forEach((serviceBucket) => {
|
||||
const serviceName = serviceBucket.key as string;
|
||||
if (isEmpty(serviceBucket.transaction_types?.buckets)) {
|
||||
scheduleAction({ serviceName, environment });
|
||||
} else {
|
||||
serviceBucket.transaction_types?.buckets.forEach((typeBucket) => {
|
||||
const transactionType = typeBucket.key as string;
|
||||
scheduleAction({ serviceName, environment, transactionType });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import * as Rx from 'rxjs';
|
||||
import { toArray, map } from 'rxjs/operators';
|
||||
import { AlertingPlugin } from '../../../../alerts/server';
|
||||
import { APMConfig } from '../..';
|
||||
import { registerTransactionErrorRateAlertType } from './register_transaction_error_rate_alert_type';
|
||||
|
||||
type Operator<T1, T2> = (source: Rx.Observable<T1>) => Rx.Observable<T2>;
|
||||
const pipeClosure = <T1, T2>(fn: Operator<T1, T2>): Operator<T1, T2> => {
|
||||
return (source: Rx.Observable<T1>) => {
|
||||
return Rx.defer(() => fn(source));
|
||||
};
|
||||
};
|
||||
const mockedConfig$ = (Rx.of('apm_oss.errorIndices').pipe(
|
||||
pipeClosure((source$) => {
|
||||
return source$.pipe(map((i) => i));
|
||||
}),
|
||||
toArray()
|
||||
) as unknown) as Observable<APMConfig>;
|
||||
|
||||
describe('Transaction error rate alert', () => {
|
||||
it("doesn't send an alert when rate is less than threshold", async () => {
|
||||
let alertExecutor: any;
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
registerTransactionErrorRateAlertType({
|
||||
alerts,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const services = {
|
||||
callCluster: jest.fn(() => ({
|
||||
hits: {
|
||||
total: {
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
})),
|
||||
alertInstanceFactory: jest.fn(),
|
||||
};
|
||||
const params = { threshold: 1 };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
expect(services.alertInstanceFactory).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('sends alerts with service name, transaction type and environment', async () => {
|
||||
let alertExecutor: any;
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
registerTransactionErrorRateAlertType({
|
||||
alerts,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
const services = {
|
||||
callCluster: jest.fn(() => ({
|
||||
hits: {
|
||||
total: {
|
||||
value: 4,
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
erroneous_transactions: {
|
||||
doc_count: 2,
|
||||
},
|
||||
services: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'foo',
|
||||
transaction_types: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'type-foo',
|
||||
environments: {
|
||||
buckets: [{ key: 'env-foo' }, { key: 'env-foo-2' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'bar',
|
||||
transaction_types: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'type-bar',
|
||||
environments: {
|
||||
buckets: [{ key: 'env-bar' }, { key: 'env-bar-2' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})),
|
||||
alertInstanceFactory: jest.fn(() => ({ scheduleActions })),
|
||||
};
|
||||
const params = { threshold: 10 };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
[
|
||||
'apm.transaction_error_rate_foo_type-foo_env-foo',
|
||||
'apm.transaction_error_rate_foo_type-foo_env-foo-2',
|
||||
'apm.transaction_error_rate_bar_type-bar_env-bar',
|
||||
'apm.transaction_error_rate_bar_type-bar_env-bar-2',
|
||||
].forEach((instanceName) =>
|
||||
expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName)
|
||||
);
|
||||
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'foo',
|
||||
transactionType: 'type-foo',
|
||||
environment: 'env-foo',
|
||||
threshold: 10,
|
||||
triggerValue: 50,
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'foo',
|
||||
transactionType: 'type-foo',
|
||||
environment: 'env-foo-2',
|
||||
threshold: 10,
|
||||
triggerValue: 50,
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'bar',
|
||||
transactionType: 'type-bar',
|
||||
environment: 'env-bar',
|
||||
threshold: 10,
|
||||
triggerValue: 50,
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'bar',
|
||||
transactionType: 'type-bar',
|
||||
environment: 'env-bar-2',
|
||||
threshold: 10,
|
||||
triggerValue: 50,
|
||||
});
|
||||
});
|
||||
it('sends alerts with service name and transaction type', async () => {
|
||||
let alertExecutor: any;
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
registerTransactionErrorRateAlertType({
|
||||
alerts,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
const services = {
|
||||
callCluster: jest.fn(() => ({
|
||||
hits: {
|
||||
total: {
|
||||
value: 4,
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
erroneous_transactions: {
|
||||
doc_count: 2,
|
||||
},
|
||||
services: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'foo',
|
||||
transaction_types: {
|
||||
buckets: [{ key: 'type-foo' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'bar',
|
||||
transaction_types: {
|
||||
buckets: [{ key: 'type-bar' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})),
|
||||
alertInstanceFactory: jest.fn(() => ({ scheduleActions })),
|
||||
};
|
||||
const params = { threshold: 10 };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
[
|
||||
'apm.transaction_error_rate_foo_type-foo',
|
||||
'apm.transaction_error_rate_bar_type-bar',
|
||||
].forEach((instanceName) =>
|
||||
expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName)
|
||||
);
|
||||
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'foo',
|
||||
transactionType: 'type-foo',
|
||||
environment: undefined,
|
||||
threshold: 10,
|
||||
triggerValue: 50,
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'bar',
|
||||
transactionType: 'type-bar',
|
||||
environment: undefined,
|
||||
threshold: 10,
|
||||
triggerValue: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends alerts with service name', async () => {
|
||||
let alertExecutor: any;
|
||||
const alerts = {
|
||||
registerType: ({ executor }) => {
|
||||
alertExecutor = executor;
|
||||
},
|
||||
} as AlertingPlugin['setup'];
|
||||
|
||||
registerTransactionErrorRateAlertType({
|
||||
alerts,
|
||||
config$: mockedConfig$,
|
||||
});
|
||||
expect(alertExecutor).toBeDefined();
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
const services = {
|
||||
callCluster: jest.fn(() => ({
|
||||
hits: {
|
||||
total: {
|
||||
value: 4,
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
erroneous_transactions: {
|
||||
doc_count: 2,
|
||||
},
|
||||
services: {
|
||||
buckets: [{ key: 'foo' }, { key: 'bar' }],
|
||||
},
|
||||
},
|
||||
})),
|
||||
alertInstanceFactory: jest.fn(() => ({ scheduleActions })),
|
||||
};
|
||||
const params = { threshold: 10 };
|
||||
|
||||
await alertExecutor!({ services, params });
|
||||
[
|
||||
'apm.transaction_error_rate_foo',
|
||||
'apm.transaction_error_rate_bar',
|
||||
].forEach((instanceName) =>
|
||||
expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName)
|
||||
);
|
||||
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'foo',
|
||||
transactionType: undefined,
|
||||
environment: undefined,
|
||||
threshold: 10,
|
||||
triggerValue: 50,
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('threshold_met', {
|
||||
serviceName: 'bar',
|
||||
transactionType: undefined,
|
||||
environment: undefined,
|
||||
threshold: 10,
|
||||
triggerValue: 50,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,6 +7,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { EventOutcome } from '../../../common/event_outcome';
|
||||
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
|
||||
|
@ -16,6 +17,7 @@ import {
|
|||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
EVENT_OUTCOME,
|
||||
SERVICE_ENVIRONMENT,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { AlertingPlugin } from '../../../../alerts/server';
|
||||
import { getApmIndices } from '../settings/apm_indices/get_apm_indices';
|
||||
|
@ -32,8 +34,8 @@ const paramsSchema = schema.object({
|
|||
windowSize: schema.number(),
|
||||
windowUnit: schema.string(),
|
||||
threshold: schema.number(),
|
||||
transactionType: schema.string(),
|
||||
serviceName: schema.string(),
|
||||
transactionType: schema.maybe(schema.string()),
|
||||
serviceName: schema.maybe(schema.string()),
|
||||
environment: schema.string(),
|
||||
});
|
||||
|
||||
|
@ -84,8 +86,18 @@ export function registerTransactionErrorRateAlertType({
|
|||
},
|
||||
},
|
||||
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
|
||||
{ term: { [SERVICE_NAME]: alertParams.serviceName } },
|
||||
{ term: { [TRANSACTION_TYPE]: alertParams.transactionType } },
|
||||
...(alertParams.serviceName
|
||||
? [{ term: { [SERVICE_NAME]: alertParams.serviceName } }]
|
||||
: []),
|
||||
...(alertParams.transactionType
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
[TRANSACTION_TYPE]: alertParams.transactionType,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...getEnvironmentUiFilterES(alertParams.environment),
|
||||
],
|
||||
},
|
||||
|
@ -94,6 +106,24 @@ export function registerTransactionErrorRateAlertType({
|
|||
erroneous_transactions: {
|
||||
filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } },
|
||||
},
|
||||
services: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: 50,
|
||||
},
|
||||
aggs: {
|
||||
transaction_types: {
|
||||
terms: { field: TRANSACTION_TYPE },
|
||||
aggs: {
|
||||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -114,16 +144,53 @@ export function registerTransactionErrorRateAlertType({
|
|||
(errornousTransactionsCount / totalTransactionCount) * 100;
|
||||
|
||||
if (transactionErrorRate > alertParams.threshold) {
|
||||
const alertInstance = services.alertInstanceFactory(
|
||||
AlertType.TransactionErrorRate
|
||||
);
|
||||
function scheduleAction({
|
||||
serviceName,
|
||||
environment,
|
||||
transactionType,
|
||||
}: {
|
||||
serviceName: string;
|
||||
environment?: string;
|
||||
transactionType?: string;
|
||||
}) {
|
||||
const alertInstanceName = [
|
||||
AlertType.TransactionErrorRate,
|
||||
serviceName,
|
||||
transactionType,
|
||||
environment,
|
||||
]
|
||||
.filter((name) => name)
|
||||
.join('_');
|
||||
|
||||
alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
|
||||
serviceName: alertParams.serviceName,
|
||||
transactionType: alertParams.transactionType,
|
||||
environment: alertParams.environment,
|
||||
threshold: alertParams.threshold,
|
||||
triggerValue: transactionErrorRate,
|
||||
const alertInstance = services.alertInstanceFactory(
|
||||
alertInstanceName
|
||||
);
|
||||
alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
|
||||
serviceName,
|
||||
transactionType,
|
||||
environment,
|
||||
threshold: alertParams.threshold,
|
||||
triggerValue: transactionErrorRate,
|
||||
});
|
||||
}
|
||||
|
||||
response.aggregations?.services.buckets.forEach((serviceBucket) => {
|
||||
const serviceName = serviceBucket.key as string;
|
||||
if (isEmpty(serviceBucket.transaction_types?.buckets)) {
|
||||
scheduleAction({ serviceName });
|
||||
} else {
|
||||
serviceBucket.transaction_types.buckets.forEach((typeBucket) => {
|
||||
const transactionType = typeBucket.key as string;
|
||||
if (isEmpty(typeBucket.environments?.buckets)) {
|
||||
scheduleAction({ serviceName, transactionType });
|
||||
} else {
|
||||
typeBucket.environments.buckets.forEach((envBucket) => {
|
||||
const environment = envBucket.key as string;
|
||||
scheduleAction({ serviceName, transactionType, environment });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -180,7 +180,7 @@ function transformResponseToServiceAnomalies(
|
|||
return serviceAnomaliesMap;
|
||||
}
|
||||
|
||||
export async function getMLJobIds(
|
||||
export async function getMLJobs(
|
||||
anomalyDetectors: ReturnType<MlPluginSetup['anomalyDetectorsProvider']>,
|
||||
environment?: string
|
||||
) {
|
||||
|
@ -198,7 +198,15 @@ export async function getMLJobIds(
|
|||
if (!matchingMLJob) {
|
||||
return [];
|
||||
}
|
||||
return [matchingMLJob.job_id];
|
||||
return [matchingMLJob];
|
||||
}
|
||||
return mlJobs;
|
||||
}
|
||||
|
||||
export async function getMLJobIds(
|
||||
anomalyDetectors: ReturnType<MlPluginSetup['anomalyDetectorsProvider']>,
|
||||
environment?: string
|
||||
) {
|
||||
const mlJobs = await getMLJobs(anomalyDetectors, environment);
|
||||
return mlJobs.map((job) => job.job_id);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue