Skip to content

Commit

Permalink
[RAC] Link inventory alerts to the right inventory view (#113553)
Browse files Browse the repository at this point in the history
Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
Alejandro Fernández Gómez and kibanamachine authored Oct 18, 2021
1 parent d08f091 commit 27c7c6f
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 25 deletions.
25 changes: 25 additions & 0 deletions x-pack/plugins/infra/common/alerting/metrics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,3 +57,25 @@ export interface MetricAnomalyParams {
threshold: Exclude<ANOMALY_THRESHOLD, ANOMALY_THRESHOLD.LOW>;
influencerFilter: rt.TypeOf<typeof metricAnomalyInfluencerFilterRT> | 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;
nodeType: InventoryItemType;
sourceId?: string;
alertOnNoData?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,48 @@
* 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 { 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 link = '/app/metrics/inventory'; // TODO https://github.com/elastic/kibana/issues/106497
const ruleParams = parseRuleParams(fields[ALERT_RULE_PARAMS]);

let link = '/app/metrics/link-to/inventory?';

if (ruleParams) {
const linkToParams: Record<string, any> = {
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 });
}

link += stringify(linkToParams);
}

return {
reason,
link,
};
};

function parseRuleParams(params?: string): InventoryMetricThresholdParams | undefined {
try {
return typeof params === 'string' ? JSON.parse(params) : undefined;
} catch (_) {
return;
}
}
2 changes: 2 additions & 0 deletions x-pack/plugins/infra/public/pages/link_to/link_to_metrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -29,6 +30,7 @@ export const LinkToMetricsPage: React.FC<LinkToPageProps> = (props) => {
path={`${props.match.url}/host-detail-via-ip/:hostIp`}
component={RedirectToHostDetailViaIP}
/>
<Route path={`${props.match.url}/inventory`} component={RedirectToInventory} />
<Redirect to="/" />
</Switch>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RouteComponentProps> = ({ location }) => {
const parsedQueryString = parseQueryString(location.search);

const inventoryQueryString = QUERY_STRING_TEMPLATE.replace(
/{(\w+)}/g,
(_, key) => parsedQueryString[key] || ''
);

return <Redirect to={'/inventory' + inventoryQueryString} />;
};

function parseQueryString(search: string): Record<string, string> {
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<string, string>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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<string, any>;
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
Expand All @@ -64,14 +56,13 @@ type InventoryMetricThresholdAlertInstanceFactory = (

export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =>
libs.metricsRules.createLifecycleRuleExecutor<
InventoryMetricThresholdAlertTypeParams,
InventoryMetricThresholdParams & Record<string, unknown>,
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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Comparator>,
timeUnit: schema.string() as Type<Unit>,
timeSize: schema.number(),
metric: schema.string(),
metric: oneOfLiterals(Object.keys(SnapshotMetricTypeKeys)) as Type<SnapshotMetricType>,
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<SnapshotCustomAggregation>,
label: schema.maybe(schema.string()),
})
),
Expand All @@ -59,7 +71,7 @@ export async function registerMetricInventoryThresholdAlertType(
params: schema.object(
{
criteria: schema.arrayOf(condition),
nodeType: schema.string(),
nodeType: schema.string() as Type<InventoryItemType>,
filterQuery: schema.maybe(
schema.string({ validate: validateIsStringElasticsearchJSONFilter })
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down

0 comments on commit 27c7c6f

Please sign in to comment.