From f79283258f829f63c48433c147aae5cc25c7260d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Thu, 30 Sep 2021 17:58:56 +0200 Subject: [PATCH 1/4] Link inventory alerts to the right inventory view --- .../inventory/rule_data_formatters.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts b/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts index 30e0de9402191..c35382fe4c65f 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts @@ -5,12 +5,36 @@ * 2.0. */ -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_RULE_PARAMS, TIMESTAMP } from '@kbn/rule-data-utils'; +import { encode } from 'rison-node'; import { ObservabilityRuleTypeFormatter } from '../../../../observability/public'; +const LINK_PARAMS_TEMPLATE = + '?waffleFilter=(expression:%27%27,kind:kuery)&waffleTime=(currentTime:{timestamp},isAutoReloading:!f)&waffleOptions=(accountId:%27%27,autoBounds:!t,boundsOverride:(max:1,min:0),customMetrics:!({customMetric}),customOptions:!(),groupBy:!(),legend:(palette:cool,reverseColors:!f,steps:10),metric:{metric},nodeType:{nodeType},region:%27%27,sort:(by:name,direction:desc),timelineOpen:!f,view:map)'; + export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { const reason = fields[ALERT_REASON] ?? '-'; - const link = '/app/metrics/inventory'; // TODO https://github.com/elastic/kibana/issues/106497 + const ruleParams = + typeof fields[ALERT_RULE_PARAMS] === 'string' ? JSON.parse(fields[ALERT_RULE_PARAMS]!) : {}; + + const urlParams: Record = { + nodeType: ruleParams.nodeType, + timestamp: Date.parse(fields[TIMESTAMP]), + customMetric: '', + }; + + // We always pick the first criteria for the URL + const criteria = ruleParams.criteria[0]; + if (criteria.customMetric.id !== 'alert-custom-metric') { + const customMetric = encode(criteria.customMetric); + urlParams.customMetric = customMetric; + urlParams.metric = customMetric; + } else { + urlParams.metric = encode({ type: criteria.metric }); + } + + const link = + '/app/metrics/inventory' + LINK_PARAMS_TEMPLATE.replace(/{(\w+)}/g, (_, key) => urlParams[key]); return { reason, From 556ca4eca597988976b21c8b0d26143194bd0210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Wed, 6 Oct 2021 17:23:30 +0200 Subject: [PATCH 2/4] Add a `link-to` route to inventory --- .../inventory/rule_data_formatters.ts | 17 +++---- .../public/pages/link_to/link_to_metrics.tsx | 2 + .../pages/link_to/redirect_to_inventory.tsx | 47 +++++++++++++++++++ 3 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/link_to/redirect_to_inventory.tsx diff --git a/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts b/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts index c35382fe4c65f..a83dd25f9ee28 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts @@ -7,34 +7,31 @@ import { ALERT_REASON, ALERT_RULE_PARAMS, TIMESTAMP } from '@kbn/rule-data-utils'; import { encode } from 'rison-node'; +import { stringify } from 'query-string'; import { ObservabilityRuleTypeFormatter } from '../../../../observability/public'; -const LINK_PARAMS_TEMPLATE = - '?waffleFilter=(expression:%27%27,kind:kuery)&waffleTime=(currentTime:{timestamp},isAutoReloading:!f)&waffleOptions=(accountId:%27%27,autoBounds:!t,boundsOverride:(max:1,min:0),customMetrics:!({customMetric}),customOptions:!(),groupBy:!(),legend:(palette:cool,reverseColors:!f,steps:10),metric:{metric},nodeType:{nodeType},region:%27%27,sort:(by:name,direction:desc),timelineOpen:!f,view:map)'; - export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { const reason = fields[ALERT_REASON] ?? '-'; const ruleParams = typeof fields[ALERT_RULE_PARAMS] === 'string' ? JSON.parse(fields[ALERT_RULE_PARAMS]!) : {}; - const urlParams: Record = { + const linkToParams: Record = { nodeType: ruleParams.nodeType, timestamp: Date.parse(fields[TIMESTAMP]), customMetric: '', }; - // We always pick the first criteria for the URL + // We always pick the first criteria metric for the URL const criteria = ruleParams.criteria[0]; if (criteria.customMetric.id !== 'alert-custom-metric') { const customMetric = encode(criteria.customMetric); - urlParams.customMetric = customMetric; - urlParams.metric = customMetric; + linkToParams.customMetric = customMetric; + linkToParams.metric = customMetric; } else { - urlParams.metric = encode({ type: criteria.metric }); + linkToParams.metric = encode({ type: criteria.metric }); } - const link = - '/app/metrics/inventory' + LINK_PARAMS_TEMPLATE.replace(/{(\w+)}/g, (_, key) => urlParams[key]); + const link = '/app/metrics/link-to/inventory?' + stringify(linkToParams); return { reason, diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to_metrics.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to_metrics.tsx index 8304e1cdb7262..05a099441b699 100644 --- a/x-pack/plugins/infra/public/pages/link_to/link_to_metrics.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/link_to_metrics.tsx @@ -10,6 +10,7 @@ import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom'; import { RedirectToNodeDetail } from './redirect_to_node_detail'; import { RedirectToHostDetailViaIP } from './redirect_to_host_detail_via_ip'; +import { RedirectToInventory } from './redirect_to_inventory'; import { inventoryModels } from '../../../common/inventory_models'; interface LinkToPageProps { @@ -29,6 +30,7 @@ export const LinkToMetricsPage: React.FC = (props) => { path={`${props.match.url}/host-detail-via-ip/:hostIp`} component={RedirectToHostDetailViaIP} /> + ); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_inventory.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_inventory.tsx new file mode 100644 index 0000000000000..37ddbacf72488 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_inventory.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { parse } from 'query-string'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; + +// FIXME what would be the right way to build this query string? +const QUERY_STRING_TEMPLATE = + "?waffleFilter=(expression:'',kind:kuery)&waffleTime=(currentTime:{timestamp},isAutoReloading:!f)&waffleOptions=(accountId:'',autoBounds:!t,boundsOverride:(max:1,min:0),customMetrics:!({customMetric}),customOptions:!(),groupBy:!(),legend:(palette:cool,reverseColors:!f,steps:10),metric:{metric},nodeType:{nodeType},region:'',sort:(by:name,direction:desc),timelineOpen:!f,view:map)"; + +export const RedirectToInventory: React.FC = ({ location }) => { + const parsedQueryString = parseQueryString(location.search); + + const inventoryQueryString = QUERY_STRING_TEMPLATE.replace( + /{(\w+)}/g, + (_, key) => parsedQueryString[key] || '' + ); + + return ; +}; + +function parseQueryString(search: string): Record { + if (search.length === 0) { + return {}; + } + + const obj = parse(search.substring(1)); + + // Force all values into string. If they are empty don't create the keys + for (const key in obj) { + if (Object.hasOwnProperty.call(obj, key)) { + if (!obj[key]) { + delete obj[key]; + } + if (Array.isArray(obj.key)) { + obj[key] = obj[key]![0]; + } + } + } + + return obj as Record; +} From c1d0cf3cd76306a04ef97a485cc3d2559710c8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Thu, 7 Oct 2021 14:27:54 +0200 Subject: [PATCH 3/4] Tweak InventoryMetricThresholdParams and use it when parsing the params --- .../infra/common/alerting/metrics/types.ts | 25 ++++++++++ .../inventory/rule_data_formatters.ts | 46 ++++++++++++------- .../inventory_metric_threshold_executor.ts | 19 ++------ 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 2e8ad1de3413c..1ac9b05c53e3e 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -5,7 +5,10 @@ * 2.0. */ import * as rt from 'io-ts'; +import { Unit } from '@elastic/datemath'; import { ANOMALY_THRESHOLD } from '../../infra_ml'; +import { InventoryItemType, SnapshotMetricType } from '../../inventory_models/types'; +import { SnapshotCustomMetricInput } from '../../http_api'; // TODO: Have threshold and inventory alerts import these types from this file instead of from their // local directories @@ -54,3 +57,25 @@ export interface MetricAnomalyParams { threshold: Exclude; influencerFilter: rt.TypeOf | undefined; } + +// Types for the executor + +export interface InventoryMetricConditions { + metric: SnapshotMetricType; + timeSize: number; + timeUnit: Unit; + sourceId?: string; + threshold: number[]; + comparator: Comparator; + customMetric?: SnapshotCustomMetricInput; + warningThreshold?: number[]; + warningComparator?: Comparator; +} + +export interface InventoryMetricThresholdParams { + criteria: InventoryMetricConditions[]; + filterQuery: string | undefined; + nodeType: InventoryItemType; + sourceId?: string; + alertOnNoData?: boolean; +} diff --git a/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts b/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts index a83dd25f9ee28..ee27f1ff09925 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/rule_data_formatters.ts @@ -9,32 +9,44 @@ import { ALERT_REASON, ALERT_RULE_PARAMS, TIMESTAMP } from '@kbn/rule-data-utils import { encode } from 'rison-node'; import { stringify } from 'query-string'; import { ObservabilityRuleTypeFormatter } from '../../../../observability/public'; +import { InventoryMetricThresholdParams } from '../../../common/alerting/metrics'; export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { const reason = fields[ALERT_REASON] ?? '-'; - const ruleParams = - typeof fields[ALERT_RULE_PARAMS] === 'string' ? JSON.parse(fields[ALERT_RULE_PARAMS]!) : {}; + const ruleParams = parseRuleParams(fields[ALERT_RULE_PARAMS]); - const linkToParams: Record = { - nodeType: ruleParams.nodeType, - timestamp: Date.parse(fields[TIMESTAMP]), - customMetric: '', - }; + let link = '/app/metrics/link-to/inventory?'; - // We always pick the first criteria metric for the URL - const criteria = ruleParams.criteria[0]; - if (criteria.customMetric.id !== 'alert-custom-metric') { - const customMetric = encode(criteria.customMetric); - linkToParams.customMetric = customMetric; - linkToParams.metric = customMetric; - } else { - linkToParams.metric = encode({ type: criteria.metric }); - } + if (ruleParams) { + const linkToParams: Record = { + nodeType: ruleParams.nodeType, + timestamp: Date.parse(fields[TIMESTAMP]), + customMetric: '', + }; + + // We always pick the first criteria metric for the URL + const criteria = ruleParams.criteria[0]; + if (criteria.customMetric && criteria.customMetric.id !== 'alert-custom-metric') { + const customMetric = encode(criteria.customMetric); + linkToParams.customMetric = customMetric; + linkToParams.metric = customMetric; + } else { + linkToParams.metric = encode({ type: criteria.metric }); + } - const link = '/app/metrics/link-to/inventory?' + stringify(linkToParams); + link += stringify(linkToParams); + } return { reason, link, }; }; + +function parseRuleParams(params?: string): InventoryMetricThresholdParams | undefined { + try { + return typeof params === 'string' ? JSON.parse(params) : undefined; + } catch (_) { + return; + } +} diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 72d9ea9e39def..5cd093c6f1472 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -11,7 +11,7 @@ import { ALERT_REASON, ALERT_RULE_PARAMS } from '@kbn/rule-data-utils'; import moment from 'moment'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; -import { AlertStates, InventoryMetricConditions } from './types'; +import { AlertStates } from './types'; import { ActionGroupIdsOf, ActionGroup, @@ -20,10 +20,11 @@ import { RecoveredActionGroup, } from '../../../../../alerting/common'; import { AlertInstance, AlertTypeState } from '../../../../../alerting/server'; -import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraBackendLibs } from '../../infra_types'; import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric_formats'; import { createFormatter } from '../../../../common/formatters'; +import { InventoryMetricThresholdParams } from '../../../../common/alerting/metrics'; import { buildErrorAlertReason, buildFiredAlertReason, @@ -33,19 +34,10 @@ import { } from '../common/messages'; import { evaluateCondition } from './evaluate_condition'; -interface InventoryMetricThresholdParams { - criteria: InventoryMetricConditions[]; - filterQuery: string | undefined; - nodeType: InventoryItemType; - sourceId?: string; - alertOnNoData?: boolean; -} - type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf< typeof FIRED_ACTIONS | typeof WARNING_ACTIONS >; -export type InventoryMetricThresholdAlertTypeParams = Record; export type InventoryMetricThresholdAlertTypeState = AlertTypeState; // no specific state used export type InventoryMetricThresholdAlertInstanceState = AlertInstanceState; // no specific state used export type InventoryMetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instance context used @@ -64,14 +56,13 @@ type InventoryMetricThresholdAlertInstanceFactory = ( export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) => libs.metricsRules.createLifecycleRuleExecutor< - InventoryMetricThresholdAlertTypeParams, + InventoryMetricThresholdParams & Record, InventoryMetricThresholdAlertTypeState, InventoryMetricThresholdAlertInstanceState, InventoryMetricThresholdAlertInstanceContext, InventoryMetricThresholdAllowedActionGroups >(async ({ services, params }) => { - const { criteria, filterQuery, sourceId, nodeType, alertOnNoData } = - params as InventoryMetricThresholdParams; + const { criteria, filterQuery, sourceId, nodeType, alertOnNoData } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); const { alertWithLifecycle, savedObjectsClient } = services; const alertInstanceFactory: InventoryMetricThresholdAlertInstanceFactory = (id, reason) => From 269f813fbfcb5a2bdfdae10b7f93380abe254909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Mon, 11 Oct 2021 10:32:57 +0200 Subject: [PATCH 4/4] Reconciliate schema and io-ts types --- .../infra/common/alerting/metrics/types.ts | 2 +- ...r_inventory_metric_threshold_alert_type.ts | 26 ++++++++++++++----- .../inventory_metric_threshold/types.ts | 4 +-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 1ac9b05c53e3e..9e8935ddb9968 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -74,7 +74,7 @@ export interface InventoryMetricConditions { export interface InventoryMetricThresholdParams { criteria: InventoryMetricConditions[]; - filterQuery: string | undefined; + filterQuery?: string; nodeType: InventoryItemType; sourceId?: string; alertOnNoData?: boolean; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index 5d516f3591419..77c85967e64f6 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { schema, Type } from '@kbn/config-schema'; +import { Unit } from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; import { PluginSetupContract } from '../../../../../alerting/server'; import { @@ -26,21 +27,32 @@ import { metricActionVariableDescription, thresholdActionVariableDescription, } from '../common/messages'; +import { + SnapshotMetricTypeKeys, + SnapshotMetricType, + InventoryItemType, +} from '../../../../common/inventory_models/types'; +import { + SNAPSHOT_CUSTOM_AGGREGATIONS, + SnapshotCustomAggregation, +} from '../../../../common/http_api/snapshot_api'; const condition = schema.object({ threshold: schema.arrayOf(schema.number()), - comparator: oneOfLiterals(Object.values(Comparator)), - timeUnit: schema.string(), + comparator: oneOfLiterals(Object.values(Comparator)) as Type, + timeUnit: schema.string() as Type, timeSize: schema.number(), - metric: schema.string(), + metric: oneOfLiterals(Object.keys(SnapshotMetricTypeKeys)) as Type, warningThreshold: schema.maybe(schema.arrayOf(schema.number())), - warningComparator: schema.maybe(oneOfLiterals(Object.values(Comparator))), + warningComparator: schema.maybe(oneOfLiterals(Object.values(Comparator))) as Type< + Comparator | undefined + >, customMetric: schema.maybe( schema.object({ type: schema.literal('custom'), id: schema.string(), field: schema.string(), - aggregation: schema.string(), + aggregation: oneOfLiterals(SNAPSHOT_CUSTOM_AGGREGATIONS) as Type, label: schema.maybe(schema.string()), }) ), @@ -59,7 +71,7 @@ export async function registerMetricInventoryThresholdAlertType( params: schema.object( { criteria: schema.arrayOf(condition), - nodeType: schema.string(), + nodeType: schema.string() as Type, filterQuery: schema.maybe( schema.string({ validate: validateIsStringElasticsearchJSONFilter }) ), diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts index 120fa47c079ab..829f34d42ee03 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts @@ -8,9 +8,9 @@ import { Unit } from '@elastic/datemath'; import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; import { SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { Comparator, AlertStates } from '../common/types'; +import { Comparator, AlertStates, Aggregators } from '../common/types'; -export { Comparator, AlertStates }; +export { Comparator, AlertStates, Aggregators }; export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';