Skip to content

Commit

Permalink
[Infra] Handle view in app for legacy metrics (elastic#190295)
Browse files Browse the repository at this point in the history
closes [elastic#189625](elastic#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
  • Loading branch information
crespocarlos authored Aug 19, 2024
1 parent 29c5381 commit d69e598
Show file tree
Hide file tree
Showing 39 changed files with 770 additions and 217 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<InventoryLocator>;

const mockAssetDetailsLocator = {
getRedirectUrl: jest
.fn()
.mockImplementation(
({ assetId, assetType, assetDetails }: AssetDetailsLocatorParams) =>
`/node-mock/${assetType}/${assetId}?receivedParams=${rison.encodeUnknown(assetDetails)}`
),
} as unknown as jest.Mocked<AssetDetailsLocator>;

describe('Inventory Threshold Rule', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('flatAlertRuleParams', () => {
it('flat ALERT_RULE_PARAMETERS', () => {
expect(
Expand Down Expand Up @@ -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<string, any>;
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&timestamp=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', () => {
Expand All @@ -96,22 +131,50 @@ describe('Inventory Threshold Rule', () => {
[`${ALERT_RULE_PARAMETERS}.nodeType`]: 'host',
[`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'],
} as unknown as ParsedTechnicalFields & Record<string, any>;
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&timestamp=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<string, any>;
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<string, any>;
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'))"
);
});

Expand Down Expand Up @@ -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<string, any>;
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&timestamp=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)"
);
});

Expand All @@ -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<string, any>;
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&timestamp=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<string, any>;
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<string, any>;
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<string, any>;
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<string, any>;
const url = getMetricsViewInAppUrl(fields);
const url = getMetricsViewInAppUrl({ fields });
expect(url).toEqual('/app/metrics/explorer');
});
});
Expand Down
Loading

0 comments on commit d69e598

Please sign in to comment.