[Metrics UI] Add inventory metric threshold alerts (#64292)
* Add new inventory metric threshold alert * Add missed file * Fix some types * Convert units on client and executor. * Move formatters to common. Properly format metrics in alert messages * Style changes * Remove unused files * fix test * Update create * Fix signature * Remove old test. Remove unecessary import * Pass in filter when clicking create alert from context menu * Fix filtering * Fix more types * Fix tests * Fix merge * Fix merge Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
f9c1033d41
commit
3cef8e6f30
|
@ -3,9 +3,9 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { InfraWaffleMapDataFormat } from '../../lib/lib';
|
||||
import { InfraWaffleMapDataFormat } from './types';
|
||||
import { createBytesFormatter } from './bytes';
|
||||
|
||||
describe('createDataFormatter', () => {
|
||||
it('should format bytes as bytesDecimal', () => {
|
||||
const formatter = createBytesFormatter(InfraWaffleMapDataFormat.bytesDecimal);
|
|
@ -3,9 +3,8 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { InfraWaffleMapDataFormat } from '../../lib/lib';
|
||||
import { formatNumber } from './number';
|
||||
import { InfraWaffleMapDataFormat } from './types';
|
||||
|
||||
/**
|
||||
* The labels are derived from these two Wikipedia articles.
|
|
@ -5,12 +5,12 @@
|
|||
*/
|
||||
|
||||
import Mustache from 'mustache';
|
||||
import { InfraWaffleMapDataFormat } from '../../lib/lib';
|
||||
import { createBytesFormatter } from './bytes';
|
||||
import { formatNumber } from './number';
|
||||
import { formatPercent } from './percent';
|
||||
import { InventoryFormatterType } from '../../../common/inventory_models/types';
|
||||
import { InventoryFormatterType } from '../inventory_models/types';
|
||||
import { formatHighPercision } from './high_precision';
|
||||
import { InfraWaffleMapDataFormat } from './types';
|
||||
|
||||
export const FORMATTERS = {
|
||||
number: formatNumber,
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
enum InfraFormatterType {
|
||||
number = 'number',
|
||||
abbreviatedNumber = 'abbreviatedNumber',
|
||||
bytes = 'bytes',
|
||||
bits = 'bits',
|
||||
percent = 'percent',
|
||||
}
|
||||
|
||||
interface MetricFormatter {
|
||||
formatter: InfraFormatterType;
|
||||
template: string;
|
||||
bounds?: { min: number; max: number };
|
||||
}
|
||||
|
||||
interface MetricFormatters {
|
||||
[key: string]: MetricFormatter;
|
||||
}
|
||||
|
||||
export const METRIC_FORMATTERS: MetricFormatters = {
|
||||
['count']: { formatter: InfraFormatterType.number, template: '{{value}}' },
|
||||
['cpu']: {
|
||||
formatter: InfraFormatterType.percent,
|
||||
template: '{{value}}',
|
||||
},
|
||||
['memory']: {
|
||||
formatter: InfraFormatterType.percent,
|
||||
template: '{{value}}',
|
||||
},
|
||||
['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' },
|
||||
['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' },
|
||||
['logRate']: {
|
||||
formatter: InfraFormatterType.abbreviatedNumber,
|
||||
template: '{{value}}/s',
|
||||
},
|
||||
['diskIOReadBytes']: {
|
||||
formatter: InfraFormatterType.bytes,
|
||||
template: '{{value}}/s',
|
||||
},
|
||||
['diskIOWriteBytes']: {
|
||||
formatter: InfraFormatterType.bytes,
|
||||
template: '{{value}}/s',
|
||||
},
|
||||
['s3BucketSize']: {
|
||||
formatter: InfraFormatterType.bytes,
|
||||
template: '{{value}}',
|
||||
},
|
||||
['s3TotalRequests']: {
|
||||
formatter: InfraFormatterType.abbreviatedNumber,
|
||||
template: '{{value}}',
|
||||
},
|
||||
['s3NumberOfObjects']: {
|
||||
formatter: InfraFormatterType.abbreviatedNumber,
|
||||
template: '{{value}}',
|
||||
},
|
||||
['s3UploadBytes']: {
|
||||
formatter: InfraFormatterType.bytes,
|
||||
template: '{{value}}',
|
||||
},
|
||||
['s3DownloadBytes']: {
|
||||
formatter: InfraFormatterType.bytes,
|
||||
template: '{{value}}',
|
||||
},
|
||||
['sqsOldestMessage']: {
|
||||
formatter: InfraFormatterType.number,
|
||||
template: '{{value}} seconds',
|
||||
},
|
||||
};
|
11
x-pack/plugins/infra/common/formatters/types.ts
Normal file
11
x-pack/plugins/infra/common/formatters/types.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum InfraWaffleMapDataFormat {
|
||||
bytesDecimal = 'bytesDecimal',
|
||||
bitsDecimal = 'bitsDecimal',
|
||||
abbreviatedNumber = 'abbreviatedNumber',
|
||||
}
|
|
@ -11,27 +11,29 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_
|
|||
import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items';
|
||||
import { SnapshotMetricType } from '../types';
|
||||
|
||||
export const ec2MetricTypes: SnapshotMetricType[] = [
|
||||
'cpu',
|
||||
'rx',
|
||||
'tx',
|
||||
'diskIOReadBytes',
|
||||
'diskIOWriteBytes',
|
||||
];
|
||||
|
||||
export const ec2groupByFields = [
|
||||
'cloud.availability_zone',
|
||||
'cloud.machine.type',
|
||||
'aws.ec2.instance.image.id',
|
||||
'aws.ec2.instance.state.name',
|
||||
];
|
||||
|
||||
export const AwsEC2ToolbarItems = (props: ToolbarProps) => {
|
||||
const metricTypes: SnapshotMetricType[] = [
|
||||
'cpu',
|
||||
'rx',
|
||||
'tx',
|
||||
'diskIOReadBytes',
|
||||
'diskIOWriteBytes',
|
||||
];
|
||||
const groupByFields = [
|
||||
'cloud.availability_zone',
|
||||
'cloud.machine.type',
|
||||
'aws.ec2.instance.image.id',
|
||||
'aws.ec2.instance.state.name',
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<CloudToolbarItems {...props} />
|
||||
<MetricsAndGroupByToolbarItems
|
||||
{...props}
|
||||
metricTypes={metricTypes}
|
||||
groupByFields={groupByFields}
|
||||
metricTypes={ec2MetricTypes}
|
||||
groupByFields={ec2groupByFields}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -11,26 +11,28 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_
|
|||
import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items';
|
||||
import { SnapshotMetricType } from '../types';
|
||||
|
||||
export const rdsMetricTypes: SnapshotMetricType[] = [
|
||||
'cpu',
|
||||
'rdsConnections',
|
||||
'rdsQueriesExecuted',
|
||||
'rdsActiveTransactions',
|
||||
'rdsLatency',
|
||||
];
|
||||
|
||||
export const rdsGroupByFields = [
|
||||
'cloud.availability_zone',
|
||||
'aws.rds.db_instance.class',
|
||||
'aws.rds.db_instance.status',
|
||||
];
|
||||
|
||||
export const AwsRDSToolbarItems = (props: ToolbarProps) => {
|
||||
const metricTypes: SnapshotMetricType[] = [
|
||||
'cpu',
|
||||
'rdsConnections',
|
||||
'rdsQueriesExecuted',
|
||||
'rdsActiveTransactions',
|
||||
'rdsLatency',
|
||||
];
|
||||
const groupByFields = [
|
||||
'cloud.availability_zone',
|
||||
'aws.rds.db_instance.class',
|
||||
'aws.rds.db_instance.status',
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<CloudToolbarItems {...props} />
|
||||
<MetricsAndGroupByToolbarItems
|
||||
{...props}
|
||||
metricTypes={metricTypes}
|
||||
groupByFields={groupByFields}
|
||||
metricTypes={rdsMetricTypes}
|
||||
groupByFields={rdsGroupByFields}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -11,22 +11,24 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_
|
|||
import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items';
|
||||
import { SnapshotMetricType } from '../types';
|
||||
|
||||
export const s3MetricTypes: SnapshotMetricType[] = [
|
||||
's3BucketSize',
|
||||
's3NumberOfObjects',
|
||||
's3TotalRequests',
|
||||
's3DownloadBytes',
|
||||
's3UploadBytes',
|
||||
];
|
||||
|
||||
export const s3GroupByFields = ['cloud.region'];
|
||||
|
||||
export const AwsS3ToolbarItems = (props: ToolbarProps) => {
|
||||
const metricTypes: SnapshotMetricType[] = [
|
||||
's3BucketSize',
|
||||
's3NumberOfObjects',
|
||||
's3TotalRequests',
|
||||
's3DownloadBytes',
|
||||
's3UploadBytes',
|
||||
];
|
||||
const groupByFields = ['cloud.region'];
|
||||
return (
|
||||
<>
|
||||
<CloudToolbarItems {...props} />
|
||||
<MetricsAndGroupByToolbarItems
|
||||
{...props}
|
||||
metricTypes={metricTypes}
|
||||
groupByFields={groupByFields}
|
||||
metricTypes={s3MetricTypes}
|
||||
groupByFields={s3GroupByFields}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -11,22 +11,23 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_
|
|||
import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items';
|
||||
import { SnapshotMetricType } from '../types';
|
||||
|
||||
export const sqsMetricTypes: SnapshotMetricType[] = [
|
||||
'sqsMessagesVisible',
|
||||
'sqsMessagesDelayed',
|
||||
'sqsMessagesSent',
|
||||
'sqsMessagesEmpty',
|
||||
'sqsOldestMessage',
|
||||
];
|
||||
export const sqsGroupByFields = ['cloud.region'];
|
||||
|
||||
export const AwsSQSToolbarItems = (props: ToolbarProps) => {
|
||||
const metricTypes: SnapshotMetricType[] = [
|
||||
'sqsMessagesVisible',
|
||||
'sqsMessagesDelayed',
|
||||
'sqsMessagesSent',
|
||||
'sqsMessagesEmpty',
|
||||
'sqsOldestMessage',
|
||||
];
|
||||
const groupByFields = ['cloud.region'];
|
||||
return (
|
||||
<>
|
||||
<CloudToolbarItems {...props} />
|
||||
<MetricsAndGroupByToolbarItems
|
||||
{...props}
|
||||
metricTypes={metricTypes}
|
||||
groupByFields={groupByFields}
|
||||
metricTypes={sqsMetricTypes}
|
||||
groupByFields={sqsGroupByFields}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -10,21 +10,22 @@ import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/compo
|
|||
import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items';
|
||||
import { SnapshotMetricType } from '../types';
|
||||
|
||||
export const containerMetricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx'];
|
||||
export const containerGroupByFields = [
|
||||
'host.name',
|
||||
'cloud.availability_zone',
|
||||
'cloud.machine.type',
|
||||
'cloud.project.id',
|
||||
'cloud.provider',
|
||||
'service.type',
|
||||
];
|
||||
|
||||
export const ContainerToolbarItems = (props: ToolbarProps) => {
|
||||
const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx'];
|
||||
const groupByFields = [
|
||||
'host.name',
|
||||
'cloud.availability_zone',
|
||||
'cloud.machine.type',
|
||||
'cloud.project.id',
|
||||
'cloud.provider',
|
||||
'service.type',
|
||||
];
|
||||
return (
|
||||
<MetricsAndGroupByToolbarItems
|
||||
{...props}
|
||||
metricTypes={metricTypes}
|
||||
groupByFields={groupByFields}
|
||||
metricTypes={containerMetricTypes}
|
||||
groupByFields={containerGroupByFields}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,20 +10,27 @@ import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/compo
|
|||
import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items';
|
||||
import { SnapshotMetricType } from '../types';
|
||||
|
||||
export const hostMetricTypes: SnapshotMetricType[] = [
|
||||
'cpu',
|
||||
'memory',
|
||||
'load',
|
||||
'rx',
|
||||
'tx',
|
||||
'logRate',
|
||||
];
|
||||
export const hostGroupByFields = [
|
||||
'cloud.availability_zone',
|
||||
'cloud.machine.type',
|
||||
'cloud.project.id',
|
||||
'cloud.provider',
|
||||
'service.type',
|
||||
];
|
||||
export const HostToolbarItems = (props: ToolbarProps) => {
|
||||
const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'load', 'rx', 'tx', 'logRate'];
|
||||
const groupByFields = [
|
||||
'cloud.availability_zone',
|
||||
'cloud.machine.type',
|
||||
'cloud.project.id',
|
||||
'cloud.provider',
|
||||
'service.type',
|
||||
];
|
||||
return (
|
||||
<MetricsAndGroupByToolbarItems
|
||||
{...props}
|
||||
metricTypes={metricTypes}
|
||||
groupByFields={groupByFields}
|
||||
metricTypes={hostMetricTypes}
|
||||
groupByFields={hostGroupByFields}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,14 +10,15 @@ import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/compo
|
|||
import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items';
|
||||
import { SnapshotMetricType } from '../types';
|
||||
|
||||
export const podMetricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx'];
|
||||
export const podGroupByFields = ['kubernetes.namespace', 'kubernetes.node.name', 'service.type'];
|
||||
|
||||
export const PodToolbarItems = (props: ToolbarProps) => {
|
||||
const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx'];
|
||||
const groupByFields = ['kubernetes.namespace', 'kubernetes.node.name', 'service.type'];
|
||||
return (
|
||||
<MetricsAndGroupByToolbarItems
|
||||
{...props}
|
||||
metricTypes={metricTypes}
|
||||
groupByFields={groupByFields}
|
||||
metricTypes={podMetricTypes}
|
||||
groupByFields={podGroupByFields}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import { AlertFlyout } from './alert_flyout';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
export const AlertDropdown = () => {
|
||||
export const MetricsAlertDropdown = () => {
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [flyoutVisible, setFlyoutVisible] = useState(false);
|
||||
const kibana = useKibana();
|
||||
|
|
|
@ -34,6 +34,8 @@ import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer
|
|||
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by';
|
||||
import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';
|
||||
import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
|
||||
|
||||
import { ExpressionRow } from './expression_row';
|
||||
import { AlertContextMeta, TimeUnit, MetricExpression } from '../types';
|
||||
import { ExpressionChart } from './expression_chart';
|
||||
|
@ -45,6 +47,7 @@ interface Props {
|
|||
groupBy?: string;
|
||||
filterQuery?: string;
|
||||
sourceId?: string;
|
||||
filterQueryText?: string;
|
||||
alertOnNoData?: boolean;
|
||||
};
|
||||
alertsContext: AlertsContextValue<AlertContextMeta>;
|
||||
|
@ -111,11 +114,15 @@ export const Expressions: React.FC<Props> = props => {
|
|||
[setAlertParams, alertParams.criteria]
|
||||
);
|
||||
|
||||
const onFilterQuerySubmit = useCallback(
|
||||
const onFilterChange = useCallback(
|
||||
(filter: any) => {
|
||||
setAlertParams('filterQuery', filter);
|
||||
setAlertParams('filterQueryText', filter);
|
||||
setAlertParams(
|
||||
'filterQuery',
|
||||
convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || ''
|
||||
);
|
||||
},
|
||||
[setAlertParams]
|
||||
[setAlertParams, derivedIndexPattern]
|
||||
);
|
||||
|
||||
const onGroupByChange = useCallback(
|
||||
|
@ -180,10 +187,19 @@ export const Expressions: React.FC<Props> = props => {
|
|||
|
||||
if (md.currentOptions) {
|
||||
if (md.currentOptions.filterQuery) {
|
||||
setAlertParams('filterQuery', md.currentOptions.filterQuery);
|
||||
setAlertParams('filterQueryText', md.currentOptions.filterQuery);
|
||||
setAlertParams(
|
||||
'filterQuery',
|
||||
convertKueryToElasticSearchQuery(md.currentOptions.filterQuery, derivedIndexPattern) ||
|
||||
''
|
||||
);
|
||||
} else if (md.currentOptions.groupBy && md.series) {
|
||||
const filter = `${md.currentOptions.groupBy}: "${md.series.id}"`;
|
||||
setAlertParams('filterQuery', filter);
|
||||
setAlertParams('filterQueryText', filter);
|
||||
setAlertParams(
|
||||
'filterQuery',
|
||||
convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || ''
|
||||
);
|
||||
}
|
||||
|
||||
setAlertParams('groupBy', md.currentOptions.groupBy);
|
||||
|
@ -200,8 +216,8 @@ export const Expressions: React.FC<Props> = props => {
|
|||
}, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleFieldSearchChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => onFilterQuerySubmit(e.target.value),
|
||||
[onFilterQuerySubmit]
|
||||
(e: ChangeEvent<HTMLInputElement>) => onFilterChange(e.target.value),
|
||||
[onFilterChange]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -304,13 +320,14 @@ export const Expressions: React.FC<Props> = props => {
|
|||
{(alertsContext.metadata && (
|
||||
<MetricsExplorerKueryBar
|
||||
derivedIndexPattern={derivedIndexPattern}
|
||||
onSubmit={onFilterQuerySubmit}
|
||||
value={alertParams.filterQuery}
|
||||
onChange={onFilterChange}
|
||||
onSubmit={onFilterChange}
|
||||
value={alertParams.filterQueryText}
|
||||
/>
|
||||
)) || (
|
||||
<EuiFieldSearch
|
||||
onChange={handleFieldSearchChange}
|
||||
value={alertParams.filterQuery}
|
||||
value={alertParams.filterQueryText}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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, { useState, useCallback, useMemo } from 'react';
|
||||
import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { AlertFlyout } from './alert_flyout';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
export const InventoryAlertDropdown = () => {
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [flyoutVisible, setFlyoutVisible] = useState(false);
|
||||
const kibana = useKibana();
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setPopoverOpen(false);
|
||||
}, [setPopoverOpen]);
|
||||
|
||||
const openPopover = useCallback(() => {
|
||||
setPopoverOpen(true);
|
||||
}, [setPopoverOpen]);
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
return [
|
||||
<EuiContextMenuItem icon="bell" key="createLink" onClick={() => setFlyoutVisible(true)}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.alerting.createAlertButton"
|
||||
defaultMessage="Create alert"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
icon="tableOfContents"
|
||||
key="manageLink"
|
||||
href={kibana.services?.application?.getUrlForApp(
|
||||
'kibana#/management/kibana/triggersActions/alerts'
|
||||
)}
|
||||
>
|
||||
<FormattedMessage id="xpack.infra.alerting.manageAlerts" defaultMessage="Manage alerts" />
|
||||
</EuiContextMenuItem>,
|
||||
];
|
||||
}, [kibana.services]);
|
||||
|
||||
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} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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, { useContext } from 'react';
|
||||
import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public';
|
||||
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
|
||||
import { InfraWaffleMapOptions } from '../../../lib/lib';
|
||||
import { InventoryItemType } from '../../../../common/inventory_models/types';
|
||||
|
||||
interface Props {
|
||||
visible?: boolean;
|
||||
options?: Partial<InfraWaffleMapOptions>;
|
||||
nodeType?: InventoryItemType;
|
||||
filter?: string;
|
||||
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const AlertFlyout = (props: Props) => {
|
||||
const { triggersActionsUI } = useContext(TriggerActionsContext);
|
||||
const { services } = useKibana();
|
||||
|
||||
return (
|
||||
<>
|
||||
{triggersActionsUI && (
|
||||
<AlertsContextProvider
|
||||
value={{
|
||||
metadata: { options: props.options, nodeType: props.nodeType, filter: props.filter },
|
||||
toastNotifications: services.notifications?.toasts,
|
||||
http: services.http,
|
||||
docLinks: services.docLinks,
|
||||
actionTypeRegistry: triggersActionsUI.actionTypeRegistry,
|
||||
alertTypeRegistry: triggersActionsUI.alertTypeRegistry,
|
||||
}}
|
||||
>
|
||||
<AlertAdd
|
||||
addFlyoutVisible={props.visible!}
|
||||
setAddFlyoutVisibility={props.setVisible}
|
||||
alertTypeId={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID}
|
||||
canChangeTrigger={false}
|
||||
consumer={'metrics'}
|
||||
/>
|
||||
</AlertsContextProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,498 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonIcon,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiFormRow,
|
||||
EuiButtonEmpty,
|
||||
EuiFieldSearch,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
Comparator,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../server/lib/alerting/metric_threshold/types';
|
||||
import { euiStyled } from '../../../../../observability/public';
|
||||
import {
|
||||
ThresholdExpression,
|
||||
ForLastExpression,
|
||||
// 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';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar';
|
||||
import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';
|
||||
import { toMetricOpt } from '../../../pages/metrics/inventory_view/components/toolbars/toolbar_wrapper';
|
||||
import { sqsMetricTypes } from '../../../../common/inventory_models/aws_sqs/toolbar_items';
|
||||
import { ec2MetricTypes } from '../../../../common/inventory_models/aws_ec2/toolbar_items';
|
||||
import { s3MetricTypes } from '../../../../common/inventory_models/aws_s3/toolbar_items';
|
||||
import { rdsMetricTypes } from '../../../../common/inventory_models/aws_rds/toolbar_items';
|
||||
import { hostMetricTypes } from '../../../../common/inventory_models/host/toolbar_items';
|
||||
import { containerMetricTypes } from '../../../../common/inventory_models/container/toolbar_items';
|
||||
import { podMetricTypes } from '../../../../common/inventory_models/pod/toolbar_items';
|
||||
import { findInventoryModel } from '../../../../common/inventory_models';
|
||||
import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
|
||||
import { MetricExpression } from './metric';
|
||||
import { NodeTypeExpression } from './node_type';
|
||||
import { InfraWaffleMapOptions } from '../../../lib/lib';
|
||||
import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
|
||||
|
||||
interface AlertContextMeta {
|
||||
options?: Partial<InfraWaffleMapOptions>;
|
||||
nodeType?: InventoryItemType;
|
||||
filter?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
errors: IErrorObject[];
|
||||
alertParams: {
|
||||
criteria: InventoryMetricConditions[];
|
||||
nodeType: InventoryItemType;
|
||||
groupBy?: string;
|
||||
filterQuery?: string;
|
||||
filterQueryText?: string;
|
||||
sourceId?: string;
|
||||
};
|
||||
alertsContext: AlertsContextValue<AlertContextMeta>;
|
||||
setAlertParams(key: string, value: any): void;
|
||||
setAlertProperty(key: string, value: any): void;
|
||||
}
|
||||
|
||||
type TimeUnit = 's' | 'm' | 'h' | 'd';
|
||||
|
||||
const defaultExpression = {
|
||||
metric: 'cpu' as SnapshotMetricType,
|
||||
comparator: Comparator.GT,
|
||||
threshold: [],
|
||||
timeSize: 1,
|
||||
timeUnit: 'm',
|
||||
} as InventoryMetricConditions;
|
||||
|
||||
export const Expressions: React.FC<Props> = props => {
|
||||
const { setAlertParams, alertParams, errors, alertsContext } = props;
|
||||
const { source, createDerivedIndexPattern } = useSourceViaHttp({
|
||||
sourceId: 'default',
|
||||
type: 'metrics',
|
||||
fetch: alertsContext.http.fetch,
|
||||
toastWarning: alertsContext.toastNotifications.addWarning,
|
||||
});
|
||||
const [timeSize, setTimeSize] = useState<number | undefined>(1);
|
||||
const [timeUnit, setTimeUnit] = useState<TimeUnit>('m');
|
||||
|
||||
const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
|
||||
createDerivedIndexPattern,
|
||||
]);
|
||||
|
||||
const updateParams = useCallback(
|
||||
(id, e: InventoryMetricConditions) => {
|
||||
const exp = alertParams.criteria ? alertParams.criteria.slice() : [];
|
||||
exp[id] = { ...exp[id], ...e };
|
||||
setAlertParams('criteria', exp);
|
||||
},
|
||||
[setAlertParams, alertParams.criteria]
|
||||
);
|
||||
|
||||
const addExpression = useCallback(() => {
|
||||
const exp = alertParams.criteria.slice();
|
||||
exp.push(defaultExpression);
|
||||
setAlertParams('criteria', exp);
|
||||
}, [setAlertParams, alertParams.criteria]);
|
||||
|
||||
const removeExpression = useCallback(
|
||||
(id: number) => {
|
||||
const exp = alertParams.criteria.slice();
|
||||
if (exp.length > 1) {
|
||||
exp.splice(id, 1);
|
||||
setAlertParams('criteria', exp);
|
||||
}
|
||||
},
|
||||
[setAlertParams, alertParams.criteria]
|
||||
);
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
(filter: any) => {
|
||||
setAlertParams('filterQueryText', filter || '');
|
||||
setAlertParams(
|
||||
'filterQuery',
|
||||
convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || ''
|
||||
);
|
||||
},
|
||||
[derivedIndexPattern, setAlertParams]
|
||||
);
|
||||
|
||||
const emptyError = useMemo(() => {
|
||||
return {
|
||||
aggField: [],
|
||||
timeSizeUnit: [],
|
||||
timeWindowSize: [],
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateTimeSize = useCallback(
|
||||
(ts: number | undefined) => {
|
||||
const criteria = alertParams.criteria.map(c => ({
|
||||
...c,
|
||||
timeSize: ts,
|
||||
}));
|
||||
setTimeSize(ts || undefined);
|
||||
setAlertParams('criteria', criteria);
|
||||
},
|
||||
[alertParams.criteria, setAlertParams]
|
||||
);
|
||||
|
||||
const updateTimeUnit = useCallback(
|
||||
(tu: string) => {
|
||||
const criteria = alertParams.criteria.map(c => ({
|
||||
...c,
|
||||
timeUnit: tu,
|
||||
}));
|
||||
setTimeUnit(tu as TimeUnit);
|
||||
setAlertParams('criteria', criteria);
|
||||
},
|
||||
[alertParams.criteria, setAlertParams]
|
||||
);
|
||||
|
||||
const updateNodeType = useCallback(
|
||||
(nt: any) => {
|
||||
setAlertParams('nodeType', nt);
|
||||
},
|
||||
[setAlertParams]
|
||||
);
|
||||
|
||||
const handleFieldSearchChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => onFilterChange(e.target.value),
|
||||
[onFilterChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const md = alertsContext.metadata;
|
||||
if (!alertParams.nodeType) {
|
||||
if (md && md.nodeType) {
|
||||
setAlertParams('nodeType', md.nodeType);
|
||||
} else {
|
||||
setAlertParams('nodeType', 'host');
|
||||
}
|
||||
}
|
||||
|
||||
if (!alertParams.criteria) {
|
||||
if (md && md.options) {
|
||||
setAlertParams('criteria', [
|
||||
{
|
||||
...defaultExpression,
|
||||
metric: md.options.metric!.type,
|
||||
} as InventoryMetricConditions,
|
||||
]);
|
||||
} else {
|
||||
setAlertParams('criteria', [defaultExpression]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!alertParams.filterQuery) {
|
||||
if (md && md.filter) {
|
||||
setAlertParams('filterQueryText', md.filter);
|
||||
setAlertParams(
|
||||
'filterQuery',
|
||||
convertKueryToElasticSearchQuery(md.filter, derivedIndexPattern) || ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!alertParams.sourceId) {
|
||||
setAlertParams('sourceId', source?.id);
|
||||
}
|
||||
}, [alertsContext.metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiText size="xs">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metrics.alertFlyout.conditions"
|
||||
defaultMessage="Conditions"
|
||||
/>
|
||||
</h4>
|
||||
</EuiText>
|
||||
<StyledExpression>
|
||||
<NodeTypeExpression
|
||||
options={nodeTypes}
|
||||
value={alertParams.nodeType || 'host'}
|
||||
onChange={updateNodeType}
|
||||
/>
|
||||
</StyledExpression>
|
||||
<EuiSpacer size={'xs'} />
|
||||
{alertParams.criteria &&
|
||||
alertParams.criteria.map((e, idx) => {
|
||||
return (
|
||||
<ExpressionRow
|
||||
nodeType={alertParams.nodeType}
|
||||
canDelete={alertParams.criteria.length > 1}
|
||||
remove={removeExpression}
|
||||
addExpression={addExpression}
|
||||
key={idx} // idx's don't usually make good key's but here the index has semantic meaning
|
||||
expressionId={idx}
|
||||
setAlertParams={updateParams}
|
||||
errors={errors[idx] || emptyError}
|
||||
expression={e || {}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<ForLastExpression
|
||||
timeWindowSize={timeSize}
|
||||
timeWindowUnit={timeUnit}
|
||||
errors={emptyError}
|
||||
onChangeWindowSize={updateTimeSize}
|
||||
onChangeWindowUnit={updateTimeUnit}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<EuiButtonEmpty
|
||||
color={'primary'}
|
||||
iconSide={'left'}
|
||||
flush={'left'}
|
||||
iconType={'plusInCircleFilled'}
|
||||
onClick={addExpression}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metrics.alertFlyout.addCondition"
|
||||
defaultMessage="Add condition"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
|
||||
<EuiSpacer size={'m'} />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.infra.metrics.alertFlyout.filterLabel', {
|
||||
defaultMessage: 'Filter (optional)',
|
||||
})}
|
||||
helpText={i18n.translate('xpack.infra.metrics.alertFlyout.filterHelpText', {
|
||||
defaultMessage: 'Use a KQL expression to limit the scope of your alert trigger.',
|
||||
})}
|
||||
fullWidth
|
||||
compressed
|
||||
>
|
||||
{(alertsContext.metadata && (
|
||||
<MetricsExplorerKueryBar
|
||||
derivedIndexPattern={derivedIndexPattern}
|
||||
onSubmit={onFilterChange}
|
||||
onChange={onFilterChange}
|
||||
value={alertParams.filterQueryText}
|
||||
/>
|
||||
)) || (
|
||||
<EuiFieldSearch
|
||||
onChange={handleFieldSearchChange}
|
||||
value={alertParams.filterQueryText}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size={'m'} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ExpressionRowProps {
|
||||
nodeType: InventoryItemType;
|
||||
expressionId: number;
|
||||
expression: Omit<InventoryMetricConditions, 'metric'> & {
|
||||
metric?: SnapshotMetricType;
|
||||
};
|
||||
errors: IErrorObject;
|
||||
canDelete: boolean;
|
||||
addExpression(): void;
|
||||
remove(id: number): void;
|
||||
setAlertParams(id: number, params: Partial<InventoryMetricConditions>): void;
|
||||
}
|
||||
|
||||
const StyledExpressionRow = euiStyled(EuiFlexGroup)`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -4px;
|
||||
`;
|
||||
|
||||
const StyledExpression = euiStyled.div`
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
export const ExpressionRow: React.FC<ExpressionRowProps> = props => {
|
||||
const { setAlertParams, expression, errors, expressionId, remove, canDelete } = props;
|
||||
const { metric, comparator = Comparator.GT, threshold = [] } = expression;
|
||||
|
||||
const updateMetric = useCallback(
|
||||
(m?: SnapshotMetricType) => {
|
||||
setAlertParams(expressionId, { ...expression, metric: m });
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
const updateComparator = useCallback(
|
||||
(c?: string) => {
|
||||
setAlertParams(expressionId, { ...expression, comparator: c as Comparator | undefined });
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
const updateThreshold = useCallback(
|
||||
t => {
|
||||
if (t.join() !== expression.threshold.join()) {
|
||||
setAlertParams(expressionId, { ...expression, threshold: t });
|
||||
}
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
const ofFields = useMemo(() => {
|
||||
let myMetrics = hostMetricTypes;
|
||||
|
||||
switch (props.nodeType) {
|
||||
case 'awsEC2':
|
||||
myMetrics = ec2MetricTypes;
|
||||
break;
|
||||
case 'awsRDS':
|
||||
myMetrics = rdsMetricTypes;
|
||||
break;
|
||||
case 'awsS3':
|
||||
myMetrics = s3MetricTypes;
|
||||
break;
|
||||
case 'awsSQS':
|
||||
myMetrics = sqsMetricTypes;
|
||||
break;
|
||||
case 'host':
|
||||
myMetrics = hostMetricTypes;
|
||||
break;
|
||||
case 'pod':
|
||||
myMetrics = podMetricTypes;
|
||||
break;
|
||||
case 'container':
|
||||
myMetrics = containerMetricTypes;
|
||||
break;
|
||||
}
|
||||
return myMetrics.map(toMetricOpt);
|
||||
}, [props.nodeType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow>
|
||||
<StyledExpressionRow>
|
||||
<StyledExpression>
|
||||
<MetricExpression
|
||||
metric={{
|
||||
value: metric!,
|
||||
text: ofFields.find(v => v?.value === metric)?.text || '',
|
||||
}}
|
||||
metrics={
|
||||
ofFields.filter(m => m !== undefined && m.value !== undefined) as Array<{
|
||||
value: SnapshotMetricType;
|
||||
text: string;
|
||||
}>
|
||||
}
|
||||
onChange={updateMetric}
|
||||
errors={errors}
|
||||
/>
|
||||
</StyledExpression>
|
||||
<StyledExpression>
|
||||
<ThresholdExpression
|
||||
thresholdComparator={comparator || Comparator.GT}
|
||||
threshold={threshold}
|
||||
onChangeSelectedThresholdComparator={updateComparator}
|
||||
onChangeSelectedThreshold={updateThreshold}
|
||||
errors={errors}
|
||||
/>
|
||||
</StyledExpression>
|
||||
{metric && (
|
||||
<StyledExpression>
|
||||
<div style={{ display: 'flex', alignItems: 'center', height: '100%' }}>
|
||||
<div>{metricUnit[metric]?.label || ''}</div>
|
||||
</div>
|
||||
</StyledExpression>
|
||||
)}
|
||||
</StyledExpressionRow>
|
||||
</EuiFlexItem>
|
||||
{canDelete && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.infra.metrics.alertFlyout.removeCondition', {
|
||||
defaultMessage: 'Remove condition',
|
||||
})}
|
||||
color={'danger'}
|
||||
iconType={'trash'}
|
||||
onClick={() => remove(expressionId)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size={'s'} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getDisplayNameForType = (type: InventoryItemType) => {
|
||||
const inventoryModel = findInventoryModel(type);
|
||||
return inventoryModel.displayName;
|
||||
};
|
||||
|
||||
export const nodeTypes: { [key: string]: any } = {
|
||||
host: {
|
||||
text: getDisplayNameForType('host'),
|
||||
value: 'host',
|
||||
},
|
||||
pod: {
|
||||
text: getDisplayNameForType('pod'),
|
||||
value: 'pod',
|
||||
},
|
||||
container: {
|
||||
text: getDisplayNameForType('container'),
|
||||
value: 'container',
|
||||
},
|
||||
awsEC2: {
|
||||
text: getDisplayNameForType('awsEC2'),
|
||||
value: 'awsEC2',
|
||||
},
|
||||
awsS3: {
|
||||
text: getDisplayNameForType('awsS3'),
|
||||
value: 'awsS3',
|
||||
},
|
||||
awsRDS: {
|
||||
text: getDisplayNameForType('awsRDS'),
|
||||
value: 'awsRDS',
|
||||
},
|
||||
awsSQS: {
|
||||
text: getDisplayNameForType('awsSQS'),
|
||||
value: 'awsSQS',
|
||||
},
|
||||
};
|
||||
|
||||
const metricUnit: Record<string, { label: string }> = {
|
||||
count: { label: '' },
|
||||
cpu: { label: '%' },
|
||||
memory: { label: '%' },
|
||||
rx: { label: 'bits/s' },
|
||||
tx: { label: 'bits/s' },
|
||||
logRate: { label: '/s' },
|
||||
diskIOReadBytes: { label: 'bytes/s' },
|
||||
diskIOWriteBytes: { label: 'bytes/s' },
|
||||
s3BucketSize: { label: 'bytes' },
|
||||
s3TotalRequests: { label: '' },
|
||||
s3NumberOfObjects: { label: '' },
|
||||
s3UploadBytes: { label: 'bytes' },
|
||||
s3DownloadBytes: { label: 'bytes' },
|
||||
sqsOldestMessage: { label: 'seconds' },
|
||||
};
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiExpression,
|
||||
EuiPopover,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiComboBox,
|
||||
} from '@elastic/eui';
|
||||
import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
|
||||
import { SnapshotMetricType } from '../../../../common/inventory_models/types';
|
||||
|
||||
interface Props {
|
||||
metric?: { value: SnapshotMetricType; text: string };
|
||||
metrics: Array<{ value: string; text: string }>;
|
||||
errors: IErrorObject;
|
||||
onChange: (metric: SnapshotMetricType) => void;
|
||||
popupPosition?:
|
||||
| 'upCenter'
|
||||
| 'upLeft'
|
||||
| 'upRight'
|
||||
| 'downCenter'
|
||||
| 'downLeft'
|
||||
| 'downRight'
|
||||
| 'leftCenter'
|
||||
| 'leftUp'
|
||||
| 'leftDown'
|
||||
| 'rightCenter'
|
||||
| 'rightUp'
|
||||
| 'rightDown';
|
||||
}
|
||||
|
||||
export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosition }: Props) => {
|
||||
const [aggFieldPopoverOpen, setAggFieldPopoverOpen] = useState(false);
|
||||
const firstFieldOption = {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.metric.selectFieldLabel', {
|
||||
defaultMessage: 'Select a metric',
|
||||
}),
|
||||
value: '',
|
||||
};
|
||||
|
||||
const availablefieldsOptions = metrics.map(m => {
|
||||
return { label: m.text, value: m.value };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="aggFieldPopover"
|
||||
button={
|
||||
<EuiExpression
|
||||
description={i18n.translate(
|
||||
'xpack.infra.metrics.alertFlyout.expression.metric.whenLabel',
|
||||
{
|
||||
defaultMessage: 'When',
|
||||
}
|
||||
)}
|
||||
value={metric?.text || firstFieldOption.text}
|
||||
isActive={aggFieldPopoverOpen || !metric}
|
||||
onClick={() => {
|
||||
setAggFieldPopoverOpen(true);
|
||||
}}
|
||||
color={metric ? 'secondary' : 'danger'}
|
||||
/>
|
||||
}
|
||||
isOpen={aggFieldPopoverOpen}
|
||||
closePopover={() => {
|
||||
setAggFieldPopoverOpen(false);
|
||||
}}
|
||||
withTitle
|
||||
anchorPosition={popupPosition ?? 'downRight'}
|
||||
zIndex={8000}
|
||||
>
|
||||
<div>
|
||||
<ClosablePopoverTitle onClose={() => setAggFieldPopoverOpen(false)}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metrics.alertFlyout.expression.metric.popoverTitle"
|
||||
defaultMessage="Metric"
|
||||
/>
|
||||
</ClosablePopoverTitle>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false} className="actOf__aggFieldContainer">
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={errors.metric.length > 0 && metric !== undefined}
|
||||
error={errors.metric}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
singleSelection={{ asPlainText: true }}
|
||||
data-test-subj="availablefieldsOptionsComboBox"
|
||||
isInvalid={errors.metric.length > 0 && metric !== undefined}
|
||||
placeholder={firstFieldOption.text}
|
||||
options={availablefieldsOptions}
|
||||
noSuggestions={!availablefieldsOptions.length}
|
||||
selectedOptions={
|
||||
metric ? availablefieldsOptions.filter(a => a.value === metric.value) : []
|
||||
}
|
||||
renderOption={(o: any) => o.label}
|
||||
onChange={selectedOptions => {
|
||||
if (selectedOptions.length > 0) {
|
||||
onChange(selectedOptions[0].value as SnapshotMetricType);
|
||||
setAggFieldPopoverOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
interface ClosablePopoverTitleProps {
|
||||
children: JSX.Element;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => {
|
||||
return (
|
||||
<EuiPopoverTitle>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="cross"
|
||||
color="danger"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.metrics.expressionItems.components.closablePopoverTitle.closeLabel',
|
||||
{
|
||||
defaultMessage: 'Close',
|
||||
}
|
||||
)}
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopoverTitle>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types';
|
||||
import { Expressions } from './expression';
|
||||
import { validateMetricThreshold } from './validation';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
|
||||
|
||||
export function getInventoryMetricAlertType(): AlertTypeModel {
|
||||
return {
|
||||
id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
|
||||
name: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertName', {
|
||||
defaultMessage: 'Inventory',
|
||||
}),
|
||||
iconClass: 'bell',
|
||||
alertParamsExpression: Expressions,
|
||||
validate: validateMetricThreshold,
|
||||
defaultActionMessage: i18n.translate(
|
||||
'xpack.infra.metrics.alerting.inventory.threshold.defaultActionMessage',
|
||||
{
|
||||
defaultMessage: `\\{\\{alertName\\}\\} - \\{\\{context.group\\}\\}
|
||||
|
||||
\\{\\{context.metricOf.condition0\\}\\} has crossed a threshold of \\{\\{context.thresholdOf.condition0\\}\\}
|
||||
Current value is \\{\\{context.valueOf.condition0\\}\\}
|
||||
`,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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, { 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 { InventoryItemType } from '../../../../common/inventory_models/types';
|
||||
|
||||
interface WhenExpressionProps {
|
||||
value: InventoryItemType;
|
||||
options: { [key: string]: { text: string; value: InventoryItemType } };
|
||||
onChange: (value: InventoryItemType) => 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
|
||||
withTitle
|
||||
anchorPosition={popupPosition ?? 'downLeft'}
|
||||
>
|
||||
<div>
|
||||
<ClosablePopoverTitle onClose={() => setAggTypePopoverOpen(false)}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metrics.alertFlyout.expression.for.popoverTitle"
|
||||
defaultMessage="Inventory Type"
|
||||
/>
|
||||
</ClosablePopoverTitle>
|
||||
<EuiSelect
|
||||
data-test-subj="forExpressionSelect"
|
||||
value={value}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
onChange(e.target.value as InventoryItemType);
|
||||
setAggTypePopoverOpen(false);
|
||||
}}
|
||||
options={Object.values(options).map(o => o)}
|
||||
/>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
interface ClosablePopoverTitleProps {
|
||||
children: JSX.Element;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => {
|
||||
return (
|
||||
<EuiPopoverTitle>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="cross"
|
||||
color="danger"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.metrics.expressionItems.components.closablePopoverTitle.closeLabel',
|
||||
{
|
||||
defaultMessage: 'Close',
|
||||
}
|
||||
)}
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopoverTitle>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { MetricExpressionParams } from '../../../../server/lib/alerting/metric_threshold/types';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ValidationResult } from '../../../../../triggers_actions_ui/public/types';
|
||||
|
||||
export function validateMetricThreshold({
|
||||
criteria,
|
||||
}: {
|
||||
criteria: MetricExpressionParams[];
|
||||
}): ValidationResult {
|
||||
const validationResult = { errors: {} };
|
||||
const errors: {
|
||||
[id: string]: {
|
||||
timeSizeUnit: string[];
|
||||
timeWindowSize: string[];
|
||||
threshold0: string[];
|
||||
threshold1: string[];
|
||||
metric: string[];
|
||||
};
|
||||
} = {};
|
||||
validationResult.errors = errors;
|
||||
|
||||
if (!criteria || !criteria.length) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
criteria.forEach((c, idx) => {
|
||||
// Create an id for each criteria, so we can map errors to specific criteria.
|
||||
const id = idx.toString();
|
||||
|
||||
errors[id] = errors[id] || {
|
||||
timeSizeUnit: [],
|
||||
timeWindowSize: [],
|
||||
threshold0: [],
|
||||
threshold1: [],
|
||||
metric: [],
|
||||
};
|
||||
|
||||
if (!c.threshold || !c.threshold.length) {
|
||||
errors[id].threshold0.push(
|
||||
i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
|
||||
defaultMessage: 'Threshold is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (c.comparator === 'between' && (!c.threshold || c.threshold.length < 2)) {
|
||||
errors[id].threshold1.push(
|
||||
i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
|
||||
defaultMessage: 'Threshold is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!c.timeSize) {
|
||||
errors[id].timeWindowSize.push(
|
||||
i18n.translate('xpack.infra.metrics.alertFlyout.error.timeRequred', {
|
||||
defaultMessage: 'Time size is Required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!c.metric && c.aggType !== 'count') {
|
||||
errors[id].metric.push(
|
||||
i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', {
|
||||
defaultMessage: 'Metric is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return validationResult;
|
||||
}
|
|
@ -22,7 +22,7 @@ import {
|
|||
} from './log_entry_column';
|
||||
import { ASSUMED_SCROLLBAR_WIDTH } from './vertical_scroll_panel';
|
||||
import { LogPositionState } from '../../../containers/logs/log_position';
|
||||
import { localizedDate } from '../../../utils/formatters/datetime';
|
||||
import { localizedDate } from '../../../../common/formatters/datetime';
|
||||
|
||||
export const LogColumnHeaders: React.FunctionComponent<{
|
||||
columnConfigurations: LogColumnConfiguration[];
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiTitle } from '@elastic/eui';
|
||||
import { localizedDate } from '../../../utils/formatters/datetime';
|
||||
import { localizedDate } from '../../../../common/formatters/datetime';
|
||||
|
||||
interface LogDateRowProps {
|
||||
timestamp: number;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useEffect, useMemo, useCallback } from 'react';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
@ -69,12 +69,15 @@ export const useSourceViaHttp = ({
|
|||
})();
|
||||
}, [makeRequest]);
|
||||
|
||||
const createDerivedIndexPattern = (indexType: 'logs' | 'metrics' | 'both' = type) => {
|
||||
return {
|
||||
fields: response?.source ? response.status.indexFields : [],
|
||||
title: pickIndexPattern(response?.source, indexType),
|
||||
};
|
||||
};
|
||||
const createDerivedIndexPattern = useCallback(
|
||||
(indexType: 'logs' | 'metrics' | 'both' = type) => {
|
||||
return {
|
||||
fields: response?.source ? response.status.indexFields : [],
|
||||
title: pickIndexPattern(response?.source, indexType),
|
||||
};
|
||||
},
|
||||
[response, type]
|
||||
);
|
||||
|
||||
const source = useMemo(() => {
|
||||
return response ? { ...response.source, status: response.status } : null;
|
||||
|
|
|
@ -16,7 +16,7 @@ export const plugin: PluginInitializer<
|
|||
return new Plugin(context);
|
||||
};
|
||||
|
||||
export { FORMATTERS } from './utils/formatters';
|
||||
export { FORMATTERS } from '../common/formatters';
|
||||
export { InfraFormatterType } from './lib/lib';
|
||||
|
||||
export type InfraAppId = 'logs' | 'metrics';
|
||||
|
|
|
@ -186,12 +186,6 @@ export enum InfraFormatterType {
|
|||
percent = 'percent',
|
||||
}
|
||||
|
||||
export enum InfraWaffleMapDataFormat {
|
||||
bytesDecimal = 'bytesDecimal',
|
||||
bitsDecimal = 'bitsDecimal',
|
||||
abbreviatedNumber = 'abbreviatedNumber',
|
||||
}
|
||||
|
||||
export interface InfraGroupByOptions {
|
||||
text: string;
|
||||
field: string;
|
||||
|
|
|
@ -28,7 +28,9 @@ import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
|||
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 { AlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown';
|
||||
|
||||
import { InventoryAlertDropdown } from '../../components/alerting/inventory/alert_dropdown';
|
||||
import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown';
|
||||
|
||||
export const InfrastructurePage = ({ match }: RouteComponentProps) => {
|
||||
const uiCapabilities = useKibana().services.application?.capabilities;
|
||||
|
@ -96,7 +98,8 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Route path={'/explorer'} component={AlertDropdown} />
|
||||
<Route path={'/explorer'} component={MetricsAlertDropdown} />
|
||||
<Route path={'/inventory'} component={InventoryAlertDropdown} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</AppNavigation>
|
||||
|
|
|
@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { AlertFlyout } from '../../../../../alerting/metric_threshold/components/alert_flyout';
|
||||
import { AlertFlyout } from '../../../../../components/alerting/inventory/alert_flyout';
|
||||
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib';
|
||||
import { getNodeDetailUrl, getNodeLogsUrl } from '../../../../link_to';
|
||||
import { createUptimeLink } from '../../lib/create_uptime_link';
|
||||
|
@ -24,6 +24,8 @@ import {
|
|||
SectionSubtitle,
|
||||
SectionLinks,
|
||||
SectionLink,
|
||||
withTheme,
|
||||
EuiTheme,
|
||||
} from '../../../../../../../observability/public';
|
||||
import { useLinkProps } from '../../../../../hooks/use_link_props';
|
||||
|
||||
|
@ -37,157 +39,178 @@ interface Props {
|
|||
popoverPosition: EuiPopoverProps['anchorPosition'];
|
||||
}
|
||||
|
||||
export const NodeContextMenu: React.FC<Props> = ({
|
||||
options,
|
||||
currentTime,
|
||||
children,
|
||||
node,
|
||||
isPopoverOpen,
|
||||
closePopover,
|
||||
nodeType,
|
||||
popoverPosition,
|
||||
}) => {
|
||||
const [flyoutVisible, setFlyoutVisible] = useState(false);
|
||||
const inventoryModel = findInventoryModel(nodeType);
|
||||
const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
|
||||
const uiCapabilities = useKibana().services.application?.capabilities;
|
||||
// Due to the changing nature of the fields between APM and this UI,
|
||||
// We need to have some exceptions until 7.0 & ECS is finalized. Reference
|
||||
// #26620 for the details for these fields.
|
||||
// TODO: This is tech debt, remove it after 7.0 & ECS migration.
|
||||
const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id;
|
||||
export const NodeContextMenu: React.FC<Props & { theme?: EuiTheme }> = withTheme(
|
||||
({
|
||||
options,
|
||||
currentTime,
|
||||
children,
|
||||
node,
|
||||
isPopoverOpen,
|
||||
closePopover,
|
||||
nodeType,
|
||||
popoverPosition,
|
||||
theme,
|
||||
}) => {
|
||||
const [flyoutVisible, setFlyoutVisible] = useState(false);
|
||||
const inventoryModel = findInventoryModel(nodeType);
|
||||
const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
|
||||
const uiCapabilities = useKibana().services.application?.capabilities;
|
||||
// Due to the changing nature of the fields between APM and this UI,
|
||||
// We need to have some exceptions until 7.0 & ECS is finalized. Reference
|
||||
// #26620 for the details for these fields.
|
||||
// TODO: This is tech debt, remove it after 7.0 & ECS migration.
|
||||
const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id;
|
||||
|
||||
const showDetail = inventoryModel.crosslinkSupport.details;
|
||||
const showLogsLink =
|
||||
inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show;
|
||||
const showAPMTraceLink =
|
||||
inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show;
|
||||
const showUptimeLink =
|
||||
inventoryModel.crosslinkSupport.uptime && (['pod', 'container'].includes(nodeType) || node.ip);
|
||||
const showDetail = inventoryModel.crosslinkSupport.details;
|
||||
const showLogsLink =
|
||||
inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show;
|
||||
const showAPMTraceLink =
|
||||
inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show;
|
||||
const showUptimeLink =
|
||||
inventoryModel.crosslinkSupport.uptime &&
|
||||
(['pod', 'container'].includes(nodeType) || node.ip);
|
||||
|
||||
const inventoryId = useMemo(() => {
|
||||
if (nodeType === 'host') {
|
||||
if (node.ip) {
|
||||
return { label: <EuiCode>host.ip</EuiCode>, value: node.ip };
|
||||
const inventoryId = useMemo(() => {
|
||||
if (nodeType === 'host') {
|
||||
if (node.ip) {
|
||||
return { label: <EuiCode>host.ip</EuiCode>, value: node.ip };
|
||||
}
|
||||
} else {
|
||||
if (options.fields) {
|
||||
const { id } = findInventoryFields(nodeType, options.fields);
|
||||
return {
|
||||
label: <EuiCode>{id}</EuiCode>,
|
||||
value: node.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (options.fields) {
|
||||
const { id } = findInventoryFields(nodeType, options.fields);
|
||||
return {
|
||||
label: <EuiCode>{id}</EuiCode>,
|
||||
value: node.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { label: '', value: '' };
|
||||
}, [nodeType, node.ip, node.id, options.fields]);
|
||||
return { label: '', value: '' };
|
||||
}, [nodeType, node.ip, node.id, options.fields]);
|
||||
|
||||
const nodeLogsMenuItemLinkProps = useLinkProps({
|
||||
app: 'logs',
|
||||
...getNodeLogsUrl({
|
||||
nodeType,
|
||||
nodeId: node.id,
|
||||
time: currentTime,
|
||||
}),
|
||||
});
|
||||
const nodeDetailMenuItemLinkProps = useLinkProps({
|
||||
...getNodeDetailUrl({
|
||||
nodeType,
|
||||
nodeId: node.id,
|
||||
from: nodeDetailFrom,
|
||||
to: currentTime,
|
||||
}),
|
||||
});
|
||||
const apmTracesMenuItemLinkProps = useLinkProps({
|
||||
app: 'apm',
|
||||
hash: 'traces',
|
||||
search: {
|
||||
kuery: `${apmField}:"${node.id}"`,
|
||||
},
|
||||
});
|
||||
const uptimeMenuItemLinkProps = useLinkProps(createUptimeLink(options, nodeType, node));
|
||||
const nodeLogsMenuItemLinkProps = useLinkProps({
|
||||
app: 'logs',
|
||||
...getNodeLogsUrl({
|
||||
nodeType,
|
||||
nodeId: node.id,
|
||||
time: currentTime,
|
||||
}),
|
||||
});
|
||||
const nodeDetailMenuItemLinkProps = useLinkProps({
|
||||
...getNodeDetailUrl({
|
||||
nodeType,
|
||||
nodeId: node.id,
|
||||
from: nodeDetailFrom,
|
||||
to: currentTime,
|
||||
}),
|
||||
});
|
||||
const apmTracesMenuItemLinkProps = useLinkProps({
|
||||
app: 'apm',
|
||||
hash: 'traces',
|
||||
search: {
|
||||
kuery: `${apmField}:"${node.id}"`,
|
||||
},
|
||||
});
|
||||
const uptimeMenuItemLinkProps = useLinkProps(createUptimeLink(options, nodeType, node));
|
||||
|
||||
const nodeLogsMenuItem: SectionLinkProps = {
|
||||
label: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', {
|
||||
defaultMessage: '{inventoryName} logs',
|
||||
values: { inventoryName: inventoryModel.singularDisplayName },
|
||||
}),
|
||||
...nodeLogsMenuItemLinkProps,
|
||||
'data-test-subj': 'viewLogsContextMenuItem',
|
||||
isDisabled: !showLogsLink,
|
||||
};
|
||||
const nodeLogsMenuItem: SectionLinkProps = {
|
||||
label: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', {
|
||||
defaultMessage: '{inventoryName} logs',
|
||||
values: { inventoryName: inventoryModel.singularDisplayName },
|
||||
}),
|
||||
...nodeLogsMenuItemLinkProps,
|
||||
'data-test-subj': 'viewLogsContextMenuItem',
|
||||
isDisabled: !showLogsLink,
|
||||
};
|
||||
|
||||
const nodeDetailMenuItem: SectionLinkProps = {
|
||||
label: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', {
|
||||
defaultMessage: '{inventoryName} metrics',
|
||||
values: { inventoryName: inventoryModel.singularDisplayName },
|
||||
}),
|
||||
...nodeDetailMenuItemLinkProps,
|
||||
isDisabled: !showDetail,
|
||||
};
|
||||
const nodeDetailMenuItem: SectionLinkProps = {
|
||||
label: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', {
|
||||
defaultMessage: '{inventoryName} metrics',
|
||||
values: { inventoryName: inventoryModel.singularDisplayName },
|
||||
}),
|
||||
...nodeDetailMenuItemLinkProps,
|
||||
isDisabled: !showDetail,
|
||||
};
|
||||
|
||||
const apmTracesMenuItem: SectionLinkProps = {
|
||||
label: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', {
|
||||
defaultMessage: '{inventoryName} APM traces',
|
||||
values: { inventoryName: inventoryModel.singularDisplayName },
|
||||
}),
|
||||
...apmTracesMenuItemLinkProps,
|
||||
'data-test-subj': 'viewApmTracesContextMenuItem',
|
||||
isDisabled: !showAPMTraceLink,
|
||||
};
|
||||
const apmTracesMenuItem: SectionLinkProps = {
|
||||
label: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', {
|
||||
defaultMessage: '{inventoryName} APM traces',
|
||||
values: { inventoryName: inventoryModel.singularDisplayName },
|
||||
}),
|
||||
...apmTracesMenuItemLinkProps,
|
||||
'data-test-subj': 'viewApmTracesContextMenuItem',
|
||||
isDisabled: !showAPMTraceLink,
|
||||
};
|
||||
|
||||
const uptimeMenuItem: SectionLinkProps = {
|
||||
label: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', {
|
||||
defaultMessage: '{inventoryName} in Uptime',
|
||||
values: { inventoryName: inventoryModel.singularDisplayName },
|
||||
}),
|
||||
...uptimeMenuItemLinkProps,
|
||||
isDisabled: !showUptimeLink,
|
||||
};
|
||||
const uptimeMenuItem: SectionLinkProps = {
|
||||
label: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', {
|
||||
defaultMessage: '{inventoryName} in Uptime',
|
||||
values: { inventoryName: inventoryModel.singularDisplayName },
|
||||
}),
|
||||
...uptimeMenuItemLinkProps,
|
||||
isDisabled: !showUptimeLink,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionMenu
|
||||
closePopover={closePopover}
|
||||
id={`${node.pathId}-popover`}
|
||||
isOpen={isPopoverOpen}
|
||||
button={children!}
|
||||
anchorPosition={popoverPosition}
|
||||
>
|
||||
<div style={{ maxWidth: 300 }} data-test-subj="nodeContextMenu">
|
||||
<Section>
|
||||
<SectionTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.nodeContextMenu.title"
|
||||
defaultMessage="{inventoryName} details"
|
||||
values={{ inventoryName: inventoryModel.singularDisplayName }}
|
||||
/>
|
||||
</SectionTitle>
|
||||
{inventoryId.label && (
|
||||
<SectionSubtitle>
|
||||
<div style={{ wordBreak: 'break-all' }}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.nodeContextMenu.description"
|
||||
defaultMessage="View details for {label} {value}"
|
||||
values={{ label: inventoryId.label, value: inventoryId.value }}
|
||||
/>
|
||||
</div>
|
||||
</SectionSubtitle>
|
||||
)}
|
||||
<SectionLinks>
|
||||
<SectionLink data-test-subj="viewLogsContextMenuItem" {...nodeLogsMenuItem} />
|
||||
<SectionLink {...nodeDetailMenuItem} />
|
||||
<SectionLink data-test-subj="viewApmTracesContextMenuItem" {...apmTracesMenuItem} />
|
||||
<SectionLink {...uptimeMenuItem} />
|
||||
</SectionLinks>
|
||||
</Section>
|
||||
</div>
|
||||
</ActionMenu>
|
||||
<AlertFlyout
|
||||
options={{ filterQuery: `${nodeType}: ${node.id}` }}
|
||||
setVisible={setFlyoutVisible}
|
||||
visible={flyoutVisible}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const createAlertMenuItem: SectionLinkProps = {
|
||||
label: i18n.translate('xpack.infra.nodeContextMenu.createAlertLink', {
|
||||
defaultMessage: 'Create alert',
|
||||
}),
|
||||
style: { color: theme?.eui.euiLinkColor || '#006BB4', fontWeight: 500, padding: 0 },
|
||||
onClick: () => {
|
||||
setFlyoutVisible(true);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionMenu
|
||||
closePopover={closePopover}
|
||||
id={`${node.pathId}-popover`}
|
||||
isOpen={isPopoverOpen}
|
||||
button={children!}
|
||||
anchorPosition={popoverPosition}
|
||||
>
|
||||
<div style={{ maxWidth: 300 }} data-test-subj="nodeContextMenu">
|
||||
<Section>
|
||||
<SectionTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.nodeContextMenu.title"
|
||||
defaultMessage="{inventoryName} details"
|
||||
values={{ inventoryName: inventoryModel.singularDisplayName }}
|
||||
/>
|
||||
</SectionTitle>
|
||||
{inventoryId.label && (
|
||||
<SectionSubtitle>
|
||||
<div style={{ wordBreak: 'break-all' }}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.nodeContextMenu.description"
|
||||
defaultMessage="View details for {label} {value}"
|
||||
values={{ label: inventoryId.label, value: inventoryId.value }}
|
||||
/>
|
||||
</div>
|
||||
</SectionSubtitle>
|
||||
)}
|
||||
<SectionLinks>
|
||||
<SectionLink data-test-subj="viewLogsContextMenuItem" {...nodeLogsMenuItem} />
|
||||
<SectionLink {...nodeDetailMenuItem} />
|
||||
<SectionLink data-test-subj="viewApmTracesContextMenuItem" {...apmTracesMenuItem} />
|
||||
<SectionLink {...uptimeMenuItem} />
|
||||
<SectionLink {...createAlertMenuItem} />
|
||||
</SectionLinks>
|
||||
</Section>
|
||||
</div>
|
||||
</ActionMenu>
|
||||
<AlertFlyout
|
||||
filter={
|
||||
options.fields
|
||||
? `${findInventoryFields(nodeType, options.fields).id}: "${node.id}"`
|
||||
: ''
|
||||
}
|
||||
options={options}
|
||||
nodeType={nodeType}
|
||||
setVisible={setFlyoutVisible}
|
||||
visible={flyoutVisible}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { createFormatter } from '../../../../utils/formatters';
|
||||
import { InfraFormatterType } from '../../../../lib/lib';
|
||||
import {
|
||||
SnapshotMetricInput,
|
||||
SnapshotCustomMetricInputRT,
|
||||
} from '../../../../../common/http_api/snapshot_api';
|
||||
import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric';
|
||||
import { createFormatter } from '../../../../../common/formatters';
|
||||
|
||||
interface MetricFormatter {
|
||||
formatter: InfraFormatterType;
|
||||
|
|
|
@ -17,7 +17,7 @@ import { get, last, max } from 'lodash';
|
|||
import React, { ReactText } from 'react';
|
||||
|
||||
import { euiStyled } from '../../../../../../observability/public';
|
||||
import { createFormatter } from '../../../../utils/formatters';
|
||||
import { createFormatter } from '../../../../../common/formatters';
|
||||
import { InventoryFormatterType } from '../../../../../common/inventory_models/types';
|
||||
import { SeriesOverrides, VisSectionProps } from '../types';
|
||||
import { getChartName } from './helpers';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { ReactText } from 'react';
|
||||
import Color from 'color';
|
||||
import { get, first, last, min, max } from 'lodash';
|
||||
import { createFormatter } from '../../../../utils/formatters';
|
||||
import { createFormatter } from '../../../../../common/formatters';
|
||||
import { InfraDataSeries } from '../../../../graphql/types';
|
||||
import {
|
||||
InventoryVisTypeRT,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer';
|
||||
import { createFormatter } from '../../../../../utils/formatters';
|
||||
import { createFormatter } from '../../../../../../common/formatters';
|
||||
import { InfraFormatterType } from '../../../../../lib/lib';
|
||||
import { metricToFormat } from './metric_to_format';
|
||||
export const createFormatterForMetric = (metric?: MetricsExplorerMetric) => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data
|
|||
interface Props {
|
||||
derivedIndexPattern: IIndexPattern;
|
||||
onSubmit: (query: string) => void;
|
||||
onChange?: (query: string) => void;
|
||||
value?: string | null;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
@ -30,6 +31,7 @@ function validateQuery(query: string) {
|
|||
export const MetricsExplorerKueryBar = ({
|
||||
derivedIndexPattern,
|
||||
onSubmit,
|
||||
onChange,
|
||||
value,
|
||||
placeholder,
|
||||
}: Props) => {
|
||||
|
@ -46,6 +48,9 @@ export const MetricsExplorerKueryBar = ({
|
|||
const handleChange = (query: string) => {
|
||||
setValidation(validateQuery(query));
|
||||
setDraftQuery(query);
|
||||
if (onChange) {
|
||||
onChange(query);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredDerivedIndexPattern = {
|
||||
|
|
|
@ -22,6 +22,7 @@ import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public
|
|||
|
||||
import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public';
|
||||
import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type';
|
||||
import { getInventoryMetricAlertType } from './components/alerting/inventory/metric_inventory_threshold_alert_type';
|
||||
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
|
||||
|
||||
export type ClientSetup = void;
|
||||
|
@ -53,6 +54,7 @@ export class Plugin
|
|||
setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) {
|
||||
registerFeatures(pluginsSetup.home);
|
||||
|
||||
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getInventoryMetricAlertType());
|
||||
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType());
|
||||
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createMetricThresholdAlertType());
|
||||
|
||||
|
|
|
@ -101,7 +101,9 @@ export const createSourcesResolvers = (
|
|||
return requestedSourceConfiguration;
|
||||
},
|
||||
async allSources(root, args, { req }) {
|
||||
const sourceConfigurations = await libs.sources.getAllSourceConfigurations(req);
|
||||
const sourceConfigurations = await libs.sources.getAllSourceConfigurations(
|
||||
req.core.savedObjects.client
|
||||
);
|
||||
|
||||
return sourceConfigurations;
|
||||
},
|
||||
|
@ -131,7 +133,7 @@ export const createSourcesResolvers = (
|
|||
Mutation: {
|
||||
async createSource(root, args, { req }) {
|
||||
const sourceConfiguration = await libs.sources.createSourceConfiguration(
|
||||
req,
|
||||
req.core.savedObjects.client,
|
||||
args.id,
|
||||
compactObject({
|
||||
...args.sourceProperties,
|
||||
|
@ -147,7 +149,7 @@ export const createSourcesResolvers = (
|
|||
};
|
||||
},
|
||||
async deleteSource(root, args, { req }) {
|
||||
await libs.sources.deleteSourceConfiguration(req, args.id);
|
||||
await libs.sources.deleteSourceConfiguration(req.core.savedObjects.client, args.id);
|
||||
|
||||
return {
|
||||
id: args.id,
|
||||
|
@ -155,7 +157,7 @@ export const createSourcesResolvers = (
|
|||
},
|
||||
async updateSource(root, args, { req }) {
|
||||
const updatedSourceConfiguration = await libs.sources.updateSourceConfiguration(
|
||||
req,
|
||||
req.core.savedObjects.client,
|
||||
args.id,
|
||||
compactObject({
|
||||
...args.sourceProperties,
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
InventoryMetricRT,
|
||||
} from '../../../../common/inventory_models/types';
|
||||
import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';
|
||||
import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../framework';
|
||||
|
||||
export class KibanaMetricsAdapter implements InfraMetricsAdapter {
|
||||
private framework: KibanaFramework;
|
||||
|
@ -120,9 +121,14 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
|
|||
indexPattern,
|
||||
options.timerange.interval
|
||||
);
|
||||
|
||||
const client = <Hit = {}, Aggregation = undefined>(
|
||||
opts: CallWithRequestParams
|
||||
): Promise<InfraDatabaseSearchResponse<Hit, Aggregation>> =>
|
||||
this.framework.callWithRequest(requestContext, 'search', opts);
|
||||
|
||||
const calculatedInterval = await calculateMetricInterval(
|
||||
this.framework,
|
||||
requestContext,
|
||||
client,
|
||||
{
|
||||
indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`,
|
||||
timestampField: options.sourceConfiguration.fields.timestamp,
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* 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 { mapValues, last, get } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
InfraDatabaseSearchResponse,
|
||||
CallWithRequestParams,
|
||||
} from '../../adapters/framework/adapter_types';
|
||||
import { Comparator, AlertStates, InventoryMetricConditions } from './types';
|
||||
import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server';
|
||||
import { InfraSnapshot } from '../../snapshot';
|
||||
import { parseFilterQuery } from '../../../utils/serialized_query';
|
||||
import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
|
||||
import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api';
|
||||
import { InfraSourceConfiguration } from '../../sources';
|
||||
import { InfraBackendLibs } from '../../infra_types';
|
||||
import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric_formats';
|
||||
import { createFormatter } from '../../../../common/formatters';
|
||||
|
||||
interface InventoryMetricThresholdParams {
|
||||
criteria: InventoryMetricConditions[];
|
||||
groupBy: string | undefined;
|
||||
filterQuery: string | undefined;
|
||||
nodeType: InventoryItemType;
|
||||
sourceId?: string;
|
||||
}
|
||||
|
||||
export const createInventoryMetricThresholdExecutor = (
|
||||
libs: InfraBackendLibs,
|
||||
alertId: string
|
||||
) => async ({ services, params }: AlertExecutorOptions) => {
|
||||
const { criteria, filterQuery, sourceId, nodeType } = params as InventoryMetricThresholdParams;
|
||||
|
||||
const source = await libs.sources.getSourceConfiguration(
|
||||
services.savedObjectsClient,
|
||||
sourceId || 'default'
|
||||
);
|
||||
|
||||
const results = await Promise.all(
|
||||
criteria.map(c => evaluateCondtion(c, nodeType, source.configuration, services, filterQuery))
|
||||
);
|
||||
|
||||
const invenotryItems = Object.keys(results[0]);
|
||||
for (const item of invenotryItems) {
|
||||
const alertInstance = services.alertInstanceFactory(`${alertId}-${item}`);
|
||||
// AND logic; all criteria must be across the threshold
|
||||
const shouldAlertFire = results.every(result => result[item].shouldFire);
|
||||
|
||||
// AND logic; because we need to evaluate all criteria, if one of them reports no data then the
|
||||
// whole alert is in a No Data/Error state
|
||||
const isNoData = results.some(result => result[item].isNoData);
|
||||
const isError = results.some(result => result[item].isError);
|
||||
|
||||
if (shouldAlertFire) {
|
||||
alertInstance.scheduleActions(FIRED_ACTIONS.id, {
|
||||
group: item,
|
||||
item,
|
||||
valueOf: mapToConditionsLookup(results, result =>
|
||||
formatMetric(result[item].metric, result[item].currentValue)
|
||||
),
|
||||
thresholdOf: mapToConditionsLookup(criteria, c => c.threshold),
|
||||
metricOf: mapToConditionsLookup(criteria, c => c.metric),
|
||||
});
|
||||
}
|
||||
|
||||
alertInstance.replaceState({
|
||||
alertState: isError
|
||||
? AlertStates.ERROR
|
||||
: isNoData
|
||||
? AlertStates.NO_DATA
|
||||
: shouldAlertFire
|
||||
? AlertStates.ALERT
|
||||
: AlertStates.OK,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
interface ConditionResult {
|
||||
shouldFire: boolean;
|
||||
currentValue?: number | null;
|
||||
isNoData: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
const evaluateCondtion = async (
|
||||
condition: InventoryMetricConditions,
|
||||
nodeType: InventoryItemType,
|
||||
sourceConfiguration: InfraSourceConfiguration,
|
||||
services: AlertServices,
|
||||
filterQuery?: string
|
||||
): Promise<Record<string, ConditionResult>> => {
|
||||
const { comparator, metric } = condition;
|
||||
let { threshold } = condition;
|
||||
|
||||
const currentValues = await getData(
|
||||
services,
|
||||
nodeType,
|
||||
metric,
|
||||
{
|
||||
to: Date.now(),
|
||||
from: moment()
|
||||
.subtract(condition.timeSize, condition.timeUnit)
|
||||
.toDate()
|
||||
.getTime(),
|
||||
interval: condition.timeUnit,
|
||||
},
|
||||
sourceConfiguration,
|
||||
filterQuery
|
||||
);
|
||||
|
||||
threshold = threshold.map(n => convertMetricValue(metric, n));
|
||||
|
||||
const comparisonFunction = comparatorMap[comparator];
|
||||
|
||||
return mapValues(currentValues, value => ({
|
||||
shouldFire: value !== undefined && value !== null && comparisonFunction(value, threshold),
|
||||
metric,
|
||||
currentValue: value,
|
||||
isNoData: value === null,
|
||||
isError: value === undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
const getData = async (
|
||||
services: AlertServices,
|
||||
nodeType: InventoryItemType,
|
||||
metric: SnapshotMetricType,
|
||||
timerange: InfraTimerangeInput,
|
||||
sourceConfiguration: InfraSourceConfiguration,
|
||||
filterQuery?: string
|
||||
) => {
|
||||
const snapshot = new InfraSnapshot();
|
||||
const esClient = <Hit = {}, Aggregation = undefined>(
|
||||
options: CallWithRequestParams
|
||||
): Promise<InfraDatabaseSearchResponse<Hit, Aggregation>> =>
|
||||
services.callCluster('search', options);
|
||||
|
||||
const options = {
|
||||
filterQuery: parseFilterQuery(filterQuery),
|
||||
nodeType,
|
||||
groupBy: [],
|
||||
sourceConfiguration,
|
||||
metric: { type: metric },
|
||||
timerange,
|
||||
};
|
||||
|
||||
const { nodes } = await snapshot.getNodes(esClient, options);
|
||||
|
||||
return nodes.reduce((acc, n) => {
|
||||
const nodePathItem = last(n.path);
|
||||
acc[nodePathItem.label] = n.metric && n.metric.value;
|
||||
return acc;
|
||||
}, {} as Record<string, number | undefined | null>);
|
||||
};
|
||||
|
||||
const comparatorMap = {
|
||||
[Comparator.BETWEEN]: (value: number, [a, b]: number[]) =>
|
||||
value >= Math.min(a, b) && value <= Math.max(a, b),
|
||||
// `threshold` is always an array of numbers in case the BETWEEN comparator is
|
||||
// used; all other compartors will just destructure the first value in the array
|
||||
[Comparator.GT]: (a: number, [b]: number[]) => a > b,
|
||||
[Comparator.LT]: (a: number, [b]: number[]) => a < b,
|
||||
[Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b,
|
||||
[Comparator.GT_OR_EQ]: (a: number, [b]: number[]) => a >= b,
|
||||
[Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b,
|
||||
};
|
||||
|
||||
const mapToConditionsLookup = (
|
||||
list: any[],
|
||||
mapFn: (value: any, index: number, array: any[]) => unknown
|
||||
) =>
|
||||
list
|
||||
.map(mapFn)
|
||||
.reduce(
|
||||
(result: Record<string, any>, value, i) => ({ ...result, [`condition${i}`]: value }),
|
||||
{}
|
||||
);
|
||||
|
||||
export const FIRED_ACTIONS = {
|
||||
id: 'metrics.invenotry_threshold.fired',
|
||||
name: i18n.translate('xpack.infra.metrics.alerting.inventory.threshold.fired', {
|
||||
defaultMessage: 'Fired',
|
||||
}),
|
||||
};
|
||||
|
||||
// Some metrics in the UI are in a different unit that what we store in ES.
|
||||
const convertMetricValue = (metric: SnapshotMetricType, value: number) => {
|
||||
if (converters[metric]) {
|
||||
return converters[metric](value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
const converters: Record<string, (n: number) => number> = {
|
||||
cpu: n => Number(n) / 100,
|
||||
memory: n => Number(n) / 100,
|
||||
};
|
||||
|
||||
const formatMetric = (metric: SnapshotMetricType, value: number) => {
|
||||
// if (SnapshotCustomMetricInputRT.is(metric)) {
|
||||
// const formatter = createFormatterForMetric(metric);
|
||||
// return formatter(val);
|
||||
// }
|
||||
const metricFormatter = get(METRIC_FORMATTERS, metric, METRIC_FORMATTERS.count);
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template);
|
||||
return formatter(value);
|
||||
};
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { curry } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import {
|
||||
createInventoryMetricThresholdExecutor,
|
||||
FIRED_ACTIONS,
|
||||
} from './inventory_metric_threshold_executor';
|
||||
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './types';
|
||||
import { InfraBackendLibs } from '../../infra_types';
|
||||
|
||||
const condition = schema.object({
|
||||
threshold: schema.arrayOf(schema.number()),
|
||||
comparator: schema.oneOf([
|
||||
schema.literal('>'),
|
||||
schema.literal('<'),
|
||||
schema.literal('>='),
|
||||
schema.literal('<='),
|
||||
schema.literal('between'),
|
||||
schema.literal('outside'),
|
||||
]),
|
||||
timeUnit: schema.string(),
|
||||
timeSize: schema.number(),
|
||||
metric: schema.string(),
|
||||
});
|
||||
|
||||
export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs) => ({
|
||||
id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
|
||||
name: 'Inventory',
|
||||
validate: {
|
||||
params: schema.object(
|
||||
{
|
||||
criteria: schema.arrayOf(condition),
|
||||
nodeType: schema.string(),
|
||||
filterQuery: schema.maybe(schema.string()),
|
||||
sourceId: schema.string(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
},
|
||||
defaultActionGroupId: FIRED_ACTIONS.id,
|
||||
actionGroups: [FIRED_ACTIONS],
|
||||
executor: curry(createInventoryMetricThresholdExecutor)(libs, uuid.v4()),
|
||||
actionVariables: {
|
||||
context: [
|
||||
{
|
||||
name: 'group',
|
||||
description: i18n.translate(
|
||||
'xpack.infra.metrics.alerting.threshold.alerting.groupActionVariableDescription',
|
||||
{
|
||||
defaultMessage: 'Name of the group reporting data',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'valueOf',
|
||||
description: i18n.translate(
|
||||
'xpack.infra.metrics.alerting.threshold.alerting.valueOfActionVariableDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Record of the current value of the watched metric; grouped by condition, i.e valueOf.condition0, valueOf.condition1, etc.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'thresholdOf',
|
||||
description: i18n.translate(
|
||||
'xpack.infra.metrics.alerting.threshold.alerting.thresholdOfActionVariableDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Record of the alerting threshold; grouped by condition, i.e thresholdOf.condition0, thresholdOf.condition1, etc.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'metricOf',
|
||||
description: i18n.translate(
|
||||
'xpack.infra.metrics.alerting.threshold.alerting.metricOfActionVariableDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Record of the watched metric; grouped by condition, i.e metricOf.condition0, metricOf.condition1, etc.',
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { SnapshotMetricType } from '../../../../common/inventory_models/types';
|
||||
|
||||
export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';
|
||||
|
||||
export enum Comparator {
|
||||
GT = '>',
|
||||
LT = '<',
|
||||
GT_OR_EQ = '>=',
|
||||
LT_OR_EQ = '<=',
|
||||
BETWEEN = 'between',
|
||||
OUTSIDE_RANGE = 'outside',
|
||||
}
|
||||
|
||||
export enum AlertStates {
|
||||
OK,
|
||||
ALERT,
|
||||
NO_DATA,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
export type TimeUnit = 's' | 'm' | 'h' | 'd';
|
||||
|
||||
export interface InventoryMetricConditions {
|
||||
metric: SnapshotMetricType;
|
||||
timeSize: number;
|
||||
timeUnit: TimeUnit;
|
||||
sourceId?: string;
|
||||
threshold: number[];
|
||||
comparator: Comparator;
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor';
|
||||
import { Comparator, AlertStates } from './types';
|
||||
import * as mocks from './test_mocks';
|
||||
|
@ -13,81 +12,14 @@ import {
|
|||
AlertServicesMock,
|
||||
AlertInstanceMock,
|
||||
} from '../../../../../alerting/server/mocks';
|
||||
|
||||
const executor = createMetricThresholdExecutor('test') as (opts: {
|
||||
params: AlertExecutorOptions['params'];
|
||||
services: { callCluster: AlertExecutorOptions['params']['callCluster'] };
|
||||
}) => Promise<void>;
|
||||
|
||||
const services: AlertServicesMock = alertsMock.createAlertServices();
|
||||
services.callCluster.mockImplementation(async (_: string, { body, index }: any) => {
|
||||
if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse;
|
||||
const metric = body.query.bool.filter[1]?.exists.field;
|
||||
if (body.aggs.groupings) {
|
||||
if (body.aggs.groupings.composite.after) {
|
||||
return mocks.compositeEndResponse;
|
||||
}
|
||||
if (metric === 'test.metric.2') {
|
||||
return mocks.alternateCompositeResponse;
|
||||
}
|
||||
return mocks.basicCompositeResponse;
|
||||
}
|
||||
if (metric === 'test.metric.2') {
|
||||
return mocks.alternateMetricResponse;
|
||||
} else if (metric === 'test.metric.3') {
|
||||
return mocks.emptyMetricResponse;
|
||||
}
|
||||
return mocks.basicMetricResponse;
|
||||
});
|
||||
services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => {
|
||||
if (sourceId === 'alternate')
|
||||
return {
|
||||
id: 'alternate',
|
||||
attributes: { metricAlias: 'alternatebeat-*' },
|
||||
type,
|
||||
references: [],
|
||||
};
|
||||
return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] };
|
||||
});
|
||||
import { InfraSources } from '../../sources';
|
||||
|
||||
interface AlertTestInstance {
|
||||
instance: AlertInstanceMock;
|
||||
actionQueue: any[];
|
||||
state: any;
|
||||
}
|
||||
const alertInstances = new Map<string, AlertTestInstance>();
|
||||
services.alertInstanceFactory.mockImplementation((instanceID: string) => {
|
||||
const alertInstance: AlertTestInstance = {
|
||||
instance: alertsMock.createAlertInstanceFactory(),
|
||||
actionQueue: [],
|
||||
state: {},
|
||||
};
|
||||
alertInstances.set(instanceID, alertInstance);
|
||||
alertInstance.instance.replaceState.mockImplementation((newState: any) => {
|
||||
alertInstance.state = newState;
|
||||
return alertInstance.instance;
|
||||
});
|
||||
alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => {
|
||||
alertInstance.actionQueue.push({ id, action });
|
||||
return alertInstance.instance;
|
||||
});
|
||||
return alertInstance.instance;
|
||||
});
|
||||
|
||||
function mostRecentAction(id: string) {
|
||||
return alertInstances.get(id)!.actionQueue.pop();
|
||||
}
|
||||
|
||||
function getState(id: string) {
|
||||
return alertInstances.get(id)!.state;
|
||||
}
|
||||
|
||||
const baseCriterion = {
|
||||
aggType: 'avg',
|
||||
metric: 'test.metric.1',
|
||||
timeSize: 1,
|
||||
timeUnit: 'm',
|
||||
};
|
||||
describe('The metric threshold alert type', () => {
|
||||
describe('querying the entire infrastructure', () => {
|
||||
const instanceID = 'test-*';
|
||||
|
@ -167,14 +99,6 @@ describe('The metric threshold alert type', () => {
|
|||
expect(action.reason).toContain('threshold of 0.75');
|
||||
expect(action.reason).toContain('test.metric.1');
|
||||
});
|
||||
test('fetches the index pattern dynamically', async () => {
|
||||
await execute(Comparator.LT, [17], 'alternate');
|
||||
expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id);
|
||||
expect(getState(instanceID).alertState).toBe(AlertStates.ALERT);
|
||||
await execute(Comparator.LT, [1.5], 'alternate');
|
||||
expect(mostRecentAction(instanceID)).toBe(undefined);
|
||||
expect(getState(instanceID).alertState).toBe(AlertStates.OK);
|
||||
});
|
||||
});
|
||||
|
||||
describe('querying with a groupBy parameter', () => {
|
||||
|
@ -338,3 +262,117 @@ describe('The metric threshold alert type', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
const createMockStaticConfiguration = (sources: any) => ({
|
||||
enabled: true,
|
||||
query: {
|
||||
partitionSize: 1,
|
||||
partitionFactor: 1,
|
||||
},
|
||||
sources,
|
||||
});
|
||||
|
||||
const mockLibs: any = {
|
||||
sources: new InfraSources({
|
||||
config: createMockStaticConfiguration({}),
|
||||
}),
|
||||
configuration: createMockStaticConfiguration({}),
|
||||
};
|
||||
|
||||
const executor = createMetricThresholdExecutor(mockLibs, 'test') as (opts: {
|
||||
params: AlertExecutorOptions['params'];
|
||||
services: { callCluster: AlertExecutorOptions['params']['callCluster'] };
|
||||
}) => Promise<void>;
|
||||
|
||||
const services: AlertServicesMock = alertsMock.createAlertServices();
|
||||
services.callCluster.mockImplementation(async (_: string, { body, index }: any) => {
|
||||
if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse;
|
||||
const metric = body.query.bool.filter[1]?.exists.field;
|
||||
if (body.aggs.groupings) {
|
||||
if (body.aggs.groupings.composite.after) {
|
||||
return mocks.compositeEndResponse;
|
||||
}
|
||||
if (metric === 'test.metric.2') {
|
||||
return mocks.alternateCompositeResponse;
|
||||
}
|
||||
return mocks.basicCompositeResponse;
|
||||
}
|
||||
if (metric === 'test.metric.2') {
|
||||
return mocks.alternateMetricResponse;
|
||||
}
|
||||
return mocks.basicMetricResponse;
|
||||
});
|
||||
services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => {
|
||||
if (sourceId === 'alternate')
|
||||
return {
|
||||
id: 'alternate',
|
||||
attributes: { metricAlias: 'alternatebeat-*' },
|
||||
type,
|
||||
references: [],
|
||||
};
|
||||
return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] };
|
||||
});
|
||||
|
||||
services.callCluster.mockImplementation(async (_: string, { body, index }: any) => {
|
||||
if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse;
|
||||
const metric = body.query.bool.filter[1]?.exists.field;
|
||||
if (body.aggs.groupings) {
|
||||
if (body.aggs.groupings.composite.after) {
|
||||
return mocks.compositeEndResponse;
|
||||
}
|
||||
if (metric === 'test.metric.2') {
|
||||
return mocks.alternateCompositeResponse;
|
||||
}
|
||||
return mocks.basicCompositeResponse;
|
||||
}
|
||||
if (metric === 'test.metric.2') {
|
||||
return mocks.alternateMetricResponse;
|
||||
} else if (metric === 'test.metric.3') {
|
||||
return mocks.emptyMetricResponse;
|
||||
}
|
||||
return mocks.basicMetricResponse;
|
||||
});
|
||||
services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => {
|
||||
if (sourceId === 'alternate')
|
||||
return {
|
||||
id: 'alternate',
|
||||
attributes: { metricAlias: 'alternatebeat-*' },
|
||||
type,
|
||||
references: [],
|
||||
};
|
||||
return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] };
|
||||
});
|
||||
|
||||
const alertInstances = new Map<string, AlertTestInstance>();
|
||||
services.alertInstanceFactory.mockImplementation((instanceID: string) => {
|
||||
const alertInstance: AlertTestInstance = {
|
||||
instance: alertsMock.createAlertInstanceFactory(),
|
||||
actionQueue: [],
|
||||
state: {},
|
||||
};
|
||||
alertInstances.set(instanceID, alertInstance);
|
||||
alertInstance.instance.replaceState.mockImplementation((newState: any) => {
|
||||
alertInstance.state = newState;
|
||||
return alertInstance.instance;
|
||||
});
|
||||
alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => {
|
||||
alertInstance.actionQueue.push({ id, action });
|
||||
return alertInstance.instance;
|
||||
});
|
||||
return alertInstance.instance;
|
||||
});
|
||||
|
||||
function mostRecentAction(id: string) {
|
||||
return alertInstances.get(id)!.actionQueue.pop();
|
||||
}
|
||||
|
||||
function getState(id: string) {
|
||||
return alertInstances.get(id)!.state;
|
||||
}
|
||||
|
||||
const baseCriterion = {
|
||||
aggType: 'avg',
|
||||
metric: 'test.metric.1',
|
||||
timeSize: 1,
|
||||
timeUnit: 'm',
|
||||
};
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
*/
|
||||
import { mapValues } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { convertSavedObjectToSavedSourceConfiguration } from '../../sources/sources';
|
||||
import { infraSourceConfigurationSavedObjectType } from '../../sources/saved_object_mappings';
|
||||
import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types';
|
||||
import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler';
|
||||
import { getAllCompositeData } from '../../../utils/get_all_composite_data';
|
||||
|
@ -22,9 +20,9 @@ import {
|
|||
import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server';
|
||||
import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
|
||||
import { getDateHistogramOffset } from '../../snapshot/query_helpers';
|
||||
import { InfraBackendLibs } from '../../infra_types';
|
||||
|
||||
const TOTAL_BUCKETS = 5;
|
||||
const DEFAULT_INDEX_PATTERN = 'metricbeat-*';
|
||||
|
||||
interface Aggregation {
|
||||
aggregatedIntervals: {
|
||||
|
@ -76,6 +74,7 @@ const getParsedFilterQuery: (
|
|||
|
||||
export const getElasticsearchMetricQuery = (
|
||||
{ metric, aggType, timeUnit, timeSize }: MetricExpressionParams,
|
||||
timefield: string,
|
||||
groupBy?: string,
|
||||
filterQuery?: string
|
||||
) => {
|
||||
|
@ -109,7 +108,7 @@ export const getElasticsearchMetricQuery = (
|
|||
const baseAggs = {
|
||||
aggregatedIntervals: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
field: timefield,
|
||||
fixed_interval: interval,
|
||||
offset,
|
||||
extended_bounds: {
|
||||
|
@ -181,43 +180,23 @@ export const getElasticsearchMetricQuery = (
|
|||
};
|
||||
};
|
||||
|
||||
const getIndexPattern: (
|
||||
services: AlertServices,
|
||||
sourceId?: string
|
||||
) => Promise<string> = async function({ savedObjectsClient }, sourceId = 'default') {
|
||||
try {
|
||||
const sourceConfiguration = await savedObjectsClient.get(
|
||||
infraSourceConfigurationSavedObjectType,
|
||||
sourceId
|
||||
);
|
||||
const { metricAlias } = convertSavedObjectToSavedSourceConfiguration(
|
||||
sourceConfiguration
|
||||
).configuration;
|
||||
return metricAlias || DEFAULT_INDEX_PATTERN;
|
||||
} catch (e) {
|
||||
if (e.output.statusCode === 404) {
|
||||
return DEFAULT_INDEX_PATTERN;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getMetric: (
|
||||
services: AlertServices,
|
||||
params: MetricExpressionParams,
|
||||
index: string,
|
||||
timefield: string,
|
||||
groupBy: string | undefined,
|
||||
filterQuery: string | undefined
|
||||
) => Promise<Record<string, number>> = async function(
|
||||
{ savedObjectsClient, callCluster },
|
||||
{ callCluster },
|
||||
params,
|
||||
index,
|
||||
timefield,
|
||||
groupBy,
|
||||
filterQuery
|
||||
) {
|
||||
const { aggType } = params;
|
||||
const searchBody = getElasticsearchMetricQuery(params, groupBy, filterQuery);
|
||||
const searchBody = getElasticsearchMetricQuery(params, timefield, groupBy, filterQuery);
|
||||
|
||||
try {
|
||||
if (groupBy) {
|
||||
|
@ -265,7 +244,7 @@ const comparatorMap = {
|
|||
[Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b,
|
||||
};
|
||||
|
||||
export const createMetricThresholdExecutor = (alertUUID: string) =>
|
||||
export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: string) =>
|
||||
async function({ services, params }: AlertExecutorOptions) {
|
||||
const { criteria, groupBy, filterQuery, sourceId, alertOnNoData } = params as {
|
||||
criteria: MetricExpressionParams[];
|
||||
|
@ -275,11 +254,22 @@ export const createMetricThresholdExecutor = (alertUUID: string) =>
|
|||
alertOnNoData: boolean;
|
||||
};
|
||||
|
||||
const source = await libs.sources.getSourceConfiguration(
|
||||
services.savedObjectsClient,
|
||||
sourceId || 'default'
|
||||
);
|
||||
const config = source.configuration;
|
||||
const alertResults = await Promise.all(
|
||||
criteria.map(criterion =>
|
||||
(async () => {
|
||||
const index = await getIndexPattern(services, sourceId);
|
||||
const currentValues = await getMetric(services, criterion, index, groupBy, filterQuery);
|
||||
criteria.map(criterion => {
|
||||
return (async () => {
|
||||
const currentValues = await getMetric(
|
||||
services,
|
||||
criterion,
|
||||
config.fields.timestamp,
|
||||
config.metricAlias,
|
||||
groupBy,
|
||||
filterQuery
|
||||
);
|
||||
const { threshold, comparator } = criterion;
|
||||
const comparisonFunction = comparatorMap[comparator];
|
||||
return mapValues(currentValues, value => ({
|
||||
|
@ -291,13 +281,14 @@ export const createMetricThresholdExecutor = (alertUUID: string) =>
|
|||
isNoData: value === null,
|
||||
isError: value === undefined,
|
||||
}));
|
||||
})()
|
||||
)
|
||||
})();
|
||||
})
|
||||
);
|
||||
|
||||
// Because each alert result has the same group definitions, just grap the groups from the first one.
|
||||
const groups = Object.keys(alertResults[0]);
|
||||
for (const group of groups) {
|
||||
const alertInstance = services.alertInstanceFactory(`${alertUUID}-${group}`);
|
||||
const alertInstance = services.alertInstanceFactory(`${alertId}-${group}`);
|
||||
|
||||
// AND logic; all criteria must be across the threshold
|
||||
const shouldAlertFire = alertResults.every(result => result[group].shouldFire);
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import uuid from 'uuid';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { PluginSetupContract } from '../../../../../alerting/server';
|
||||
import { curry } from 'lodash';
|
||||
import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer';
|
||||
import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor';
|
||||
import { InfraBackendLibs } from '../../infra_types';
|
||||
import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types';
|
||||
import { InfraBackendLibs } from '../../infra_types';
|
||||
|
||||
const oneOfLiterals = (arrayOfLiterals: Readonly<string[]>) =>
|
||||
schema.string({
|
||||
|
@ -18,17 +18,7 @@ const oneOfLiterals = (arrayOfLiterals: Readonly<string[]>) =>
|
|||
arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`,
|
||||
});
|
||||
|
||||
export async function registerMetricThresholdAlertType(
|
||||
alertingPlugin: PluginSetupContract,
|
||||
libs: InfraBackendLibs
|
||||
) {
|
||||
if (!alertingPlugin) {
|
||||
throw new Error(
|
||||
'Cannot register metric threshold alert type. Both the actions and alerting plugins need to be enabled.'
|
||||
);
|
||||
}
|
||||
const alertUUID = uuid.v4();
|
||||
|
||||
export function registerMetricThresholdAlertType(libs: InfraBackendLibs) {
|
||||
const baseCriterion = {
|
||||
threshold: schema.arrayOf(schema.number()),
|
||||
comparator: oneOfLiterals(Object.values(Comparator)),
|
||||
|
@ -70,21 +60,24 @@ export async function registerMetricThresholdAlertType(
|
|||
}
|
||||
);
|
||||
|
||||
alertingPlugin.registerType({
|
||||
return {
|
||||
id: METRIC_THRESHOLD_ALERT_TYPE_ID,
|
||||
name: 'Metric threshold',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])),
|
||||
groupBy: schema.maybe(schema.string()),
|
||||
filterQuery: schema.maybe(schema.string()),
|
||||
sourceId: schema.string(),
|
||||
alertOnNoData: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
params: schema.object(
|
||||
{
|
||||
criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])),
|
||||
groupBy: schema.maybe(schema.string()),
|
||||
filterQuery: schema.maybe(schema.string()),
|
||||
sourceId: schema.string(),
|
||||
alertOnNoData: schema.maybe(schema.boolean()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
},
|
||||
defaultActionGroupId: FIRED_ACTIONS.id,
|
||||
actionGroups: [FIRED_ACTIONS],
|
||||
executor: createMetricThresholdExecutor(alertUUID),
|
||||
executor: curry(createMetricThresholdExecutor)(libs, uuid.v4()),
|
||||
actionVariables: {
|
||||
context: [
|
||||
{ name: 'group', description: groupActionVariableDescription },
|
||||
|
@ -92,5 +85,5 @@ export async function registerMetricThresholdAlertType(
|
|||
{ name: 'reason', description: reasonActionVariableDescription },
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,13 +6,16 @@
|
|||
|
||||
import { PluginSetupContract } from '../../../../alerting/server';
|
||||
import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type';
|
||||
import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type';
|
||||
import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type';
|
||||
import { InfraBackendLibs } from '../infra_types';
|
||||
|
||||
const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => {
|
||||
if (alertingPlugin) {
|
||||
const registerFns = [registerMetricThresholdAlertType, registerLogThresholdAlertType];
|
||||
alertingPlugin.registerType(registerMetricThresholdAlertType(libs));
|
||||
alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs));
|
||||
|
||||
const registerFns = [registerLogThresholdAlertType];
|
||||
registerFns.forEach(fn => {
|
||||
fn(alertingPlugin, libs);
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ export function compose(core: CoreSetup, config: InfraConfig, plugins: InfraServ
|
|||
const sourceStatus = new InfraSourceStatus(new InfraElasticsearchSourceStatusAdapter(framework), {
|
||||
sources,
|
||||
});
|
||||
const snapshot = new InfraSnapshot({ sources, framework });
|
||||
const snapshot = new InfraSnapshot();
|
||||
const logEntryCategoriesAnalysis = new LogEntryCategoriesAnalysis({ framework });
|
||||
const logEntryRateAnalysis = new LogEntryRateAnalysis({ framework });
|
||||
|
||||
|
|
|
@ -5,26 +5,23 @@
|
|||
*/
|
||||
|
||||
import { uniq } from 'lodash';
|
||||
import { RequestHandlerContext } from 'kibana/server';
|
||||
import { InfraSnapshotRequestOptions } from './types';
|
||||
import { getMetricsAggregations } from './query_helpers';
|
||||
import { calculateMetricInterval } from '../../utils/calculate_metric_interval';
|
||||
import { SnapshotModel, SnapshotModelMetricAggRT } from '../../../common/inventory_models/types';
|
||||
import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter';
|
||||
import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field';
|
||||
import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api';
|
||||
import { ESSearchClient } from '.';
|
||||
|
||||
export const createTimeRangeWithInterval = async (
|
||||
framework: KibanaFramework,
|
||||
requestContext: RequestHandlerContext,
|
||||
client: ESSearchClient,
|
||||
options: InfraSnapshotRequestOptions
|
||||
): Promise<InfraTimerangeInput> => {
|
||||
const aggregations = getMetricsAggregations(options);
|
||||
const modules = await aggregationsToModules(framework, requestContext, aggregations, options);
|
||||
const modules = await aggregationsToModules(client, aggregations, options);
|
||||
const interval = Math.max(
|
||||
(await calculateMetricInterval(
|
||||
framework,
|
||||
requestContext,
|
||||
client,
|
||||
{
|
||||
indexPattern: options.sourceConfiguration.metricAlias,
|
||||
timestampField: options.sourceConfiguration.fields.timestamp,
|
||||
|
@ -43,8 +40,7 @@ export const createTimeRangeWithInterval = async (
|
|||
};
|
||||
|
||||
const aggregationsToModules = async (
|
||||
framework: KibanaFramework,
|
||||
requestContext: RequestHandlerContext,
|
||||
client: ESSearchClient,
|
||||
aggregations: SnapshotModel,
|
||||
options: InfraSnapshotRequestOptions
|
||||
): Promise<string[]> => {
|
||||
|
@ -59,12 +55,7 @@ const aggregationsToModules = async (
|
|||
const fields = await Promise.all(
|
||||
uniqueFields.map(
|
||||
async field =>
|
||||
await getDatasetForField(
|
||||
framework,
|
||||
requestContext,
|
||||
field as string,
|
||||
options.sourceConfiguration.metricAlias
|
||||
)
|
||||
await getDatasetForField(client, field as string, options.sourceConfiguration.metricAlias)
|
||||
)
|
||||
);
|
||||
return fields.filter(f => f) as string[];
|
||||
|
|
|
@ -3,11 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { RequestHandlerContext } from 'src/core/server';
|
||||
import { InfraDatabaseSearchResponse } from '../adapters/framework';
|
||||
import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter';
|
||||
import { InfraSources } from '../sources';
|
||||
import { InfraDatabaseSearchResponse, CallWithRequestParams } from '../adapters/framework';
|
||||
|
||||
import { JsonObject } from '../../../common/typed_json';
|
||||
import { SNAPSHOT_COMPOSITE_REQUEST_SIZE } from './constants';
|
||||
|
@ -31,36 +27,26 @@ import { InfraSnapshotRequestOptions } from './types';
|
|||
import { createTimeRangeWithInterval } from './create_timerange_with_interval';
|
||||
import { SnapshotNode } from '../../../common/http_api/snapshot_api';
|
||||
|
||||
export type ESSearchClient = <Hit = {}, Aggregation = undefined>(
|
||||
options: CallWithRequestParams
|
||||
) => Promise<InfraDatabaseSearchResponse<Hit, Aggregation>>;
|
||||
export class InfraSnapshot {
|
||||
constructor(private readonly libs: { sources: InfraSources; framework: KibanaFramework }) {}
|
||||
|
||||
public async getNodes(
|
||||
requestContext: RequestHandlerContext,
|
||||
client: ESSearchClient,
|
||||
options: InfraSnapshotRequestOptions
|
||||
): Promise<{ nodes: SnapshotNode[]; interval: string }> {
|
||||
// Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch
|
||||
// in order to page through the results of their respective composite aggregations.
|
||||
// Both chains of requests are supposed to run in parallel, and their results be merged
|
||||
// when they have both been completed.
|
||||
const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(
|
||||
this.libs.framework,
|
||||
requestContext,
|
||||
options
|
||||
);
|
||||
const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, options);
|
||||
const optionsWithTimerange = { ...options, timerange: timeRangeWithIntervalApplied };
|
||||
const groupedNodesPromise = requestGroupedNodes(
|
||||
requestContext,
|
||||
optionsWithTimerange,
|
||||
this.libs.framework
|
||||
);
|
||||
const nodeMetricsPromise = requestNodeMetrics(
|
||||
requestContext,
|
||||
optionsWithTimerange,
|
||||
this.libs.framework
|
||||
);
|
||||
const groupedNodesPromise = requestGroupedNodes(client, optionsWithTimerange);
|
||||
const nodeMetricsPromise = requestNodeMetrics(client, optionsWithTimerange);
|
||||
|
||||
const groupedNodeBuckets = await groupedNodesPromise;
|
||||
const nodeMetricBuckets = await nodeMetricsPromise;
|
||||
|
||||
return {
|
||||
nodes: mergeNodeBuckets(groupedNodeBuckets, nodeMetricBuckets, options),
|
||||
interval: timeRangeWithIntervalApplied.interval,
|
||||
|
@ -77,15 +63,12 @@ const handleAfterKey = createAfterKeyHandler(
|
|||
input => input?.aggregations?.nodes?.after_key
|
||||
);
|
||||
|
||||
const callClusterFactory = (framework: KibanaFramework, requestContext: RequestHandlerContext) => (
|
||||
opts: any
|
||||
) =>
|
||||
framework.callWithRequest<{}, InfraSnapshotAggregationResponse>(requestContext, 'search', opts);
|
||||
const callClusterFactory = (search: ESSearchClient) => (opts: any) =>
|
||||
search<{}, InfraSnapshotAggregationResponse>(opts);
|
||||
|
||||
const requestGroupedNodes = async (
|
||||
requestContext: RequestHandlerContext,
|
||||
options: InfraSnapshotRequestOptions,
|
||||
framework: KibanaFramework
|
||||
client: ESSearchClient,
|
||||
options: InfraSnapshotRequestOptions
|
||||
): Promise<InfraSnapshotNodeGroupByBucket[]> => {
|
||||
const inventoryModel = findInventoryModel(options.nodeType);
|
||||
const query = {
|
||||
|
@ -124,13 +107,12 @@ const requestGroupedNodes = async (
|
|||
return await getAllCompositeData<
|
||||
InfraSnapshotAggregationResponse,
|
||||
InfraSnapshotNodeGroupByBucket
|
||||
>(callClusterFactory(framework, requestContext), query, bucketSelector, handleAfterKey);
|
||||
>(callClusterFactory(client), query, bucketSelector, handleAfterKey);
|
||||
};
|
||||
|
||||
const requestNodeMetrics = async (
|
||||
requestContext: RequestHandlerContext,
|
||||
options: InfraSnapshotRequestOptions,
|
||||
framework: KibanaFramework
|
||||
client: ESSearchClient,
|
||||
options: InfraSnapshotRequestOptions
|
||||
): Promise<InfraSnapshotNodeMetricsBucket[]> => {
|
||||
const index =
|
||||
options.metric.type === 'logRate'
|
||||
|
@ -175,7 +157,7 @@ const requestNodeMetrics = async (
|
|||
return await getAllCompositeData<
|
||||
InfraSnapshotAggregationResponse,
|
||||
InfraSnapshotNodeMetricsBucket
|
||||
>(callClusterFactory(framework, requestContext), query, bucketSelector, handleAfterKey);
|
||||
>(callClusterFactory(client), query, bucketSelector, handleAfterKey);
|
||||
};
|
||||
|
||||
// buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[]
|
||||
|
|
|
@ -9,7 +9,7 @@ import { failure } from 'io-ts/lib/PathReporter';
|
|||
import { identity, constant } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { map, fold } from 'fp-ts/lib/Either';
|
||||
import { RequestHandlerContext, SavedObjectsClientContract } from 'src/core/server';
|
||||
import { SavedObjectsClientContract } from 'src/core/server';
|
||||
import { defaultSourceConfiguration } from './defaults';
|
||||
import { NotFoundError } from './errors';
|
||||
import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings';
|
||||
|
@ -41,7 +41,6 @@ export class InfraSources {
|
|||
sourceId: string
|
||||
): Promise<InfraSource> {
|
||||
const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration();
|
||||
|
||||
const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId)
|
||||
.then(internalSourceConfiguration => ({
|
||||
id: sourceId,
|
||||
|
@ -79,10 +78,12 @@ export class InfraSources {
|
|||
return savedSourceConfiguration;
|
||||
}
|
||||
|
||||
public async getAllSourceConfigurations(requestContext: RequestHandlerContext) {
|
||||
public async getAllSourceConfigurations(savedObjectsClient: SavedObjectsClientContract) {
|
||||
const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration();
|
||||
|
||||
const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(requestContext);
|
||||
const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(
|
||||
savedObjectsClient
|
||||
);
|
||||
|
||||
return savedSourceConfigurations.map(savedSourceConfiguration => ({
|
||||
...savedSourceConfiguration,
|
||||
|
@ -94,7 +95,7 @@ export class InfraSources {
|
|||
}
|
||||
|
||||
public async createSourceConfiguration(
|
||||
requestContext: RequestHandlerContext,
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
sourceId: string,
|
||||
source: InfraSavedSourceConfiguration
|
||||
) {
|
||||
|
@ -106,7 +107,7 @@ export class InfraSources {
|
|||
);
|
||||
|
||||
const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration(
|
||||
await requestContext.core.savedObjects.client.create(
|
||||
await savedObjectsClient.create(
|
||||
infraSourceConfigurationSavedObjectType,
|
||||
pickSavedSourceConfiguration(newSourceConfiguration) as any,
|
||||
{ id: sourceId }
|
||||
|
@ -122,22 +123,22 @@ export class InfraSources {
|
|||
};
|
||||
}
|
||||
|
||||
public async deleteSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) {
|
||||
await requestContext.core.savedObjects.client.delete(
|
||||
infraSourceConfigurationSavedObjectType,
|
||||
sourceId
|
||||
);
|
||||
public async deleteSourceConfiguration(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
sourceId: string
|
||||
) {
|
||||
await savedObjectsClient.delete(infraSourceConfigurationSavedObjectType, sourceId);
|
||||
}
|
||||
|
||||
public async updateSourceConfiguration(
|
||||
requestContext: RequestHandlerContext,
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
sourceId: string,
|
||||
sourceProperties: InfraSavedSourceConfiguration
|
||||
) {
|
||||
const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration();
|
||||
|
||||
const { configuration, version } = await this.getSourceConfiguration(
|
||||
requestContext.core.savedObjects.client,
|
||||
savedObjectsClient,
|
||||
sourceId
|
||||
);
|
||||
|
||||
|
@ -147,7 +148,7 @@ export class InfraSources {
|
|||
);
|
||||
|
||||
const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration(
|
||||
await requestContext.core.savedObjects.client.update(
|
||||
await savedObjectsClient.update(
|
||||
infraSourceConfigurationSavedObjectType,
|
||||
sourceId,
|
||||
pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any,
|
||||
|
@ -213,8 +214,8 @@ export class InfraSources {
|
|||
return convertSavedObjectToSavedSourceConfiguration(savedObject);
|
||||
}
|
||||
|
||||
private async getAllSavedSourceConfigurations(requestContext: RequestHandlerContext) {
|
||||
const savedObjects = await requestContext.core.savedObjects.client.find({
|
||||
private async getAllSavedSourceConfigurations(savedObjectsClient: SavedObjectsClientContract) {
|
||||
const savedObjects = await savedObjectsClient.find({
|
||||
type: infraSourceConfigurationSavedObjectType,
|
||||
});
|
||||
|
||||
|
|
|
@ -109,7 +109,7 @@ export class InfraServerPlugin {
|
|||
sources,
|
||||
}
|
||||
);
|
||||
const snapshot = new InfraSnapshot({ sources, framework });
|
||||
const snapshot = new InfraSnapshot();
|
||||
const logEntryCategoriesAnalysis = new LogEntryCategoriesAnalysis({ framework });
|
||||
const logEntryRateAnalysis = new LogEntryRateAnalysis({ framework });
|
||||
|
||||
|
|
|
@ -82,12 +82,12 @@ export const initLogSourceConfigurationRoutes = ({ framework, sources }: InfraBa
|
|||
const sourceConfigurationExists = sourceConfiguration.origin === 'stored';
|
||||
const patchedSourceConfiguration = await (sourceConfigurationExists
|
||||
? sources.updateSourceConfiguration(
|
||||
requestContext,
|
||||
requestContext.core.savedObjects.client,
|
||||
sourceId,
|
||||
patchedSourceConfigurationProperties
|
||||
)
|
||||
: sources.createSourceConfiguration(
|
||||
requestContext,
|
||||
requestContext.core.savedObjects.client,
|
||||
sourceId,
|
||||
patchedSourceConfigurationProperties
|
||||
));
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { RequestHandlerContext } from 'kibana/server';
|
||||
import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter';
|
||||
import { ESSearchClient } from '../../../lib/snapshot';
|
||||
|
||||
interface EventDatasetHit {
|
||||
_source: {
|
||||
|
@ -16,8 +15,7 @@ interface EventDatasetHit {
|
|||
}
|
||||
|
||||
export const getDatasetForField = async (
|
||||
framework: KibanaFramework,
|
||||
requestContext: RequestHandlerContext,
|
||||
client: ESSearchClient,
|
||||
field: string,
|
||||
indexPattern: string
|
||||
) => {
|
||||
|
@ -33,11 +31,8 @@ export const getDatasetForField = async (
|
|||
},
|
||||
};
|
||||
|
||||
const response = await framework.callWithRequest<EventDatasetHit>(
|
||||
requestContext,
|
||||
'search',
|
||||
params
|
||||
);
|
||||
const response = await client<EventDatasetHit>(params);
|
||||
|
||||
if (response.hits.total.value === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,10 @@ import { createMetricModel } from './create_metrics_model';
|
|||
import { JsonObject } from '../../../../common/typed_json';
|
||||
import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';
|
||||
import { getDatasetForField } from './get_dataset_for_field';
|
||||
import {
|
||||
CallWithRequestParams,
|
||||
InfraDatabaseSearchResponse,
|
||||
} from '../../../lib/adapters/framework';
|
||||
|
||||
export const populateSeriesWithTSVBData = (
|
||||
request: KibanaRequest,
|
||||
|
@ -52,17 +56,21 @@ export const populateSeriesWithTSVBData = (
|
|||
}
|
||||
const timerange = { min: options.timerange.from, max: options.timerange.to };
|
||||
|
||||
const client = <Hit = {}, Aggregation = undefined>(
|
||||
opts: CallWithRequestParams
|
||||
): Promise<InfraDatabaseSearchResponse<Hit, Aggregation>> =>
|
||||
framework.callWithRequest(requestContext, 'search', opts);
|
||||
|
||||
// Create the TSVB model based on the request options
|
||||
const model = createMetricModel(options);
|
||||
const modules = await Promise.all(
|
||||
uniq(options.metrics.filter(m => m.field)).map(
|
||||
async m =>
|
||||
await getDatasetForField(framework, requestContext, m.field as string, options.indexPattern)
|
||||
async m => await getDatasetForField(client, m.field as string, options.indexPattern)
|
||||
)
|
||||
);
|
||||
|
||||
const calculatedInterval = await calculateMetricInterval(
|
||||
framework,
|
||||
requestContext,
|
||||
client,
|
||||
{
|
||||
indexPattern: options.indexPattern,
|
||||
timestampField: options.timerange.field,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { UsageCollector } from '../../usage/usage_collector';
|
|||
import { parseFilterQuery } from '../../utils/serialized_query';
|
||||
import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api';
|
||||
import { throwErrors } from '../../../common/runtime_types';
|
||||
import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../../lib/adapters/framework';
|
||||
|
||||
const escapeHatch = schema.object({}, { unknowns: 'allow' });
|
||||
|
||||
|
@ -57,7 +58,13 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
|
|||
metric,
|
||||
timerange,
|
||||
};
|
||||
const nodesWithInterval = await libs.snapshot.getNodes(requestContext, options);
|
||||
|
||||
const searchES = <Hit = {}, Aggregation = undefined>(
|
||||
opts: CallWithRequestParams
|
||||
): Promise<InfraDatabaseSearchResponse<Hit, Aggregation>> =>
|
||||
framework.callWithRequest(requestContext, 'search', opts);
|
||||
|
||||
const nodesWithInterval = await libs.snapshot.getNodes(searchES, options);
|
||||
return response.ok({
|
||||
body: SnapshotNodeResponseRT.encode(nodesWithInterval),
|
||||
});
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { RequestHandlerContext } from 'src/core/server';
|
||||
// import { RequestHandlerContext } from 'src/core/server';
|
||||
import { findInventoryModel } from '../../common/inventory_models';
|
||||
import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter';
|
||||
// import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter';
|
||||
import { InventoryItemType } from '../../common/inventory_models/types';
|
||||
import { ESSearchClient } from '../lib/snapshot';
|
||||
|
||||
interface Options {
|
||||
indexPattern: string;
|
||||
|
@ -23,8 +24,7 @@ interface Options {
|
|||
* This is useful for visualizing metric modules like s3 that only send metrics once per day.
|
||||
*/
|
||||
export const calculateMetricInterval = async (
|
||||
framework: KibanaFramework,
|
||||
requestContext: RequestHandlerContext,
|
||||
client: ESSearchClient,
|
||||
options: Options,
|
||||
modules?: string[],
|
||||
nodeType?: InventoryItemType // TODO: check that this type still makes sense
|
||||
|
@ -73,11 +73,7 @@ export const calculateMetricInterval = async (
|
|||
},
|
||||
};
|
||||
|
||||
const resp = await framework.callWithRequest<{}, PeriodAggregationData>(
|
||||
requestContext,
|
||||
'search',
|
||||
query
|
||||
);
|
||||
const resp = await client<{}, PeriodAggregationData>(query);
|
||||
|
||||
// if ES doesn't return an aggregations key, something went seriously wrong.
|
||||
if (!resp.aggregations) {
|
||||
|
|
|
@ -32,7 +32,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
describe('querying the entire infrastructure', () => {
|
||||
for (const aggType of aggs) {
|
||||
it(`should work with the ${aggType} aggregator`, async () => {
|
||||
const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType));
|
||||
const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType), '@timestamp');
|
||||
const result = await client.search({
|
||||
index,
|
||||
body: searchBody,
|
||||
|
@ -45,6 +45,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
it('should work with a filterQuery', async () => {
|
||||
const searchBody = getElasticsearchMetricQuery(
|
||||
getSearchParams('avg'),
|
||||
'@timestamp',
|
||||
undefined,
|
||||
'{"bool":{"should":[{"match_phrase":{"agent.hostname":"foo"}}],"minimum_should_match":1}}'
|
||||
);
|
||||
|
@ -59,6 +60,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
it('should work with a filterQuery in KQL format', async () => {
|
||||
const searchBody = getElasticsearchMetricQuery(
|
||||
getSearchParams('avg'),
|
||||
'@timestamp',
|
||||
undefined,
|
||||
'"agent.hostname":"foo"'
|
||||
);
|
||||
|
@ -74,7 +76,11 @@ export default function({ getService }: FtrProviderContext) {
|
|||
describe('querying with a groupBy parameter', () => {
|
||||
for (const aggType of aggs) {
|
||||
it(`should work with the ${aggType} aggregator`, async () => {
|
||||
const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType), 'agent.id');
|
||||
const searchBody = getElasticsearchMetricQuery(
|
||||
getSearchParams(aggType),
|
||||
'@timestamp',
|
||||
'agent.id'
|
||||
);
|
||||
const result = await client.search({
|
||||
index,
|
||||
body: searchBody,
|
||||
|
@ -87,6 +93,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
it('should work with a filterQuery', async () => {
|
||||
const searchBody = getElasticsearchMetricQuery(
|
||||
getSearchParams('avg'),
|
||||
'@timestamp',
|
||||
'agent.id',
|
||||
'{"bool":{"should":[{"match_phrase":{"agent.hostname":"foo"}}],"minimum_should_match":1}}'
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue