mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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
This commit is contained in:
parent
29c5381935
commit
d69e598e30
39 changed files with 776 additions and 223 deletions
|
@ -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(
|
||||
|
@ -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×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<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×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<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'))"
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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×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<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×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<string, any>;
|
||||
const url = getMetricsViewInAppUrl(fields);
|
||||
const url = getMetricsViewInAppUrl({
|
||||
fields,
|
||||
assetDetailsLocator: mockAssetDetailsLocator,
|
||||
groupBy: ['host.name'],
|
||||
});
|
||||
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=(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(
|
||||
"/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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<string, unknown[]> => {
|
||||
return Object.entries(params).reduce((acc, [key, field]) => {
|
||||
|
@ -32,10 +38,18 @@ export const flatAlertRuleParams = (params: {}, pKey = ''): Record<string, unkno
|
|||
}, {} as Record<string, unknown[]>);
|
||||
};
|
||||
|
||||
export const getInventoryViewInAppUrl = (
|
||||
fields: ParsedTechnicalFields & Record<string, any>
|
||||
): string => {
|
||||
let inventoryFields = fields;
|
||||
export const getInventoryViewInAppUrl = ({
|
||||
fields,
|
||||
assetDetailsLocator,
|
||||
inventoryLocator,
|
||||
}: {
|
||||
fields: ParsedTechnicalFields & Record<string, any>;
|
||||
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
|
||||
inventoryLocator?: LocatorPublic<InventoryLocatorParams>;
|
||||
}): 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,
|
||||
});
|
||||
}
|
||||
|
||||
return LINK_TO_INVENTORY;
|
||||
};
|
||||
|
||||
export const getMetricsViewInAppUrl = (fields: ParsedTechnicalFields & Record<string, any>) => {
|
||||
const hostName = fields[HOST_NAME_FIELD];
|
||||
const timestamp = fields[TIMESTAMP];
|
||||
|
||||
return hostName ? getLinkToHostDetails({ hostName, timestamp }) : METRICS_EXPLORER_URL;
|
||||
};
|
||||
|
||||
export function getLinkToHostDetails({
|
||||
hostName,
|
||||
timestamp,
|
||||
}: {
|
||||
hostName: string;
|
||||
timestamp: string;
|
||||
}): string {
|
||||
const queryParams = {
|
||||
from: Date.parse(timestamp),
|
||||
to: Date.parse(timestamp) + fifteenMinutesInMilliseconds,
|
||||
const linkToParams = {
|
||||
nodeType,
|
||||
timestamp: Date.parse(inventoryFields[TIMESTAMP]),
|
||||
customMetric: '',
|
||||
metric: '',
|
||||
};
|
||||
|
||||
const encodedParams = encode(stringify(queryParams));
|
||||
// We always pick the first criteria metric for the URL
|
||||
|
||||
return `/app/metrics/link-to/host-detail/${hostName}?${encodedParams}`;
|
||||
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 inventoryLocator.getRedirectUrl({
|
||||
...linkToParams,
|
||||
});
|
||||
};
|
||||
|
||||
export const getMetricsViewInAppUrl = ({
|
||||
fields,
|
||||
groupBy,
|
||||
assetDetailsLocator,
|
||||
}: {
|
||||
fields: ParsedTechnicalFields & Record<string, any>;
|
||||
groupBy?: string[];
|
||||
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
|
||||
}) => {
|
||||
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<string, InventoryItemType>);
|
||||
|
||||
// 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 getLinkToAssetDetails({
|
||||
assetId,
|
||||
assetType: supportedAssetType,
|
||||
timestamp,
|
||||
assetDetailsLocator,
|
||||
});
|
||||
} else {
|
||||
return METRICS_EXPLORER_URL;
|
||||
}
|
||||
};
|
||||
|
||||
function getLinkToAssetDetails({
|
||||
assetId,
|
||||
assetType,
|
||||
timestamp,
|
||||
alertMetric,
|
||||
assetDetailsLocator,
|
||||
}: {
|
||||
assetId: string;
|
||||
assetType: InventoryItemType;
|
||||
timestamp: string;
|
||||
alertMetric?: string;
|
||||
assetDetailsLocator: LocatorPublic<AssetDetailsLocatorParams>;
|
||||
}): string {
|
||||
return assetDetailsLocator.getRedirectUrl({
|
||||
assetId,
|
||||
assetType,
|
||||
assetDetails: {
|
||||
dateRange: {
|
||||
from: timestamp,
|
||||
to: moment(timestamp).add(fifteenMinutesInMilliseconds, 'ms').toISOString(),
|
||||
},
|
||||
...(alertMetric && alertMetric !== CUSTOM_METRIC_TYPE ? { alertMetric } : undefined),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<InventoryMetricRuleTypeParams> {
|
||||
export function createInventoryMetricRuleType({
|
||||
assetDetailsLocator,
|
||||
inventoryLocator,
|
||||
}: {
|
||||
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
|
||||
inventoryLocator?: LocatorPublic<InventoryLocatorParams>;
|
||||
}): ObservabilityRuleTypeModel<InventoryMetricRuleTypeParams> {
|
||||
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<Inve
|
|||
defaultActionMessage: inventoryDefaultActionMessage,
|
||||
defaultRecoveryMessage: inventoryDefaultRecoveryMessage,
|
||||
requiresAppContext: false,
|
||||
format: formatReason,
|
||||
format,
|
||||
priority: 20,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,13 +7,27 @@
|
|||
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { ObservabilityRuleTypeFormatter } from '@kbn/observability-plugin/public';
|
||||
import type { LocatorPublic } from '@kbn/share-plugin/common';
|
||||
import type {
|
||||
AssetDetailsLocatorParams,
|
||||
InventoryLocatorParams,
|
||||
} from '@kbn/observability-shared-plugin/common';
|
||||
import { getInventoryViewInAppUrl } from '../../../common/alerting/metrics/alert_link';
|
||||
|
||||
export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => {
|
||||
const reason = fields[ALERT_REASON] ?? '-';
|
||||
export const getRuleFormat = ({
|
||||
assetDetailsLocator,
|
||||
inventoryLocator,
|
||||
}: {
|
||||
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
|
||||
inventoryLocator?: LocatorPublic<InventoryLocatorParams>;
|
||||
}): ObservabilityRuleTypeFormatter => {
|
||||
return ({ fields }) => {
|
||||
const reason = fields[ALERT_REASON] ?? '-';
|
||||
|
||||
return {
|
||||
reason,
|
||||
link: getInventoryViewInAppUrl(fields),
|
||||
return {
|
||||
reason,
|
||||
link: getInventoryViewInAppUrl({ fields, assetDetailsLocator, inventoryLocator }),
|
||||
hasBasePath: true,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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<MetricThresholdRuleTypeParams> {
|
||||
export function createMetricThresholdRuleType({
|
||||
assetDetailsLocator,
|
||||
}: {
|
||||
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
|
||||
}): ObservabilityRuleTypeModel<MetricThresholdRuleTypeParams> {
|
||||
return {
|
||||
id: METRIC_THRESHOLD_ALERT_TYPE_ID,
|
||||
description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', {
|
||||
|
@ -65,7 +71,7 @@ export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel<Metr
|
|||
defaultActionMessage: metricThresholdDefaultActionMessage,
|
||||
defaultRecoveryMessage: metricThresholdDefaultRecoveryMessage,
|
||||
requiresAppContext: false,
|
||||
format: formatReason,
|
||||
format: getRuleFormat({ assetDetailsLocator }),
|
||||
alertDetailsAppSection: lazy(() => import('./components/alert_details_app_section')),
|
||||
priority: 10,
|
||||
};
|
||||
|
|
|
@ -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<AssetDetailsLocatorParams>;
|
||||
}): ObservabilityRuleTypeFormatter => {
|
||||
return ({ fields }) => {
|
||||
const reason = fields[ALERT_REASON] ?? '-';
|
||||
const parameters = fields[ALERT_RULE_PARAMETERS];
|
||||
|
||||
const link = getMetricsViewInAppUrl({
|
||||
fields,
|
||||
groupBy: castArray<string>(parameters?.groupBy as string[] | string),
|
||||
assetDetailsLocator,
|
||||
});
|
||||
|
||||
return {
|
||||
reason,
|
||||
link,
|
||||
hasBasePath: link !== METRICS_EXPLORER_URL,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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 = <T extends InventoryItemType>(
|
||||
inventoryModel: InventoryModels<T>,
|
||||
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 (
|
||||
<LegacyAlertMetricCallout
|
||||
visibleFor={INCOMING_ALERT_CALLOUT_VISIBLE_FOR}
|
||||
metric={alertMetric}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -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<boolean>(
|
||||
`${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 (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="warning"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.assetDetails.callouts.legacyMetricAlertCallout.title"
|
||||
defaultMessage="We have an updated definition for {metric}"
|
||||
values={{
|
||||
metric: metricLabel.text,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
data-test-subj="infraAssetDetailsLegacyMetricAlertCallout"
|
||||
onDismiss={onDismiss}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.assetDetails.callouts.legacyMetricAlertCallout"
|
||||
defaultMessage="The alert you have clicked through is using the legacy {metric}. {learnMoreLink}"
|
||||
values={{
|
||||
metric: metricLabel.text,
|
||||
learnMoreLink: (
|
||||
<EuiLink
|
||||
data-test-subj="infraAssetDetailsLegacyMetricAlertCalloutLink"
|
||||
href={HOST_METRICS_DOC_HREF}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.assetDetails.callouts.legacyMetricAlertCallout.learnMoreLinkLabel"
|
||||
defaultMessage="Learn More"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -22,22 +22,30 @@ import {
|
|||
Profiling,
|
||||
} from '../tabs';
|
||||
import { ContentTabIds } from '../types';
|
||||
import { Callouts } from './callouts';
|
||||
|
||||
export const Content = () => {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper
|
||||
visibleFor={[
|
||||
ContentTabIds.OVERVIEW,
|
||||
ContentTabIds.LOGS,
|
||||
ContentTabIds.METADATA,
|
||||
ContentTabIds.METRICS,
|
||||
ContentTabIds.PROCESSES,
|
||||
ContentTabIds.ANOMALIES,
|
||||
ContentTabIds.DASHBOARDS,
|
||||
]}
|
||||
/>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<Callouts />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper
|
||||
visibleFor={[
|
||||
ContentTabIds.OVERVIEW,
|
||||
ContentTabIds.LOGS,
|
||||
ContentTabIds.METADATA,
|
||||
ContentTabIds.METRICS,
|
||||
ContentTabIds.PROCESSES,
|
||||
ContentTabIds.ANOMALIES,
|
||||
ContentTabIds.DASHBOARDS,
|
||||
]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TabPanel activeWhen={ContentTabIds.ANOMALIES}>
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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: (
|
||||
|
|
|
@ -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 ? <EuiLoadingSpinner size="m" /> : 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 ? (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
css={css`
|
||||
height: calc(100vh - var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0)));
|
||||
`}
|
||||
>
|
||||
<InfraLoadingPanel
|
||||
height="100%"
|
||||
width="auto"
|
||||
text={i18n.translate('xpack.infra.waffle.loadingDataText', {
|
||||
defaultMessage: 'Loading data',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<Content />
|
||||
)}
|
||||
<Content />
|
||||
</InfraPageTemplate>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
|
||||
|
||||
const { error, name } = useHostIpToName(hostIp, (metricsView && metricsView.indices) || null);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
|
||||
const inventoryLocator =
|
||||
pluginsSetup.share.url.locators.get<InventoryLocatorParams>(INVENTORY_LOCATOR_ID);
|
||||
|
||||
pluginsSetup.observability.observabilityRuleTypeRegistry.register(
|
||||
createInventoryMetricRuleType()
|
||||
createInventoryMetricRuleType({ assetDetailsLocator, inventoryLocator })
|
||||
);
|
||||
|
||||
pluginsSetup.observability.observabilityRuleTypeRegistry.register(
|
||||
createMetricThresholdRuleType()
|
||||
createMetricThresholdRuleType({ assetDetailsLocator })
|
||||
);
|
||||
|
||||
if (this.config.featureFlags.logsUIEnabled) {
|
||||
|
|
|
@ -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<AssetDetailsLocatorParams>;
|
||||
inventoryLocator?: LocatorPublic<InventoryLocatorParams>;
|
||||
}) => {
|
||||
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<AssetDetailsLocatorParams>;
|
||||
}) => {
|
||||
const fields = {
|
||||
[TIMESTAMP]: timestamp,
|
||||
[HOST_NAME]: hostName,
|
||||
};
|
||||
|
||||
return addSpaceIdToPath(
|
||||
basePath.publicBaseUrl,
|
||||
spaceId,
|
||||
getMetricsViewInAppUrl(parseTechnicalFields(fields, true))
|
||||
getMetricsViewInAppUrl({
|
||||
fields: parseTechnicalFields(fields, true),
|
||||
groupBy,
|
||||
assetDetailsLocator,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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<string, AlertTestInstance>();
|
||||
|
|
|
@ -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<AlertsLocatorParams>(alertsLocatorID);
|
||||
const assetDetailsLocator =
|
||||
share.setup.url.locators.get<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
|
||||
const inventoryLocator =
|
||||
share.setup.url.locators.get<InventoryLocatorParams>(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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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<AlertsLocatorParams>(alertsLocatorID);
|
||||
const assetDetailsLocator =
|
||||
share.setup.url.locators.get<AssetDetailsLocatorParams>(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<string>(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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<InfraServerPluginSetupDeps>[key];
|
||||
} & (key extends keyof InfraServerPluginStartDeps
|
||||
? {
|
||||
start: () => Promise<Required<InfraServerPluginStartDeps>[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<AlertsLocatorParams>;
|
||||
apmDataAccess: ApmDataAccessPluginSetup;
|
||||
plugins: Plugins;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -11,6 +11,7 @@ export {
|
|||
getFieldByType,
|
||||
findInventoryFields,
|
||||
metrics,
|
||||
type InventoryModels,
|
||||
} from './inventory_models';
|
||||
|
||||
export { podSnapshotMetricTypes } from './inventory_models/kubernetes/pod';
|
||||
|
|
|
@ -56,4 +56,5 @@ export const host: InventoryModel<typeof metrics> = {
|
|||
...nginxRequireMetrics,
|
||||
],
|
||||
tooltipMetrics: ['cpuV2', 'memory', 'txV2', 'rxV2', 'cpu', 'tx', 'rx'],
|
||||
legacyMetrics: ['cpu', 'tx', 'rx'],
|
||||
};
|
||||
|
|
|
@ -29,7 +29,7 @@ const catalog = {
|
|||
|
||||
export const inventoryModels = Object.values(catalog);
|
||||
|
||||
type InventoryModels<T extends InventoryItemType> = (typeof catalog)[T];
|
||||
export type InventoryModels<T extends InventoryItemType> = (typeof catalog)[T];
|
||||
|
||||
export const findInventoryModel = <T extends InventoryItemType>(type: T): InventoryModels<T> => {
|
||||
const model = inventoryModels.find((m) => m.id === type);
|
||||
|
|
|
@ -423,6 +423,7 @@ export interface InventoryModel<TMetrics = InventoryMetrics> {
|
|||
};
|
||||
metrics: TMetrics;
|
||||
requiredMetrics: InventoryMetric[];
|
||||
legacyMetrics?: SnapshotMetricType[];
|
||||
tooltipMetrics: SnapshotMetricType[];
|
||||
nodeFilter?: object[];
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -13,7 +13,7 @@ export type AssetDetailsLocator = LocatorPublic<AssetDetailsLocatorParams>;
|
|||
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<AssetDetailsLocatorParams> {
|
||||
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 : {},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Record<string, string>>((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),
|
||||
|
|
|
@ -352,5 +352,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) {
|
|||
|
||||
return testSubjects.click(buttonSubject);
|
||||
},
|
||||
|
||||
// Callouts
|
||||
async legacyMetricAlertCalloutExists() {
|
||||
return testSubjects.exists('infraAssetDetailsLegacyMetricAlertCallout');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue