From d69e598e30977d7601147c2d40098388d98e1c2a Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 19 Aug 2024 11:31:16 +0200 Subject: [PATCH] [Infra] Handle view in app for legacy metrics (#190295) closes [#189625](https://github.com/elastic/kibana/issues/189625) ## Summary This PR changes the asset details to display a call if the user comes from the alerts page via an inventory rule created with one of the legacy metrics. Besides that, it changes how the link is built to use locators. Legacy metrics example https://github.com/user-attachments/assets/12308f4e-e269-4580-b86d-808ae9f6fe10 **Regression** Metrics Threshold https://github.com/user-attachments/assets/94032f51-6b2c-4760-8019-158746a1aa13 Inventory Rule (new/hosts view metrics) https://github.com/user-attachments/assets/0f872f3a-7bdb-4fb8-a925-7ed3621fee2d Inventory Rule (custom metric) https://github.com/user-attachments/assets/f2e5ded5-b2e6-45ff-878d-6361c4540140 ### Fix While working on it, I discovered that alerts for containers were not redirecting the users to the asset details page for containers. That was fixed too Inventory rule for containers https://github.com/user-attachments/assets/05f20c12-6fdc-45c0-bc38-b756bfbf3658 Metrics threshold rule for containers ### How to test - Start a local Kibana instance (easier if pointed to an oblt cluster) - Create Inventory Rule alerts for: - host: 1 legacy metric and 1 non-legacy metric - container - Create Metric Threshold alerts with - avg on `system.cpu.total.norm.pct` grouped by `host.name` - avg on `kubernetes.container.cpu.usage.limit.pct` grouped by `container.id` - Navigate to the alerts page and click on the `view in app` button, as shown in the recordings above - Test if the navigation to the asset details page works - For a legacy metric, the callout should be displayed - Once dismissed, the callout should not appear again for that metric --- .../alerting/metrics/alert_link.test.ts | 143 ++++++++++-- .../common/alerting/metrics/alert_link.ts | 206 ++++++++++++------ .../infra/common/asset_details/types.ts | 10 + .../infra/common/constants.ts | 1 - .../infra/public/alerting/inventory/index.ts | 19 +- .../inventory/rule_data_formatters.ts | 24 +- .../public/alerting/metric_threshold/index.ts | 12 +- .../metric_threshold/rule_data_formatters.ts | 31 ++- .../components/asset_details/constants.ts | 13 +- .../asset_details/content/callouts.tsx | 48 ++++ .../callouts/legacy_metric_callout.tsx | 82 +++++++ .../asset_details/content/content.tsx | 30 ++- .../hooks/use_asset_details_url_state.ts | 1 + .../asset_details/hooks/use_page_header.tsx | 4 +- .../asset_details/template/page.tsx | 26 +-- .../public/components/asset_details/types.ts | 7 +- .../redirect_to_host_detail_via_ip.tsx | 7 +- .../pages/link_to/redirect_to_node_detail.tsx | 9 +- .../infra/public/plugin.ts | 15 +- .../infra/server/lib/alerting/common/utils.ts | 31 ++- ...nventory_metric_threshold_executor.test.ts | 6 + .../inventory_metric_threshold_executor.ts | 32 ++- .../metric_threshold_executor.test.ts | 6 + .../metric_threshold_executor.ts | 36 ++- .../lib/helpers/get_apm_data_access_client.ts | 6 +- .../infra/server/lib/infra_types.ts | 16 +- .../infra/server/plugin.ts | 18 +- .../infra/server/routes/services/index.ts | 2 +- .../metrics_data_access/common/index.ts | 1 + .../common/inventory_models/host/index.ts | 1 + .../common/inventory_models/index.ts | 2 +- .../common/inventory_models/types.ts | 1 + .../formatters/snapshot_metric_formats.ts | 6 + .../locators/infra/asset_details_locator.ts | 23 +- .../locators/infra/inventory_locator.ts | 6 +- .../common/locators/infra/locators.test.ts | 4 +- .../observability_shared/public/plugin.ts | 1 + .../functional/apps/infra/node_details.ts | 96 ++++++-- .../functional/page_objects/asset_details.ts | 5 + 39 files changed, 770 insertions(+), 217 deletions(-) create mode 100644 x-pack/plugins/observability_solution/infra/common/asset_details/types.ts create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.tsx create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.tsx diff --git a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts index 441922d3fb7712..2513a3432742d8 100644 --- a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts +++ b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts @@ -7,13 +7,43 @@ import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils'; +import rison from '@kbn/rison'; import { getInventoryViewInAppUrl, flatAlertRuleParams, getMetricsViewInAppUrl, } from './alert_link'; +import { + InventoryLocator, + AssetDetailsLocator, + InventoryLocatorParams, + AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; + +jest.mock('@kbn/observability-shared-plugin/common'); + +const mockInventoryLocator = { + getRedirectUrl: jest + .fn() + .mockImplementation( + (params: InventoryLocatorParams) => + `/inventory-mock?receivedParams=${rison.encodeUnknown(params)}` + ), +} as unknown as jest.Mocked; + +const mockAssetDetailsLocator = { + getRedirectUrl: jest + .fn() + .mockImplementation( + ({ assetId, assetType, assetDetails }: AssetDetailsLocatorParams) => + `/node-mock/${assetType}/${assetId}?receivedParams=${rison.encodeUnknown(assetDetails)}` + ), +} as unknown as jest.Mocked; describe('Inventory Threshold Rule', () => { + afterEach(() => { + jest.clearAllMocks(); + }); describe('flatAlertRuleParams', () => { it('flat ALERT_RULE_PARAMETERS', () => { expect( @@ -85,9 +115,14 @@ describe('Inventory Threshold Rule', () => { [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`]: ['avg'], [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`]: ['system.cpu.user.pct'], } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&metric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&nodeType=h×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',metric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',nodeType:host,timestamp:1640995200000)" ); }); it('should work with non-custom metrics', () => { @@ -96,22 +131,50 @@ describe('Inventory Threshold Rule', () => { [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'host', [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'], } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=&metric=%28type%3Acpu%29&nodeType=h×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'',metric:'(type:cpu)',nodeType:host,timestamp:1640995200000)" ); }); - it('should point to host-details when host.name is present', () => { + it('should point to asset details when nodeType is host and host.name is present', () => { const fields = { [TIMESTAMP]: '2022-01-01T00:00:00.000Z', - [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'kubernetes', + [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'host', [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'], [`host.name`]: ['my-host'], } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/host-detail/my-host?from=1640995200000&to=1640996100000' + "/node-mock/host/my-host?receivedParams=(alertMetric:cpu,dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" + ); + }); + + it('should point to asset details when nodeType is container and container.id is present', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'container', + [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'], + [`container.id`]: ['my-container'], + } as unknown as ParsedTechnicalFields & Record; + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); + expect(url).toEqual( + "/node-mock/container/my-container?receivedParams=(alertMetric:cpu,dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" ); }); @@ -140,9 +203,14 @@ describe('Inventory Threshold Rule', () => { _id: 'eaa439aa-a4bb-4e7c-b7f8-fbe532ca7366', _index: '.internal.alerts-observability.metrics.alerts-default-000001', } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&metric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&nodeType=host×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',metric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',nodeType:host,timestamp:1640995200000)" ); }); @@ -165,32 +233,75 @@ describe('Inventory Threshold Rule', () => { _id: 'eaa439aa-a4bb-4e7c-b7f8-fbe532ca7366', _index: '.internal.alerts-observability.metrics.alerts-default-000001', } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=&metric=%28type%3Acpu%29&nodeType=host×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'',metric:'(type:cpu)',nodeType:host,timestamp:1640995200000)" ); }); }); }); describe('Metrics Rule', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + describe('getMetricsViewInAppUrl', () => { - it('should point to host-details when host.name is present', () => { + it('should point to host details when host.name is present', () => { const fields = { [TIMESTAMP]: '2022-01-01T00:00:00.000Z', [`host.name`]: ['my-host'], } as unknown as ParsedTechnicalFields & Record; - const url = getMetricsViewInAppUrl(fields); + const url = getMetricsViewInAppUrl({ + fields, + assetDetailsLocator: mockAssetDetailsLocator, + groupBy: ['host.name'], + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); + expect(url).toEqual( + "/node-mock/host/my-host?receivedParams=(dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" + ); + }); + + it('should point to container details when host.name is present', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`container.id`]: ['my-host-5xyz'], + } as unknown as ParsedTechnicalFields & Record; + const url = getMetricsViewInAppUrl({ + fields, + assetDetailsLocator: mockAssetDetailsLocator, + groupBy: ['container.id'], + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/host-detail/my-host?from=1640995200000&to=1640996100000' + "/node-mock/container/my-host-5xyz?receivedParams=(dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" ); }); + it('should point to metrics when group by field is not supported by the asset details', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`host.name`]: ['my-host'], + } as unknown as ParsedTechnicalFields & Record; + const url = getMetricsViewInAppUrl({ + fields, + assetDetailsLocator: mockAssetDetailsLocator, + groupBy: ['kubernetes.pod.name'], + }); + expect(url).toEqual('/app/metrics/explorer'); + }); + it('should point to metrics explorer', () => { const fields = { [TIMESTAMP]: '2022-01-01T00:00:00.000Z', } as unknown as ParsedTechnicalFields & Record; - const url = getMetricsViewInAppUrl(fields); + const url = getMetricsViewInAppUrl({ fields }); expect(url).toEqual('/app/metrics/explorer'); }); }); diff --git a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts index 06d34a83f123ac..3dc84f0406145f 100644 --- a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts +++ b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts @@ -6,16 +6,22 @@ */ import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils'; +import moment from 'moment'; import { encode } from '@kbn/rison'; -import { stringify } from 'query-string'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; -import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; +import { type InventoryItemType, findInventoryModel } from '@kbn/metrics-data-access-plugin/common'; +import type { LocatorPublic } from '@kbn/share-plugin/common'; import { - fifteenMinutesInMilliseconds, - HOST_NAME_FIELD, - LINK_TO_INVENTORY, - METRICS_EXPLORER_URL, -} from '../../constants'; + type AssetDetailsLocatorParams, + type InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; +import { castArray } from 'lodash'; +import { fifteenMinutesInMilliseconds, METRICS_EXPLORER_URL } from '../../constants'; +import { SupportedAssetTypes } from '../../asset_details/types'; + +const ALERT_RULE_PARAMTERS_INVENTORY_METRIC_ID = `${ALERT_RULE_PARAMETERS}.criteria.metric`; +export const ALERT_RULE_PARAMETERS_NODE_TYPE = `${ALERT_RULE_PARAMETERS}.nodeType`; +const CUSTOM_METRIC_TYPE = 'custom'; export const flatAlertRuleParams = (params: {}, pKey = ''): Record => { return Object.entries(params).reduce((acc, [key, field]) => { @@ -32,10 +38,18 @@ export const flatAlertRuleParams = (params: {}, pKey = ''): Record); }; -export const getInventoryViewInAppUrl = ( - fields: ParsedTechnicalFields & Record -): string => { - let inventoryFields = fields; +export const getInventoryViewInAppUrl = ({ + fields, + assetDetailsLocator, + inventoryLocator, +}: { + fields: ParsedTechnicalFields & Record; + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; +}): string => { + if (!assetDetailsLocator || !inventoryLocator) { + return ''; + } /* Temporary Solution -> https://github.com/elastic/kibana/issues/137033 * In the alert table from timelines plugin (old table), we are using an API who is flattening all the response @@ -45,75 +59,131 @@ export const getInventoryViewInAppUrl = ( * triggersActionUI then we will stop using this flattening way and we will update the code to work with fields API, * it will be less magic. */ - if (fields[ALERT_RULE_PARAMETERS]) { - inventoryFields = { - ...fields, - ...flatAlertRuleParams(fields[ALERT_RULE_PARAMETERS] as {}, ALERT_RULE_PARAMETERS), - }; + const inventoryFields = fields[ALERT_RULE_PARAMETERS] + ? { + ...fields, + ...flatAlertRuleParams(fields[ALERT_RULE_PARAMETERS] as {}, ALERT_RULE_PARAMETERS), + } + : fields; + + const nodeType = castArray(inventoryFields[ALERT_RULE_PARAMETERS_NODE_TYPE])[0]; + + if (!nodeType) { + return ''; } - const nodeTypeField = `${ALERT_RULE_PARAMETERS}.nodeType`; - const nodeType = inventoryFields[nodeTypeField] as InventoryItemType; - const hostName = inventoryFields[HOST_NAME_FIELD]; + const assetIdField = findInventoryModel(nodeType).fields.id; + const assetId = inventoryFields[assetIdField]; + const assetDetailsSupported = Object.values(SupportedAssetTypes).includes( + nodeType as SupportedAssetTypes + ); + const criteriaMetric = inventoryFields[ALERT_RULE_PARAMTERS_INVENTORY_METRIC_ID][0]; - if (nodeType) { - if (hostName) { - return getLinkToHostDetails({ hostName, timestamp: inventoryFields[TIMESTAMP] }); - } - const linkToParams = { - nodeType: inventoryFields[nodeTypeField][0], - timestamp: Date.parse(inventoryFields[TIMESTAMP]), - customMetric: '', - metric: '', - }; - - // We always pick the first criteria metric for the URL - const criteriaMetric = inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.metric`][0]; - if (criteriaMetric === 'custom') { - const criteriaCustomMetricId = - inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`][0]; - const criteriaCustomMetricAggregation = - inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`][0]; - const criteriaCustomMetricField = - inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`][0]; - - const customMetric = encode({ - id: criteriaCustomMetricId, - type: 'custom', - field: criteriaCustomMetricField, - aggregation: criteriaCustomMetricAggregation, - }); - linkToParams.customMetric = customMetric; - linkToParams.metric = customMetric; - } else { - linkToParams.metric = encode({ type: criteriaMetric }); - } - return `${LINK_TO_INVENTORY}?${stringify(linkToParams)}`; + if (assetId && assetDetailsSupported) { + return getLinkToAssetDetails({ + assetId, + assetType: nodeType, + timestamp: inventoryFields[TIMESTAMP], + alertMetric: criteriaMetric, + assetDetailsLocator, + }); + } + + const linkToParams = { + nodeType, + timestamp: Date.parse(inventoryFields[TIMESTAMP]), + customMetric: '', + metric: '', + }; + + // We always pick the first criteria metric for the URL + + if (criteriaMetric === CUSTOM_METRIC_TYPE) { + const criteriaCustomMetricId = + inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`][0]; + const criteriaCustomMetricAggregation = + inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`][0]; + const criteriaCustomMetricField = + inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`][0]; + + const customMetric = encode({ + id: criteriaCustomMetricId, + type: CUSTOM_METRIC_TYPE, + field: criteriaCustomMetricField, + aggregation: criteriaCustomMetricAggregation, + }); + linkToParams.customMetric = customMetric; + linkToParams.metric = customMetric; + } else { + linkToParams.metric = encode({ type: criteriaMetric }); } - return LINK_TO_INVENTORY; + return inventoryLocator.getRedirectUrl({ + ...linkToParams, + }); }; -export const getMetricsViewInAppUrl = (fields: ParsedTechnicalFields & Record) => { - const hostName = fields[HOST_NAME_FIELD]; - const timestamp = fields[TIMESTAMP]; +export const getMetricsViewInAppUrl = ({ + fields, + groupBy, + assetDetailsLocator, +}: { + fields: ParsedTechnicalFields & Record; + groupBy?: string[]; + assetDetailsLocator?: LocatorPublic; +}) => { + if (!groupBy || !assetDetailsLocator) { + return METRICS_EXPLORER_URL; + } + + // creates an object of asset details supported assetType by their assetId field name + const assetTypeByAssetId = Object.values(SupportedAssetTypes).reduce((acc, curr) => { + acc[findInventoryModel(curr).fields.id] = curr; + return acc; + }, {} as Record); + + // detemines if the groupBy has a field that the asset details supports + const supportedAssetId = groupBy?.find((field) => !!assetTypeByAssetId[field]); + // assigns a nodeType if the groupBy field is supported by asset details + const supportedAssetType = supportedAssetId ? assetTypeByAssetId[supportedAssetId] : undefined; + + if (supportedAssetType) { + const assetId = fields[findInventoryModel(supportedAssetType).fields.id]; + const timestamp = fields[TIMESTAMP]; - return hostName ? getLinkToHostDetails({ hostName, timestamp }) : METRICS_EXPLORER_URL; + return getLinkToAssetDetails({ + assetId, + assetType: supportedAssetType, + timestamp, + assetDetailsLocator, + }); + } else { + return METRICS_EXPLORER_URL; + } }; -export function getLinkToHostDetails({ - hostName, +function getLinkToAssetDetails({ + assetId, + assetType, timestamp, + alertMetric, + assetDetailsLocator, }: { - hostName: string; + assetId: string; + assetType: InventoryItemType; timestamp: string; + alertMetric?: string; + assetDetailsLocator: LocatorPublic; }): string { - const queryParams = { - from: Date.parse(timestamp), - to: Date.parse(timestamp) + fifteenMinutesInMilliseconds, - }; - - const encodedParams = encode(stringify(queryParams)); - - return `/app/metrics/link-to/host-detail/${hostName}?${encodedParams}`; + return assetDetailsLocator.getRedirectUrl({ + assetId, + assetType, + assetDetails: { + dateRange: { + from: timestamp, + to: moment(timestamp).add(fifteenMinutesInMilliseconds, 'ms').toISOString(), + }, + ...(alertMetric && alertMetric !== CUSTOM_METRIC_TYPE ? { alertMetric } : undefined), + }, + }); } diff --git a/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts b/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts new file mode 100644 index 00000000000000..685b2bcacb2e4c --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ +export enum SupportedAssetTypes { + container = 'container', + host = 'host', +} diff --git a/x-pack/plugins/observability_solution/infra/common/constants.ts b/x-pack/plugins/observability_solution/infra/common/constants.ts index 0bbabffbb17ced..63dfa663ce2567 100644 --- a/x-pack/plugins/observability_solution/infra/common/constants.ts +++ b/x-pack/plugins/observability_solution/infra/common/constants.ts @@ -43,7 +43,6 @@ export const O11Y_AAD_FIELDS = [ 'tags', ]; -export const LINK_TO_INVENTORY = '/app/metrics/link-to/inventory'; export const METRICS_EXPLORER_URL = '/app/metrics/explorer'; export const fifteenMinutesInMilliseconds = 15 * 60 * 1000; diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts b/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts index d95440d7cac737..0d0fd398909a0a 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts @@ -9,12 +9,17 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public'; +import type { LocatorPublic } from '@kbn/share-plugin/common'; +import type { + AssetDetailsLocatorParams, + InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { InventoryMetricConditions, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, } from '../../../common/alerting/metrics'; import { validateMetricThreshold } from './components/validation'; -import { formatReason } from './rule_data_formatters'; +import { getRuleFormat } from './rule_data_formatters'; interface InventoryMetricRuleTypeParams extends RuleTypeParams { criteria: InventoryMetricConditions[]; @@ -50,7 +55,15 @@ const inventoryDefaultRecoveryMessage = i18n.translate( } ); -export function createInventoryMetricRuleType(): ObservabilityRuleTypeModel { +export function createInventoryMetricRuleType({ + assetDetailsLocator, + inventoryLocator, +}: { + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; +}): ObservabilityRuleTypeModel { + const format = getRuleFormat({ assetDetailsLocator, inventoryLocator }); + return { id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertDescription', { @@ -65,7 +78,7 @@ export function createInventoryMetricRuleType(): ObservabilityRuleTypeModel { - const reason = fields[ALERT_REASON] ?? '-'; +export const getRuleFormat = ({ + assetDetailsLocator, + inventoryLocator, +}: { + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; +}): ObservabilityRuleTypeFormatter => { + return ({ fields }) => { + const reason = fields[ALERT_REASON] ?? '-'; - return { - reason, - link: getInventoryViewInAppUrl(fields), + return { + reason, + link: getInventoryViewInAppUrl({ fields, assetDetailsLocator, inventoryLocator }), + hasBasePath: true, + }; }; }; diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts index 362c6a500dd8bf..a37d14c061f76c 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts @@ -9,12 +9,14 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; import { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import { AssetDetailsLocatorParams } from '@kbn/observability-shared-plugin/common'; import { MetricExpressionParams, METRIC_THRESHOLD_ALERT_TYPE_ID, } from '../../../common/alerting/metrics'; import { validateMetricThreshold } from './components/validation'; -import { formatReason } from './rule_data_formatters'; +import { getRuleFormat } from './rule_data_formatters'; export interface MetricThresholdRuleTypeParams extends RuleTypeParams { criteria: MetricExpressionParams[]; @@ -50,7 +52,11 @@ const metricThresholdDefaultRecoveryMessage = i18n.translate( } ); -export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel { +export function createMetricThresholdRuleType({ + assetDetailsLocator, +}: { + assetDetailsLocator?: LocatorPublic; +}): ObservabilityRuleTypeModel { return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', { @@ -65,7 +71,7 @@ export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel import('./components/alert_details_app_section')), priority: 10, }; diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts index 75d5bceb61327c..85169903c68d98 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts @@ -5,14 +5,33 @@ * 2.0. */ -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; import { ObservabilityRuleTypeFormatter } from '@kbn/observability-plugin/public'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import type { AssetDetailsLocatorParams } from '@kbn/observability-shared-plugin/common'; +import { castArray } from 'lodash'; +import { METRICS_EXPLORER_URL } from '../../../common/constants'; import { getMetricsViewInAppUrl } from '../../../common/alerting/metrics/alert_link'; -export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { - const reason = fields[ALERT_REASON] ?? '-'; - return { - reason, - link: getMetricsViewInAppUrl(fields), +export const getRuleFormat = ({ + assetDetailsLocator, +}: { + assetDetailsLocator?: LocatorPublic; +}): ObservabilityRuleTypeFormatter => { + return ({ fields }) => { + const reason = fields[ALERT_REASON] ?? '-'; + const parameters = fields[ALERT_RULE_PARAMETERS]; + + const link = getMetricsViewInAppUrl({ + fields, + groupBy: castArray(parameters?.groupBy as string[] | string), + assetDetailsLocator, + }); + + return { + reason, + link, + hasBasePath: link !== METRICS_EXPLORER_URL, + }; }; }; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts index e189c8e3524f32..3b3db1b21bd093 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { SupportedAssetTypes } from '../../../common/asset_details/types'; import type { DockerContainerMetrics, KubernetesContainerMetrics } from './charts/types'; -import { INTEGRATION_NAME, ASSET_DETAILS_ASSET_TYPE } from './types'; +import { IntegrationEventModules } from './types'; export const ASSET_DETAILS_FLYOUT_COMPONENT_NAME = 'infraAssetDetailsFlyout'; export const ASSET_DETAILS_PAGE_COMPONENT_NAME = 'infraAssetDetailsPage'; @@ -15,16 +16,16 @@ export const APM_HOST_FILTER_FIELD = 'host.hostname'; export const APM_CONTAINER_FILTER_FIELD = 'container.id'; export const APM_FILTER_FIELD_PER_ASSET_TYPE = { - [ASSET_DETAILS_ASSET_TYPE.container]: APM_CONTAINER_FILTER_FIELD, - [ASSET_DETAILS_ASSET_TYPE.host]: APM_HOST_FILTER_FIELD, + [SupportedAssetTypes.container]: APM_CONTAINER_FILTER_FIELD, + [SupportedAssetTypes.host]: APM_HOST_FILTER_FIELD, }; export const ASSET_DETAILS_URL_STATE_KEY = 'assetDetails'; export const INTEGRATIONS = { - [INTEGRATION_NAME.kubernetesNode]: 'kubernetes.node', - [INTEGRATION_NAME.kubernetesContainer]: 'kubernetes.container', - [INTEGRATION_NAME.docker]: 'docker', + [IntegrationEventModules.kubernetesNode]: 'kubernetes.node', + [IntegrationEventModules.kubernetesContainer]: 'kubernetes.container', + [IntegrationEventModules.docker]: 'docker', }; export const DOCKER_METRIC_TYPES: DockerContainerMetrics[] = ['cpu', 'memory', 'network', 'disk']; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.tsx new file mode 100644 index 00000000000000..135c7e2ce77e03 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.tsx @@ -0,0 +1,48 @@ +/* + * 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 { + type SnapshotMetricType, + findInventoryModel, + type InventoryModels, + InventoryItemType, +} from '@kbn/metrics-data-access-plugin/common'; +import { useAssetDetailsUrlState } from '../hooks/use_asset_details_url_state'; +import { useAssetDetailsRenderPropsContext } from '../hooks/use_asset_details_render_props'; +import { LegacyAlertMetricCallout } from './callouts/legacy_metric_callout'; +import { ContentTabIds } from '../types'; + +const INCOMING_ALERT_CALLOUT_VISIBLE_FOR = [ContentTabIds.OVERVIEW, ContentTabIds.METRICS]; + +const isSnapshotMetricType = ( + inventoryModel: InventoryModels, + value?: string +): value is SnapshotMetricType => { + return !!value && !!inventoryModel.metrics.snapshot[value]; +}; + +export const Callouts = () => { + const { asset } = useAssetDetailsRenderPropsContext(); + const [state] = useAssetDetailsUrlState(); + + const assetConfig = findInventoryModel(asset.type); + const alertMetric = isSnapshotMetricType(assetConfig, state?.alertMetric) + ? state?.alertMetric + : undefined; + + if (asset.type === 'host' && alertMetric && assetConfig.legacyMetrics?.includes(alertMetric)) { + return ( + + ); + } + + return null; +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.tsx new file mode 100644 index 00000000000000..f38897155fac26 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.tsx @@ -0,0 +1,82 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { InventoryItemType, SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { HOST_METRICS_DOC_HREF } from '../../../../common/visualizations'; +import { toMetricOpt } from '../../../../../common/snapshot_metric_i18n'; +import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; +import { ContentTabIds } from '../../types'; +import { useTabSwitcherContext } from '../../hooks/use_tab_switcher'; + +const DISMISSAL_LEGACY_ALERT_METRIC_STORAGE_KEY = 'infraAssetDetails:legacy_alert_metric_dismissed'; + +export const LegacyAlertMetricCallout = ({ + visibleFor, + metric, +}: { + visibleFor: ContentTabIds[]; + metric: SnapshotMetricType; +}) => { + const { activeTabId } = useTabSwitcherContext(); + const { asset } = useAssetDetailsRenderPropsContext(); + const [isDismissed, setDismissed] = useLocalStorage( + `${DISMISSAL_LEGACY_ALERT_METRIC_STORAGE_KEY}_${metric}`, + false + ); + + const onDismiss = () => { + setDismissed(true); + }; + + const metricLabel = toMetricOpt(metric, asset.id as InventoryItemType); + const hideCallout = isDismissed || !visibleFor.includes(activeTabId as ContentTabIds); + + if (hideCallout || !metricLabel) { + return null; + } + + return ( + + } + data-test-subj="infraAssetDetailsLegacyMetricAlertCallout" + onDismiss={onDismiss} + > + + + + ), + }} + /> + + ); +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx index 52bff06e75a338..eadcc74c5a8d37 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx @@ -22,22 +22,30 @@ import { Profiling, } from '../tabs'; import { ContentTabIds } from '../types'; +import { Callouts } from './callouts'; export const Content = () => { return ( - + + + + + + + + diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts index f7d8ca564f2936..d0694ef7f207fc 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts @@ -99,6 +99,7 @@ const AssetDetailsUrlStateRT = rt.partial({ profilingSearch: rt.string, alertStatus: AlertStatusRT, dashboardId: rt.string, + alertMetric: rt.string, }); const AssetDetailsUrlRT = rt.union([AssetDetailsUrlStateRT, rt.null]); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx index be98902ad9c57e..a3d94c5c6e14be 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx @@ -15,7 +15,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useUiSetting } from '@kbn/kibana-react-plugin/public'; import { enableInfrastructureAssetCustomDashboards } from '@kbn/observability-plugin/common'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import { capitalize } from 'lodash'; +import { capitalize, isEmpty } from 'lodash'; import React, { useCallback, useMemo } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { usePluginConfig } from '../../../containers/plugin_config_context'; @@ -62,7 +62,7 @@ export const useTemplateHeaderBreadcrumbs = () => { const breadcrumbs: EuiBreadcrumbsProps['breadcrumbs'] = // If there is a state object in location, it's persisted in case the page is opened in a new tab or after page refresh // With that, we can show the return button. Otherwise, it will be hidden (ex: the user opened a shared URL or opened the page from their bookmarks) - location.state || history.length > 1 + !isEmpty(location.state) || history.length > 1 ? [ { text: ( diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx index 5ac8809be54431..346acb6d8a1647 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx @@ -5,16 +5,13 @@ * 2.0. */ -import { EuiFlexGroup } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { SYSTEM_INTEGRATION } from '../../../../common/constants'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; import { useParentBreadcrumbResolver } from '../../../hooks/use_parent_breadcrumb_resolver'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import { InfraLoadingPanel } from '../../loading'; import { ASSET_DETAILS_PAGE_COMPONENT_NAME } from '../constants'; import { Content } from '../content/content'; import { useAssetDetailsRenderPropsContext } from '../hooks/use_asset_details_render_props'; @@ -86,7 +83,7 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { onboardingFlow={asset.type === 'host' ? OnboardingFlow.Hosts : OnboardingFlow.Infra} dataAvailabilityModules={DATA_AVAILABILITY_PER_TYPE[asset.type] || undefined} pageHeader={{ - pageTitle: asset.name, + pageTitle: loading ? : asset.name, tabs: tabEntries, rightSideItems, breadcrumbs: headerBreadcrumbs, @@ -94,24 +91,7 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { data-component-name={ASSET_DETAILS_PAGE_COMPONENT_NAME} data-asset-type={asset.type} > - {loading ? ( - - - - ) : ( - - )} + ); }; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts index 01700206285e8f..064b82094a5059 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts @@ -94,13 +94,8 @@ export interface RouteState { export type DataViewOrigin = 'logs' | 'metrics'; -export enum INTEGRATION_NAME { +export enum IntegrationEventModules { kubernetesNode = 'kubernetesNode', kubernetesContainer = 'kubernetesContainer', docker = 'docker', } - -export enum ASSET_DETAILS_ASSET_TYPE { - container = 'container', - host = 'host', -} diff --git a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx index ca271b46146c8e..d8522aa0f4d590 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx @@ -10,7 +10,10 @@ import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import type { SerializableRecord } from '@kbn/utility-types'; -import { ASSET_DETAILS_LOCATOR_ID } from '@kbn/observability-shared-plugin/common'; +import { + ASSET_DETAILS_LOCATOR_ID, + type AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { useHostIpToName } from './use_host_ip_to_name'; import { LoadingPage } from '../../components/loading_page'; import { Error } from '../error'; @@ -32,7 +35,7 @@ export const RedirectToHostDetailViaIP = ({ const { services: { share }, } = useKibanaContextForPlugin(); - const baseLocator = share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const baseLocator = share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); const { error, name } = useHostIpToName(hostIp, (metricsView && metricsView.indices) || null); diff --git a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx index 714be106fad3f6..d0bac8d8c9bf25 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx @@ -14,7 +14,8 @@ import { type AssetDetailsLocatorParams, } from '@kbn/observability-shared-plugin/common'; import type { SerializableRecord } from '@kbn/utility-types'; -import { AssetDetailsUrlState } from '../../components/asset_details/types'; +import { SupportedAssetTypes } from '../../../common/asset_details/types'; +import { type AssetDetailsUrlState } from '../../components/asset_details/types'; import { ASSET_DETAILS_URL_STATE_KEY } from '../../components/asset_details/constants'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; @@ -22,7 +23,7 @@ export const REDIRECT_NODE_DETAILS_FROM_KEY = 'from'; export const REDIRECT_NODE_DETAILS_TO_KEY = 'to'; export const REDIRECT_ASSET_DETAILS_KEY = 'assetDetails'; -const getHostDetailSearch = (queryParams: URLSearchParams) => { +const getAssetDetailsQueryParams = (queryParams: URLSearchParams) => { const from = queryParams.get(REDIRECT_NODE_DETAILS_FROM_KEY); const to = queryParams.get(REDIRECT_NODE_DETAILS_TO_KEY); const assetDetailsParam = queryParams.get(REDIRECT_ASSET_DETAILS_KEY); @@ -59,7 +60,9 @@ const getNodeDetailSearch = (queryParams: URLSearchParams) => { }; export const getSearchParams = (nodeType: InventoryItemType, queryParams: URLSearchParams) => - nodeType === 'host' ? getHostDetailSearch(queryParams) : getNodeDetailSearch(queryParams); + Object.values(SupportedAssetTypes).includes(nodeType as SupportedAssetTypes) + ? getAssetDetailsQueryParams(queryParams) + : getNodeDetailSearch(queryParams); export const RedirectToNodeDetail = () => { const { diff --git a/x-pack/plugins/observability_solution/infra/public/plugin.ts b/x-pack/plugins/observability_solution/infra/public/plugin.ts index 6a4e813064eefa..86d5e7816ce7ae 100644 --- a/x-pack/plugins/observability_solution/infra/public/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/public/plugin.ts @@ -23,6 +23,12 @@ import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { apiCanAddNewPanel } from '@kbn/presentation-containers'; import { IncompatibleActionError, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public'; +import { + ASSET_DETAILS_LOCATOR_ID, + INVENTORY_LOCATOR_ID, + type AssetDetailsLocatorParams, + type InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import type { InfraPublicConfig } from '../common/plugin_config_types'; import { createInventoryMetricRuleType } from './alerting/inventory'; import { createLogThresholdRuleType } from './alerting/log_threshold'; @@ -80,12 +86,17 @@ export class Plugin implements InfraClientPluginClass { id: ObservabilityTriggerId.LogEntryContextMenu, }); + const assetDetailsLocator = + pluginsSetup.share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const inventoryLocator = + pluginsSetup.share.url.locators.get(INVENTORY_LOCATOR_ID); + pluginsSetup.observability.observabilityRuleTypeRegistry.register( - createInventoryMetricRuleType() + createInventoryMetricRuleType({ assetDetailsLocator, inventoryLocator }) ); pluginsSetup.observability.observabilityRuleTypeRegistry.register( - createMetricThresholdRuleType() + createMetricThresholdRuleType({ assetDetailsLocator }) ); if (this.config.featureFlags.logsUIEnabled) { diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts index 0de0a5a0797b47..73a7ed749446e2 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts @@ -21,7 +21,13 @@ import { set } from '@kbn/safer-lodash-set'; import { Alert } from '@kbn/alerts-as-data-utils'; import { type Group } from '@kbn/observability-alerting-rule-utils'; import { ParsedExperimentalFields } from '@kbn/rule-registry-plugin/common/parse_experimental_fields'; +import type { LocatorPublic } from '@kbn/share-plugin/common'; +import type { + AssetDetailsLocatorParams, + InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { + ALERT_RULE_PARAMETERS_NODE_TYPE, getInventoryViewInAppUrl, getMetricsViewInAppUrl, } from '../../../../common/alerting/metrics/alert_link'; @@ -130,6 +136,8 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ spaceId, timestamp, hostName, + assetDetailsLocator, + inventoryLocator, }: { basePath: IBasePath; criteria: InventoryMetricConditions[]; @@ -137,6 +145,8 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ spaceId: string; timestamp: string; hostName?: string; + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; }) => { const { metric, customMetric } = criteria[0]; @@ -145,7 +155,7 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`]: [customMetric?.id], [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`]: [customMetric?.aggregation], [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`]: [customMetric?.field], - [`${ALERT_RULE_PARAMETERS}.nodeType`]: [nodeType], + [ALERT_RULE_PARAMETERS_NODE_TYPE]: [nodeType], [TIMESTAMP]: timestamp, [HOST_NAME]: hostName, }; @@ -153,7 +163,11 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ return addSpaceIdToPath( basePath.publicBaseUrl, spaceId, - getInventoryViewInAppUrl(parseTechnicalFields(fields, true)) + getInventoryViewInAppUrl({ + fields: parseTechnicalFields(fields, true), + assetDetailsLocator, + inventoryLocator, + }) ); }; @@ -161,22 +175,27 @@ export const getMetricsViewInAppUrlWithSpaceId = ({ basePath, spaceId, timestamp, - hostName, + groupBy, + assetDetailsLocator, }: { basePath: IBasePath; spaceId: string; timestamp: string; - hostName?: string; + groupBy?: string[]; + assetDetailsLocator?: LocatorPublic; }) => { const fields = { [TIMESTAMP]: timestamp, - [HOST_NAME]: hostName, }; return addSpaceIdToPath( basePath.publicBaseUrl, spaceId, - getMetricsViewInAppUrl(parseTechnicalFields(fields, true)) + getMetricsViewInAppUrl({ + fields: parseTechnicalFields(fields, true), + groupBy, + assetDetailsLocator, + }) ); }; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts index f76a6e82e67d54..2f621d04f38dfb 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts @@ -23,6 +23,7 @@ import { InfraBackendLibs } from '../../infra_types'; import { infraPluginMock } from '../../../mocks'; import { logsSharedPluginMock } from '@kbn/logs-shared-plugin/server/mocks'; import { createLogSourcesServiceMock } from '@kbn/logs-data-access-plugin/common/services/log_sources_service/log_sources_service.mocks'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; jest.mock('./evaluate_condition', () => ({ evaluateCondition: jest.fn() })); @@ -136,6 +137,11 @@ const mockLibs = { publicBaseUrl: 'http://localhost:5601', prepend: (path: string) => path, }, + plugins: { + share: { + setup: sharePluginMock.createSetupContract(), + }, + }, logger, } as unknown as InfraBackendLibs; const alerts = new Map(); diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 80da1034df5ac2..9f8b3b6d0bfa04 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -21,9 +21,20 @@ import { AlertInstanceState as AlertState, } from '@kbn/alerting-plugin/common'; import { AlertsClientError, RuleExecutorOptions, RuleTypeState } from '@kbn/alerting-plugin/server'; -import { convertToBuiltInComparators, getAlertUrl } from '@kbn/observability-plugin/common'; +import { + AlertsLocatorParams, + alertsLocatorID, + convertToBuiltInComparators, + getAlertUrl, +} from '@kbn/observability-plugin/common'; import type { InventoryItemType, SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common'; import { ObservabilityMetricsAlert } from '@kbn/alerts-as-data-utils'; +import { + ASSET_DETAILS_LOCATOR_ID, + INVENTORY_LOCATOR_ID, + type AssetDetailsLocatorParams, + type InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates, @@ -96,6 +107,13 @@ export const createInventoryMetricThresholdExecutor = getTimeRange, } = options; + const { share } = libs.plugins; + const alertsLocator = share.setup.url.locators.get(alertsLocatorID); + const assetDetailsLocator = + share.setup.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const inventoryLocator = + share.setup.url.locators.get(INVENTORY_LOCATOR_ID); + const startTime = Date.now(); const { criteria, filterQuery, sourceId = 'default', nodeType, alertOnNoData } = params; @@ -141,7 +159,7 @@ export const createInventoryMetricThresholdExecutor = uuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[AlertStates.ERROR], @@ -156,6 +174,8 @@ export const createInventoryMetricThresholdExecutor = nodeType, timestamp: indexedStartedAt, spaceId, + assetDetailsLocator, + inventoryLocator, }), }, }); @@ -293,7 +313,7 @@ export const createInventoryMetricThresholdExecutor = uuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[nextState], @@ -312,6 +332,8 @@ export const createInventoryMetricThresholdExecutor = timestamp: indexedStartedAt, spaceId, hostName: additionalContext?.host?.name, + assetDetailsLocator, + inventoryLocator, }), ...additionalContext, }; @@ -347,7 +369,7 @@ export const createInventoryMetricThresholdExecutor = alertUuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[AlertStates.OK], @@ -362,6 +384,8 @@ export const createInventoryMetricThresholdExecutor = timestamp: indexedStartedAt, spaceId, hostName: additionalContext?.host?.name, + assetDetailsLocator, + inventoryLocator, }), originalAlertState: translateActionGroupToAlertState(originalActionGroup), originalAlertStateWasALERT: originalActionGroup === FIRED_ACTIONS_ID, diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 9b562e3d491432..44cd61943df495 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -31,6 +31,7 @@ import { ALERT_GROUP, } from '@kbn/rule-data-utils'; import { type Group } from '@kbn/observability-alerting-rule-utils'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; jest.mock('./lib/evaluate_rule', () => ({ evaluateRule: jest.fn() })); @@ -2473,6 +2474,11 @@ const mockLibs: any = { publicBaseUrl: 'http://localhost:5601', prepend: (path: string) => path, }, + plugins: { + share: { + setup: sharePluginMock.createSetupContract(), + }, + }, logger, }; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 4c0a19ae2e5128..258a410d4775c9 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -12,7 +12,7 @@ import { ALERT_GROUP, ALERT_REASON, } from '@kbn/rule-data-utils'; -import { isEqual } from 'lodash'; +import { castArray, isEqual } from 'lodash'; import { ActionGroupIdsOf, AlertInstanceContext as AlertContext, @@ -20,11 +20,20 @@ import { RecoveredActionGroup, } from '@kbn/alerting-plugin/common'; import { AlertsClientError, RuleExecutorOptions, RuleTypeState } from '@kbn/alerting-plugin/server'; -import { TimeUnitChar, getAlertUrl } from '@kbn/observability-plugin/common'; +import { + AlertsLocatorParams, + TimeUnitChar, + alertsLocatorID, + getAlertUrl, +} from '@kbn/observability-plugin/common'; import { ObservabilityMetricsAlert } from '@kbn/alerts-as-data-utils'; import { COMPARATORS } from '@kbn/alerting-comparators'; import { getEcsGroups, type Group } from '@kbn/observability-alerting-rule-utils'; import { convertToBuiltInComparators } from '@kbn/observability-plugin/common/utils/convert_legacy_outside_comparator'; +import { + ASSET_DETAILS_LOCATOR_ID, + AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; @@ -111,6 +120,11 @@ export const createMetricThresholdExecutor = MetricThresholdAlert > ) => { + const { share } = libs.plugins; + const alertsLocator = share.setup.url.locators.get(alertsLocatorID); + const assetDetailsLocator = + share.setup.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const startTime = Date.now(); const { @@ -126,6 +140,8 @@ export const createMetricThresholdExecutor = const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); + const groupBy = castArray(params.groupBy); + const logger = createScopedLogger(libs.logger, 'metricThresholdRule', { alertId: ruleId, executionId, @@ -167,7 +183,7 @@ export const createMetricThresholdExecutor = uuid, spaceId, start ?? startedAt.toISOString(), - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), }, @@ -203,6 +219,8 @@ export const createMetricThresholdExecutor = basePath: libs.basePath, spaceId, timestamp, + groupBy, + assetDetailsLocator, }), }; @@ -217,7 +235,7 @@ export const createMetricThresholdExecutor = state: { lastRunTimestamp: startedAt.valueOf(), missingGroups: [], - groupBy: params.groupBy, + groupBy, filterQuery: params.filterQuery, }, }; @@ -410,7 +428,8 @@ export const createMetricThresholdExecutor = basePath: libs.basePath, spaceId, timestamp, - hostName: additionalContext?.host?.name, + groupBy, + assetDetailsLocator, }), ...additionalContext, }; @@ -450,7 +469,7 @@ export const createMetricThresholdExecutor = alertUuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[AlertStates.OK], @@ -468,7 +487,8 @@ export const createMetricThresholdExecutor = basePath: libs.basePath, spaceId, timestamp: indexedStartedAt, - hostName: additionalContext?.host?.name, + groupBy, + assetDetailsLocator, }), originalAlertState: translateActionGroupToAlertState(originalActionGroup), @@ -486,7 +506,7 @@ export const createMetricThresholdExecutor = state: { lastRunTimestamp: startedAt.valueOf(), missingGroups: [...nextMissingGroups], - groupBy: params.groupBy, + groupBy, filterQuery: params.filterQuery, }, }; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts b/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts index 1936c59d7a6375..e99d57eb4d6c81 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts @@ -28,12 +28,12 @@ export const getApmDataAccessClient = ({ request: KibanaRequest; }) => { const hasPrivileges = async () => { - const [, { apmDataAccess }] = await libs.getStartServices(); - return apmDataAccess.hasPrivileges({ request }); + const apmDataAccessStart = await libs.plugins.apmDataAccess.start(); + return apmDataAccessStart.hasPrivileges({ request }); }; const getServices = async () => { - const { apmDataAccess } = libs; + const apmDataAccess = libs.plugins.apmDataAccess.setup; const coreContext = await context.core; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts b/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts index 96c5cd9f311d71..f13424c6331d3d 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts @@ -8,23 +8,30 @@ import type { Logger } from '@kbn/logging'; import type { IBasePath } from '@kbn/core/server'; import type { handleEsError } from '@kbn/es-ui-shared-plugin/server'; -import type { AlertsLocatorParams } from '@kbn/observability-plugin/common'; import { ObservabilityConfig } from '@kbn/observability-plugin/server'; -import type { LocatorPublic } from '@kbn/share-plugin/common'; import type { ILogsSharedLogEntriesDomain } from '@kbn/logs-shared-plugin/server'; -import type { ApmDataAccessPluginSetup } from '@kbn/apm-data-access-plugin/server'; import { RulesServiceSetup } from '../services/rules'; import { InfraConfig, InfraPluginStartServicesAccessor } from '../types'; import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; import { InfraMetricsDomain } from './domains/metrics_domain'; import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; +import type { InfraServerPluginSetupDeps, InfraServerPluginStartDeps } from './adapters/framework'; export interface InfraDomainLibs { logEntries: ILogsSharedLogEntriesDomain; metrics: InfraMetricsDomain; } +type Plugins = { + [key in keyof InfraServerPluginSetupDeps]: { + setup: Required[key]; + } & (key extends keyof InfraServerPluginStartDeps + ? { + start: () => Promise[key]>; + } + : {}); +}; export interface InfraBackendLibs extends InfraDomainLibs { basePath: IBasePath; configuration: InfraConfig; @@ -37,6 +44,5 @@ export interface InfraBackendLibs extends InfraDomainLibs { getStartServices: InfraPluginStartServicesAccessor; handleEsError: typeof handleEsError; logger: Logger; - alertsLocator?: LocatorPublic; - apmDataAccess: ApmDataAccessPluginSetup; + plugins: Plugins; } diff --git a/x-pack/plugins/observability_solution/infra/server/plugin.ts b/x-pack/plugins/observability_solution/infra/server/plugin.ts index 6e6f87776fbcab..530dec8bc1ca4b 100644 --- a/x-pack/plugins/observability_solution/infra/server/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/server/plugin.ts @@ -16,9 +16,9 @@ import { import { handleEsError } from '@kbn/es-ui-shared-plugin/server'; import { i18n } from '@kbn/i18n'; import { Logger } from '@kbn/logging'; -import { alertsLocatorID } from '@kbn/observability-plugin/common'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { GetMetricIndicesOptions } from '@kbn/metrics-data-access-plugin/server'; +import { mapValues } from 'lodash'; import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; import { publicConfigKeys } from '../common/plugin_config_types'; import { LOGS_FEATURE, METRICS_FEATURE } from './features'; @@ -212,12 +212,24 @@ export class InfraServerPlugin metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), }; + // Instead of passing plugins individually to `libs` on a necessity basis, + // this provides an object with all plugins infra depends on + const libsPlugins = mapValues(plugins, (value, key) => { + return { + setup: value, + start: () => + core.getStartServices().then((services) => { + const [, pluginsStartContracts] = services; + return pluginsStartContracts[key as keyof InfraServerPluginStartDeps]; + }), + }; + }) as InfraBackendLibs['plugins']; + this.libs = { configuration: this.config, framework, sources, sourceStatus, - apmDataAccess: plugins.apmDataAccess, ...domainLibs, handleEsError, logsRules: this.logsRules.setup(core, plugins), @@ -226,7 +238,7 @@ export class InfraServerPlugin getAlertDetailsConfig: () => plugins.observability.getAlertDetailsConfig(), logger: this.logger, basePath: core.http.basePath, - alertsLocator: plugins.share.url.locators.get(alertsLocatorID), + plugins: libsPlugins, }; plugins.features.registerKibanaFeature(METRICS_FEATURE); diff --git a/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts index e962a11f9a396d..86af345d5175e8 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts @@ -39,7 +39,7 @@ export const initServicesRoute = (libs: InfraBackendLibs) => { const client = createSearchClient(requestContext, framework, request); const soClient = savedObjects.getScopedClient(request); - const apmIndices = await libs.apmDataAccess.getApmIndices(soClient); + const apmIndices = await libs.plugins.apmDataAccess.setup.getApmIndices(soClient); const services = await getServices(client, apmIndices, { from, to, diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts index 12a4b6c4e13cef..b0f801d2613c1d 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts @@ -11,6 +11,7 @@ export { getFieldByType, findInventoryFields, metrics, + type InventoryModels, } from './inventory_models'; export { podSnapshotMetricTypes } from './inventory_models/kubernetes/pod'; diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts index d77f15ad4ca383..731a84f1e83ab2 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts @@ -56,4 +56,5 @@ export const host: InventoryModel = { ...nginxRequireMetrics, ], tooltipMetrics: ['cpuV2', 'memory', 'txV2', 'rxV2', 'cpu', 'tx', 'rx'], + legacyMetrics: ['cpu', 'tx', 'rx'], }; diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts index 41115a95405e29..7dddfab593784c 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts @@ -29,7 +29,7 @@ const catalog = { export const inventoryModels = Object.values(catalog); -type InventoryModels = (typeof catalog)[T]; +export type InventoryModels = (typeof catalog)[T]; export const findInventoryModel = (type: T): InventoryModels => { const model = inventoryModels.find((m) => m.id === type); diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts index cc018b24eea13c..042958ef7bd590 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts @@ -423,6 +423,7 @@ export interface InventoryModel { }; metrics: TMetrics; requiredMetrics: InventoryMetric[]; + legacyMetrics?: SnapshotMetricType[]; tooltipMetrics: SnapshotMetricType[]; nodeFilter?: object[]; } diff --git a/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts b/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts index 1715a28b1caab7..81586bf25ed789 100644 --- a/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts +++ b/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts @@ -29,12 +29,18 @@ export const METRIC_FORMATTERS: MetricFormatters = { formatter: InfraFormatterType.percent, template: '{{value}}', }, + ['cpuV2']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, ['memory']: { formatter: InfraFormatterType.percent, template: '{{value}}', }, ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['rxV2']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['txV2']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, ['logRate']: { formatter: InfraFormatterType.abbreviatedNumber, template: '{{value}}/s', diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts index 59729aeb71f0ed..ca44baa6de6e1a 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts @@ -13,7 +13,7 @@ export type AssetDetailsLocator = LocatorPublic; export interface AssetDetailsLocatorParams extends SerializableRecord { assetType: string; assetId: string; - state?: SerializableRecord; + // asset types not migrated to use the asset details page _a?: { time?: { from?: string; @@ -23,11 +23,13 @@ export interface AssetDetailsLocatorParams extends SerializableRecord { }; assetDetails?: { tabId?: string; + name?: string; dashboardId?: string; dateRange?: { from: string; to: string; }; + alertMetric?: string; }; } @@ -36,12 +38,23 @@ export const ASSET_DETAILS_LOCATOR_ID = 'ASSET_DETAILS_LOCATOR'; export class AssetDetailsLocatorDefinition implements LocatorDefinition { public readonly id = ASSET_DETAILS_LOCATOR_ID; - public readonly getLocation = async (params: AssetDetailsLocatorParams) => { - const searchPath = rison.encodeUnknown(params._a); - const assetDetails = rison.encodeUnknown(params.assetDetails); + public readonly getLocation = async ( + params: AssetDetailsLocatorParams & { state?: SerializableRecord } + ) => { + const legacyNodeDetailsQueryParams = rison.encodeUnknown(params._a); + const assetDetailsQueryParams = rison.encodeUnknown(params.assetDetails); + + const queryParams = []; + if (assetDetailsQueryParams !== undefined) { + queryParams.push(`assetDetails=${assetDetailsQueryParams}`); + } + if (legacyNodeDetailsQueryParams !== undefined) { + queryParams.push(`_a=${legacyNodeDetailsQueryParams}`); + } + return { app: 'metrics', - path: `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}&_a=${searchPath}`, + path: `/detail/${params.assetType}/${params.assetId}?${queryParams.join('&')}`, state: params.state ? params.state : {}, }; }; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts index ca6e997468b5b2..9f4cd58188edb7 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts @@ -40,12 +40,12 @@ export interface InventoryLocatorParams extends SerializableRecord { metric: string; // encoded value nodeType: string; region?: string; - sort: { + sort?: { by: string; direction: 'desc' | 'async'; }; - timelineOpen: boolean; - view: 'map' | 'table'; + timelineOpen?: boolean; + view?: 'map' | 'table'; state?: SerializableRecord; } diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts index c7b5e16625e038..8c7dc0d4b61136 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts @@ -60,7 +60,7 @@ describe('Infra Locators', () => { expect(app).toBe('metrics'); expect(path).toBe( - `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}&_a=undefined` + `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}` ); expect(state).toBeDefined(); expect(Object.keys(state)).toHaveLength(0); @@ -72,7 +72,7 @@ describe('Infra Locators', () => { expect(app).toBe('metrics'); expect(path).toBe( - `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}&_a=undefined` + `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}` ); expect(state).toBeDefined(); expect(Object.keys(state)).toHaveLength(0); diff --git a/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts b/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts index 8773ace90c71fb..2ac6a69c6a0d9e 100644 --- a/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts @@ -19,6 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import { createLazyObservabilityPageTemplate } from './components/page_template'; import { createNavigationRegistry } from './components/page_template/helpers/navigation_registry'; import { registerProfilingComponent } from './components/profiling/helpers/component_registry'; +export { updateGlobalNavigation } from './services/update_global_navigation'; import { AssetDetailsFlyoutLocatorDefinition, AssetDetailsLocatorDefinition, diff --git a/x-pack/test/functional/apps/infra/node_details.ts b/x-pack/test/functional/apps/infra/node_details.ts index fbc442b5079c0c..f960208ab47456 100644 --- a/x-pack/test/functional/apps/infra/node_details.ts +++ b/x-pack/test/functional/apps/infra/node_details.ts @@ -7,6 +7,7 @@ import moment from 'moment'; import expect from '@kbn/expect'; +import rison from '@kbn/rison'; import { InfraSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { enableInfrastructureContainerAssetView, @@ -42,6 +43,11 @@ const END_HOST_KUBERNETES_SECTION_DATE = moment.utc( const START_CONTAINER_DATE = moment.utc(DATE_WITH_DOCKER_DATA_FROM); const END_CONTAINER_DATE = moment.utc(DATE_WITH_DOCKER_DATA_TO); +interface QueryParams { + name?: string; + alertMetric?: string; +} + export default ({ getPageObjects, getService }: FtrProviderContext) => { const observability = getService('observability'); const browser = getService('browser'); @@ -59,19 +65,24 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'timePicker', ]); - const getNodeDetailsUrl = (assetName: string) => { - const queryParams = new URLSearchParams(); - - queryParams.set('assetName', assetName); - - return queryParams.toString(); + const getNodeDetailsUrl = (queryParams?: QueryParams) => { + return rison.encodeUnknown( + Object.entries(queryParams ?? {}).reduce>((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}) + ); }; - const navigateToNodeDetails = async (assetId: string, assetName: string, assetType: string) => { + const navigateToNodeDetails = async ( + assetId: string, + assetType: string, + queryParams?: QueryParams + ) => { await pageObjects.common.navigateToUrlWithBrowserHistory( 'infraOps', `/${NODE_DETAILS_PATH}/${assetType}/${assetId}`, - getNodeDetailsUrl(assetName), + `assetDetails=${getNodeDetailsUrl(queryParams)}`, { insertTimestamp: false, ensureCurrentUrl: false, @@ -113,7 +124,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); await browser.setWindowSize(1600, 1200); - await navigateToNodeDetails('Jennys-MBP.fritz.box', 'Jennys-MBP.fritz.box', 'host'); + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + }); await pageObjects.header.waitUntilLoadingHasFinished(); }); @@ -270,7 +283,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const ALL_ALERTS = ACTIVE_ALERTS + RECOVERED_ALERTS; const COLUMNS = 11; before(async () => { - await navigateToNodeDetails('demo-stack-apache-01', 'demo-stack-apache-01', 'host'); + await navigateToNodeDetails('demo-stack-apache-01', 'host', { + name: 'demo-stack-apache-01', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await pageObjects.timePicker.setAbsoluteRange( @@ -282,7 +297,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); after(async () => { - await navigateToNodeDetails('Jennys-MBP.fritz.box', 'Jennys-MBP.fritz.box', 'host'); + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await pageObjects.timePicker.setAbsoluteRange( @@ -505,7 +522,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Host with alerts and no processes', () => { before(async () => { - await navigateToNodeDetails('demo-stack-mysql-01', 'demo-stack-mysql-01', 'host'); + await navigateToNodeDetails('demo-stack-mysql-01', 'host', { + name: 'demo-stack-mysql-01', + }); await pageObjects.timePicker.setAbsoluteRange( START_HOST_ALERTS_DATE.format(DATE_PICKER_FORMAT), END_HOST_ALERTS_DATE.format(DATE_PICKER_FORMAT) @@ -539,11 +558,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('#With Kubernetes section', () => { before(async () => { - await navigateToNodeDetails( - 'demo-stack-kubernetes-01', - 'demo-stack-kubernetes-01', - 'host' - ); + await navigateToNodeDetails('demo-stack-kubernetes-01', 'host', { + name: 'demo-stack-kubernetes-01', + }); await pageObjects.header.waitUntilLoadingHasFinished(); }); @@ -623,6 +640,43 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); + + describe('Callouts', () => { + describe('Legacy alert metric callout', () => { + [{ metric: 'cpu' }, { metric: 'rx' }, { metric: 'tx' }].forEach(({ metric }) => { + it(`Should show for: ${metric}`, async () => { + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + alertMetric: metric, + }); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async () => { + expect(await pageObjects.assetDetails.legacyMetricAlertCalloutExists()).to.be( + true + ); + }); + }); + }); + + [{ metric: 'cpuV2' }, { metric: 'rxV2' }, { metric: 'txV2' }].forEach(({ metric }) => { + it(`Should not show for: ${metric}`, async () => { + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + alertMetric: metric, + }); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async () => { + expect(await pageObjects.assetDetails.legacyMetricAlertCalloutExists()).to.be( + false + ); + }); + }); + }); + }); + }); }); describe('#Asset Type: container', () => { @@ -647,7 +701,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('when container asset view is disabled', () => { it('should show old view of container details', async () => { await setInfrastructureContainerAssetViewUiSetting(false); - await navigateToNodeDetails('container-id-0', 'container-id-0', 'container'); + await navigateToNodeDetails('container-id-0', 'container', { + name: 'container-id-0', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.find('metricsEmptyViewState'); }); @@ -656,7 +712,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('when container asset view is enabled', () => { before(async () => { await setInfrastructureContainerAssetViewUiSetting(true); - await navigateToNodeDetails('container-id-0', 'container-id-0', 'container'); + await navigateToNodeDetails('container-id-0', 'container', { + name: 'container-id-0', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await pageObjects.timePicker.setAbsoluteRange( START_CONTAINER_DATE.format(DATE_PICKER_FORMAT), diff --git a/x-pack/test/functional/page_objects/asset_details.ts b/x-pack/test/functional/page_objects/asset_details.ts index 4a56b3dce469cb..4e3da871a91b6f 100644 --- a/x-pack/test/functional/page_objects/asset_details.ts +++ b/x-pack/test/functional/page_objects/asset_details.ts @@ -352,5 +352,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { return testSubjects.click(buttonSubject); }, + + // Callouts + async legacyMetricAlertCalloutExists() { + return testSubjects.exists('infraAssetDetailsLegacyMetricAlertCallout'); + }, }; }