Skip to content

Commit

Permalink
Implement Metrics Explorer Locator (elastic#190821)
Browse files Browse the repository at this point in the history
Relates to elastic#176667 

## Summary

This PR adds the `MetricsExplorerLocator` and fixes some bugs that were
found regarding how locators were getting created and propagated.

## Testing

Alerts were created with group_by and no grouping to trigger for some
CPU usage. Going to the alert page and clicking the app view icon should
take you to the asset details page (if grouped) or to the metrics
explorer page (if no group) via the locators.

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Elastic Machine <[email protected]>
Co-authored-by: Carlos Crespo <[email protected]>
Co-authored-by: Bena Kansara <[email protected]>
  • Loading branch information
4 people authored Sep 3, 2024
1 parent 051f04c commit 1a92a4b
Show file tree
Hide file tree
Showing 24 changed files with 249 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
AssetDetailsLocator,
InventoryLocatorParams,
AssetDetailsLocatorParams,
MetricsExplorerLocatorParams,
MetricsExplorerLocator,
} from '@kbn/observability-shared-plugin/common';

jest.mock('@kbn/observability-shared-plugin/common');
Expand All @@ -40,6 +42,12 @@ const mockAssetDetailsLocator = {
),
} as unknown as jest.Mocked<AssetDetailsLocator>;

const mockMetricsExplorerLocator = {
getRedirectUrl: jest
.fn()
.mockImplementation(({}: MetricsExplorerLocatorParams) => `/metrics-mock`),
} as unknown as jest.Mocked<MetricsExplorerLocator>;

describe('Inventory Threshold Rule', () => {
afterEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -260,6 +268,7 @@ describe('Metrics Rule', () => {
const url = getMetricsViewInAppUrl({
fields,
assetDetailsLocator: mockAssetDetailsLocator,
metricsExplorerLocator: mockMetricsExplorerLocator,
groupBy: ['host.name'],
});
expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1);
Expand All @@ -276,6 +285,7 @@ describe('Metrics Rule', () => {
const url = getMetricsViewInAppUrl({
fields,
assetDetailsLocator: mockAssetDetailsLocator,
metricsExplorerLocator: mockMetricsExplorerLocator,
groupBy: ['container.id'],
});
expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1);
Expand All @@ -292,17 +302,24 @@ describe('Metrics Rule', () => {
const url = getMetricsViewInAppUrl({
fields,
assetDetailsLocator: mockAssetDetailsLocator,
metricsExplorerLocator: mockMetricsExplorerLocator,
groupBy: ['kubernetes.pod.name'],
});
expect(url).toEqual('/app/metrics/explorer');
expect(mockMetricsExplorerLocator.getRedirectUrl).toHaveBeenCalledTimes(1);
expect(url).toEqual('/metrics-mock');
});

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 });
expect(url).toEqual('/app/metrics/explorer');
const url = getMetricsViewInAppUrl({
fields,
assetDetailsLocator: mockAssetDetailsLocator,
metricsExplorerLocator: mockMetricsExplorerLocator,
});
expect(mockMetricsExplorerLocator.getRedirectUrl).toHaveBeenCalledTimes(1);
expect(url).toEqual('/metrics-mock');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_te
import { type InventoryItemType, findInventoryModel } from '@kbn/metrics-data-access-plugin/common';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import {
MetricsExplorerLocatorParams,
type AssetDetailsLocatorParams,
type InventoryLocatorParams,
} from '@kbn/observability-shared-plugin/common';
import { castArray } from 'lodash';
import { fifteenMinutesInMilliseconds, METRICS_EXPLORER_URL } from '../../constants';
import { fifteenMinutesInMilliseconds } from '../../constants';
import { SupportedAssetTypes } from '../../asset_details/types';

const ALERT_RULE_PARAMTERS_INVENTORY_METRIC_ID = `${ALERT_RULE_PARAMETERS}.criteria.metric`;
Expand Down Expand Up @@ -48,7 +49,7 @@ export const getInventoryViewInAppUrl = ({
inventoryLocator?: LocatorPublic<InventoryLocatorParams>;
}): string => {
if (!assetDetailsLocator || !inventoryLocator) {
return '';
throw new Error('Locators for Asset Details and Inventory are required');
}

/* Temporary Solution -> https://github.com/elastic/kibana/issues/137033
Expand Down Expand Up @@ -127,13 +128,19 @@ export const getMetricsViewInAppUrl = ({
fields,
groupBy,
assetDetailsLocator,
metricsExplorerLocator,
}: {
fields: ParsedTechnicalFields & Record<string, any>;
groupBy?: string[];
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
metricsExplorerLocator?: LocatorPublic<MetricsExplorerLocatorParams>;
}) => {
if (!groupBy || !assetDetailsLocator) {
return METRICS_EXPLORER_URL;
if (!assetDetailsLocator || !metricsExplorerLocator) {
throw new Error('Locators for Asset Details and Metrics Explorer are required');
}

if (!groupBy) {
return metricsExplorerLocator.getRedirectUrl({});
}

// creates an object of asset details supported assetType by their assetId field name
Expand All @@ -143,12 +150,19 @@ export const getMetricsViewInAppUrl = ({
}, {} as Record<string, InventoryItemType>);

// detemines if the groupBy has a field that the asset details supports
const supportedAssetId = groupBy?.find((field) => !!assetTypeByAssetId[field]);
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];

// A supported asset type can still return no id. In such a case, we can't
// generate a valid link, so we redirect to Metrics Explorer.
if (!assetId) {
return metricsExplorerLocator.getRedirectUrl({});
}

const timestamp = fields[TIMESTAMP];

return getLinkToAssetDetails({
Expand All @@ -158,7 +172,7 @@ export const getMetricsViewInAppUrl = ({
assetDetailsLocator,
});
} else {
return METRICS_EXPLORER_URL;
return metricsExplorerLocator.getRedirectUrl({});
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export const O11Y_AAD_FIELDS = [
'tags',
];

export const METRICS_EXPLORER_URL = '/app/metrics/explorer';
export const fifteenMinutesInMilliseconds = 15 * 60 * 1000;

export const DEFAULT_METRICS_VIEW_ATTRIBUTES = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ 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 {
AssetDetailsLocatorParams,
MetricsExplorerLocatorParams,
} from '@kbn/observability-shared-plugin/common';
import {
MetricExpressionParams,
METRIC_THRESHOLD_ALERT_TYPE_ID,
Expand Down Expand Up @@ -54,8 +57,10 @@ const metricThresholdDefaultRecoveryMessage = i18n.translate(

export function createMetricThresholdRuleType({
assetDetailsLocator,
metricsExplorerLocator,
}: {
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
metricsExplorerLocator?: LocatorPublic<MetricsExplorerLocatorParams>;
}): ObservabilityRuleTypeModel<MetricThresholdRuleTypeParams> {
return {
id: METRIC_THRESHOLD_ALERT_TYPE_ID,
Expand All @@ -71,7 +76,7 @@ export function createMetricThresholdRuleType({
defaultActionMessage: metricThresholdDefaultActionMessage,
defaultRecoveryMessage: metricThresholdDefaultRecoveryMessage,
requiresAppContext: false,
format: getRuleFormat({ assetDetailsLocator }),
format: getRuleFormat({ assetDetailsLocator, metricsExplorerLocator }),
alertDetailsAppSection: lazy(() => import('./components/alert_details_app_section')),
priority: 10,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@
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 type {
AssetDetailsLocatorParams,
MetricsExplorerLocatorParams,
} 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 getRuleFormat = ({
assetDetailsLocator,
metricsExplorerLocator,
}: {
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
metricsExplorerLocator?: LocatorPublic<MetricsExplorerLocatorParams>;
}): ObservabilityRuleTypeFormatter => {
return ({ fields }) => {
const reason = fields[ALERT_REASON] ?? '-';
Expand All @@ -26,12 +30,13 @@ export const getRuleFormat = ({
fields,
groupBy: castArray<string>(parameters?.groupBy as string[] | string),
assetDetailsLocator,
metricsExplorerLocator,
});

return {
reason,
link,
hasBasePath: link !== METRICS_EXPLORER_URL,
hasBasePath: true,
};
};
};
12 changes: 10 additions & 2 deletions x-pack/plugins/observability_solution/infra/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
} from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { enableInfrastructureHostsView } from '@kbn/observability-plugin/public';
import { ObservabilityTriggerId } from '@kbn/observability-shared-plugin/common';
import {
METRICS_EXPLORER_LOCATOR_ID,
MetricsExplorerLocatorParams,
ObservabilityTriggerId,
} from '@kbn/observability-shared-plugin/common';
import { BehaviorSubject, combineLatest, from } from 'rxjs';
import { map } from 'rxjs';
import type { EmbeddableApiContext } from '@kbn/presentation-publishing';
Expand Down Expand Up @@ -90,13 +94,17 @@ export class Plugin implements InfraClientPluginClass {
pluginsSetup.share.url.locators.get<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
const inventoryLocator =
pluginsSetup.share.url.locators.get<InventoryLocatorParams>(INVENTORY_LOCATOR_ID);
const metricsExplorerLocator =
pluginsSetup.share.url.locators.get<MetricsExplorerLocatorParams>(
METRICS_EXPLORER_LOCATOR_ID
);

pluginsSetup.observability.observabilityRuleTypeRegistry.register(
createInventoryMetricRuleType({ assetDetailsLocator, inventoryLocator })
);

pluginsSetup.observability.observabilityRuleTypeRegistry.register(
createMetricThresholdRuleType({ assetDetailsLocator })
createMetricThresholdRuleType({ assetDetailsLocator, metricsExplorerLocator })
);

if (this.config.featureFlags.logsUIEnabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { flattenObject } from './utils';
describe('FlattenObject', () => {
it('flattens multi level item', () => {
const data = {
key0: 'value',
key1: {
item1: 'value 1',
item2: { itemA: 'value 2' },
Expand All @@ -22,6 +23,7 @@ describe('FlattenObject', () => {

const flatten = flattenObject(data);
expect(flatten).toEqual({
key0: 'value',
'key2.item3.itemA.itemAB': 'value AB',
'key2.item4': 'value 4',
'key1.item1': 'value 1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
import { isEmpty, isError } from 'lodash';
import { schema } from '@kbn/config-schema';
import { Logger, LogMeta } from '@kbn/logging';
import type { ElasticsearchClient, IBasePath } from '@kbn/core/server';
import type { ElasticsearchClient } from '@kbn/core/server';
import { ObservabilityConfig } from '@kbn/observability-plugin/server';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils';
import {
ParsedTechnicalFields,
Expand All @@ -25,6 +24,7 @@ import type { LocatorPublic } from '@kbn/share-plugin/common';
import type {
AssetDetailsLocatorParams,
InventoryLocatorParams,
MetricsExplorerLocatorParams,
} from '@kbn/observability-shared-plugin/common';
import {
ALERT_RULE_PARAMETERS_NODE_TYPE,
Expand Down Expand Up @@ -130,19 +130,15 @@ export const getAlertDetailsPageEnabledForApp = (
};

export const getInventoryViewInAppUrlWithSpaceId = ({
basePath,
criteria,
nodeType,
spaceId,
timestamp,
hostName,
assetDetailsLocator,
inventoryLocator,
}: {
basePath: IBasePath;
criteria: InventoryMetricConditions[];
nodeType: string;
spaceId: string;
timestamp: string;
hostName?: string;
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
Expand All @@ -160,43 +156,37 @@ export const getInventoryViewInAppUrlWithSpaceId = ({
[HOST_NAME]: hostName,
};

return addSpaceIdToPath(
basePath.publicBaseUrl,
spaceId,
getInventoryViewInAppUrl({
fields: parseTechnicalFields(fields, true),
assetDetailsLocator,
inventoryLocator,
})
);
return getInventoryViewInAppUrl({
fields: parseTechnicalFields(fields, true),
assetDetailsLocator,
inventoryLocator,
});
};

export const getMetricsViewInAppUrlWithSpaceId = ({
basePath,
spaceId,
timestamp,
groupBy,
assetDetailsLocator,
metricsExplorerLocator,
additionalContext,
}: {
basePath: IBasePath;
spaceId: string;
timestamp: string;
groupBy?: string[];
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
metricsExplorerLocator?: LocatorPublic<MetricsExplorerLocatorParams>;
additionalContext?: AdditionalContext;
}) => {
const fields = {
...flattenAdditionalContext(additionalContext),
[TIMESTAMP]: timestamp,
};

return addSpaceIdToPath(
basePath.publicBaseUrl,
spaceId,
getMetricsViewInAppUrl({
fields: parseTechnicalFields(fields, true),
groupBy,
assetDetailsLocator,
})
);
return getMetricsViewInAppUrl({
fields: parseTechnicalFields(fields, true),
groupBy,
assetDetailsLocator,
metricsExplorerLocator,
});
};

export const KUBERNETES_POD_UID = 'kubernetes.pod.uid';
Expand Down
Loading

0 comments on commit 1a92a4b

Please sign in to comment.