From 31f79b386cc38e00c06620406a93fc498de2a953 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 22 Oct 2024 14:05:58 +0200 Subject: [PATCH 01/17] Redirect ECS k8s entities to dashboards --- .../entities_grid/entity_name/index.tsx | 74 +++++---------- .../entity_name/use_detail_view_redirect.ts | 93 +++++++++++++++++++ .../observability_shared/common/index.ts | 1 + .../locators/apm/service_overview_locator.ts | 3 +- 4 files changed, 119 insertions(+), 52 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/use_detail_view_redirect.ts diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index 982a616da8fda..ae14647492c5c 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -6,19 +6,11 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; -import { - ASSET_DETAILS_LOCATOR_ID, - AssetDetailsLocatorParams, - ENTITY_DISPLAY_NAME, - ENTITY_IDENTITY_FIELDS, - ENTITY_TYPE, - SERVICE_ENVIRONMENT, - ServiceOverviewParams, -} from '@kbn/observability-shared-plugin/common'; -import React, { useCallback } from 'react'; -import { Entity } from '../../../../common/entities'; -import { useKibana } from '../../../hooks/use_kibana'; +import { ENTITY_DISPLAY_NAME } from '@kbn/observability-shared-plugin/common'; +import React from 'react'; +import type { Entity } from '../../../../common/entities'; import { EntityIcon } from '../../entity_icon'; +import { useDetailViewRedirect } from './use_detail_view_redirect'; interface EntityNameProps { entity: Entity; @@ -26,14 +18,12 @@ interface EntityNameProps { export function EntityName({ entity }: EntityNameProps) { const { - services: { telemetry, share }, + services: { telemetry }, } = useKibana(); - const assetDetailsLocator = - share?.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const { getEntityRedirectUrl } = useDetailViewRedirect(); - const serviceOverviewLocator = - share?.url.locators.get('serviceOverviewLocator'); + const href = getEntityRedirectUrl(entity); const handleLinkClick = useCallback(() => { telemetry.reportEntityViewClicked({ @@ -42,47 +32,29 @@ export function EntityName({ entity }: EntityNameProps) { }); }, [entity, telemetry]); - const getEntityRedirectUrl = useCallback(() => { - const type = entity[ENTITY_TYPE]; - // For service, host and container type there is only one identity field - const identityField = Array.isArray(entity[ENTITY_IDENTITY_FIELDS]) - ? entity[ENTITY_IDENTITY_FIELDS][0] - : entity[ENTITY_IDENTITY_FIELDS]; - const identityValue = entity[identityField]; - - switch (type) { - case 'host': - case 'container': - return assetDetailsLocator?.getRedirectUrl({ - assetId: identityValue, - assetType: type, - }); - - case 'service': - return serviceOverviewLocator?.getRedirectUrl({ - serviceName: identityValue, - environment: [entity[SERVICE_ENVIRONMENT] || undefined].flat()[0], - }); - } - }, [entity, assetDetailsLocator, serviceOverviewLocator]); + const entityName = ( + + + + + + + {entity[ENTITY_DISPLAY_NAME]} + + + + ); - return ( + return href ? ( // eslint-disable-next-line @elastic/eui/href-or-on-click - - - - - - - {entity[ENTITY_DISPLAY_NAME]} - - - + {entityName} + ) : ( + entityName ); } diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/use_detail_view_redirect.ts new file mode 100644 index 0000000000000..ba5d533590601 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/use_detail_view_redirect.ts @@ -0,0 +1,93 @@ +/* + * 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 { + ASSET_DETAILS_LOCATOR_ID, + AssetDetailsLocatorParams, + ENTITY_IDENTITY_FIELDS, + ENTITY_TYPE, + SERVICE_ENVIRONMENT, + SERVICE_OVERVIEW_LOCATOR_ID, + ServiceOverviewParams, +} from '@kbn/observability-shared-plugin/common'; +import { useCallback } from 'react'; +import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; +import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; +import { Entity } from '../../../../common/entities'; +import { useKibana } from '../../../hooks/use_kibana'; + +const KUBERNETES_DASHBOARDS: Record = { + kubernetes_cluster_ecs: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', + kubernetes_cron_job_ecs: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', + kubernetes_daemon_set_ecs: 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', + kubernetes_deployment_ecs: 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', + kubernetes_job_ecs: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', + kubernetes_node_ecs: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', + kubernetes_pod_ecs: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', + kubernetes_stateful_set_ecs: 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', +}; + +export const useDetailViewRedirect = () => { + const { services } = useKibana(); + + const assetDetailsLocator = + services.share?.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const dashboardLocator = + services.share?.url.locators.get(DASHBOARD_APP_LOCATOR); + const serviceOverviewLocator = services.share?.url.locators.get( + SERVICE_OVERVIEW_LOCATOR_ID + ); + + const getIdentityValue = (entity: Entity) => { + const identityField = Array.isArray(entity[ENTITY_IDENTITY_FIELDS]) + ? entity[ENTITY_IDENTITY_FIELDS][0] + : entity[ENTITY_IDENTITY_FIELDS]; + return entity[identityField]; + }; + + const getDetailViewRedirectUrl = useCallback( + (entity: Entity) => { + const type = entity[ENTITY_TYPE]; + const identityValue = getIdentityValue(entity); + + switch (type) { + case 'host': + case 'container': + return assetDetailsLocator?.getRedirectUrl({ + assetId: identityValue, + assetType: type, + }); + + case 'service': + return serviceOverviewLocator?.getRedirectUrl({ + serviceName: identityValue, + environment: [entity[SERVICE_ENVIRONMENT] || undefined].flat()[0], + }); + + default: + return undefined; + } + }, + [assetDetailsLocator, serviceOverviewLocator] + ); + + const getDashboardRedirectUrl = useCallback( + (entity: Entity) => { + const type = entity[ENTITY_TYPE]; + const dashboardId = KUBERNETES_DASHBOARDS[type]; + + return dashboardId ? dashboardLocator?.getRedirectUrl({ dashboardId }) : undefined; + }, + [dashboardLocator] + ); + + const getEntityRedirectUrl = useCallback( + (entity: Entity) => getDetailViewRedirectUrl(entity) ?? getDashboardRedirectUrl(entity), + [getDashboardRedirectUrl, getDetailViewRedirectUrl] + ); + + return { getEntityRedirectUrl }; +}; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/index.ts index e9be61e8fde34..b673b5d12e862 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/index.ts @@ -193,6 +193,7 @@ export type { export { ServiceOverviewLocatorDefinition, + SERVICE_OVERVIEW_LOCATOR_ID, TransactionDetailsByNameLocatorDefinition, ASSET_DETAILS_FLYOUT_LOCATOR_ID, AssetDetailsFlyoutLocatorDefinition, diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/service_overview_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/service_overview_locator.ts index 2a4e8aac330ec..e216640f31b4f 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/service_overview_locator.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/service_overview_locator.ts @@ -16,9 +16,10 @@ export interface ServiceOverviewParams extends SerializableRecord { } export type ServiceOverviewLocator = LocatorPublic; +export const SERVICE_OVERVIEW_LOCATOR_ID = 'serviceOverviewLocator'; export class ServiceOverviewLocatorDefinition implements LocatorDefinition { - public readonly id = 'serviceOverviewLocator'; + public readonly id = SERVICE_OVERVIEW_LOCATOR_ID; public readonly getLocation = async ({ rangeFrom, From e50ed57f105048bb537cb9e984db11edd885d137 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 23 Oct 2024 16:58:36 +0200 Subject: [PATCH 02/17] Create entity_client functions --- .../public/lib/entity_client.test.ts | 146 ++++++++++++++++++ .../public/lib/entity_client.ts | 39 +++++ .../inventory/common/entities.ts | 16 +- ...parse_identity_field_values_to_kql.test.ts | 91 ----------- .../parse_identity_field_values_to_kql.ts | 34 ---- .../common/utils/unflatten_entity.ts | 13 ++ .../components/alerts_badge/alerts_badge.tsx | 7 +- .../entities_grid/entity_name/index.tsx | 3 +- .../use_detail_view_redirect.ts | 66 ++++---- 9 files changed, 251 insertions(+), 164 deletions(-) create mode 100644 x-pack/plugins/entity_manager/public/lib/entity_client.test.ts delete mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts delete mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts create mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts rename x-pack/plugins/observability_solution/inventory/public/{components/entities_grid/entity_name => hooks}/use_detail_view_redirect.ts (56%) diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts new file mode 100644 index 0000000000000..47e9eebde4ce1 --- /dev/null +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts @@ -0,0 +1,146 @@ +/* + * 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 { EntityClient, EntityLatest } from './entity_client'; +import { coreMock } from '@kbn/core/public/mocks'; + +const commonEntityFields: EntityLatest = { + agent: { + name: 'node', + }, + entity: { + lastSeenTimestamp: '2023-10-09T00:00:00Z', + id: '1', + displayName: 'entity_name', + definitionId: 'entity_definition_id', + } as EntityLatest['entity'], +}; + +describe('EntityClient', () => { + let entityClient: EntityClient; + + beforeEach(() => { + entityClient = new EntityClient(coreMock.createStart()); + }); + + describe('asKqlFilter', () => { + it('should return the value when identityFields is a single string', () => { + const entityLatest: EntityLatest = { + ...commonEntityFields, + entity: { + ...commonEntityFields.entity, + identityFields: ['service.name', 'service.environment'], + type: 'service', + }, + service: { + name: 'my-service', + }, + }; + + const result = entityClient.asKqlFilter(entityLatest); + expect(result).toEqual('service.name: my-service'); + }); + + it('should return values when identityFields is an array of strings', () => { + const entityLatest: EntityLatest = { + ...commonEntityFields, + entity: { + ...commonEntityFields.entity, + identityFields: ['service.name', 'service.environment'], + type: 'service', + }, + service: { + name: 'my-service', + environment: 'staging', + }, + }; + + const result = entityClient.asKqlFilter(entityLatest); + expect(result).toEqual('(service.name: my-service AND service.environment: staging)'); + }); + + it('should throw an error when identity fields are missing', () => { + const entityLatest: EntityLatest = { + ...commonEntityFields, + }; + + expect(() => entityClient.asKqlFilter(entityLatest)).toThrow('Identity fields are missing'); + }); + + it('should ignore fields that are not present in the entity', () => { + const entityLatest: EntityLatest = { + ...commonEntityFields, + entity: { + ...commonEntityFields.entity, + identityFields: ['host.name', 'foo.bar'], + }, + host: { + name: 'my-host', + }, + }; + + const result = entityClient.asKqlFilter(entityLatest); + expect(result).toEqual('host.name: my-host'); + }); + }); + + describe('getIdentityFieldsValue', () => { + it('should return identity fields values', () => { + const serviceEntity: EntityLatest = { + ...commonEntityFields, + entity: { + ...commonEntityFields.entity, + identityFields: ['service.name', 'service.environment'], + type: 'service', + }, + service: { + name: 'my-service', + }, + }; + + expect(entityClient.getIdentityFieldsValue(serviceEntity)).toEqual({ + 'service.name': 'my-service', + }); + }); + + it('should return identity fields values when indentity field is an array of string', () => { + const serviceEntity: EntityLatest = { + ...commonEntityFields, + entity: { + ...commonEntityFields.entity, + identityFields: ['service.name', 'service.environment'], + type: 'service', + }, + service: { + name: 'my-service', + environment: 'staging', + }, + }; + + expect(entityClient.getIdentityFieldsValue(serviceEntity)).toEqual({ + 'service.name': 'my-service', + 'service.environment': 'staging', + }); + }); + + it('should return identity fields when field is in the root', () => { + const serviceEntity: EntityLatest = { + ...commonEntityFields, + entity: { + ...commonEntityFields.entity, + identityFields: ['name'], + type: 'service', + }, + name: 'foo', + }; + + expect(entityClient.getIdentityFieldsValue(serviceEntity)).toEqual({ + name: 'foo', + }); + }); + }); +}); diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.ts index dc22a0b991b0d..a9288c06b54f7 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { z } from '@kbn/zod'; import { CoreSetup, CoreStart } from '@kbn/core/public'; import { ClientRequestParamsOf, @@ -12,6 +13,8 @@ import { createRepositoryClient, isHttpFetchError, } from '@kbn/server-route-repository-client'; +import { KueryNode, nodeTypes, toKqlExpression } from '@kbn/es-query'; +import { entityLatestSchema } from '@kbn/entities-schema'; import { DisableManagedEntityResponse, EnableManagedEntityResponse, @@ -35,6 +38,8 @@ type CreateEntityDefinitionQuery = QueryParamOf< ClientRequestParamsOf >; +export type EntityLatest = z.infer; + export class EntityClient { public readonly repositoryClient: EntityManagerRepositoryClient['fetch']; @@ -83,4 +88,38 @@ export class EntityClient { throw err; } } + + asKqlFilter(entityLatest: EntityLatest) { + const identityFieldsValue = this.getIdentityFieldsValue(entityLatest); + + const nodes: KueryNode[] = Object.entries(identityFieldsValue).map(([identityField, value]) => + nodeTypes.function.buildNode('is', identityField, value) + ); + + if (nodes.length === 0) return ''; + + const kqlExpression = nodes.length > 1 ? nodeTypes.function.buildNode('and', nodes) : nodes[0]; + + return toKqlExpression(kqlExpression); + } + + getIdentityFieldsValue(entityLatest: EntityLatest) { + const { identityFields } = entityLatest.entity; + + if (!identityFields) { + throw new Error('Identity fields are missing'); + } + + return identityFields.reduce((acc, field) => { + const value = field.split('.').reduce((obj: any, part: string) => { + return obj && typeof obj === 'object' ? (obj as Record)[part] : undefined; + }, entityLatest); + + if (value) { + acc[field] = value; + } + + return acc; + }, {} as Record); + } } diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index d8a056074e339..3ae52ef8ff0c6 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema'; +import { z } from '@kbn/zod'; +import { entityLatestSchema } from '@kbn/entities-schema'; import { ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, @@ -29,10 +30,11 @@ export const defaultEntitySortField: EntityColumnIds = 'alertsCount'; export const MAX_NUMBER_OF_ENTITIES = 500; -export const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ - type: '*', - dataset: ENTITY_LATEST, -}); +export const ENTITIES_LATEST_ALIAS = '.entities.v1.latest*'; +// entitiesAliasPattern({ +// type: '*', +// dataset: ENTITY_LATEST, +// }); const entityArrayRt = t.array(t.string); export const entityTypesRt = new t.Type( @@ -67,3 +69,7 @@ export interface Entity { alertsCount?: number; [key: string]: any; } + +export type InventoryEntityLatest = z.infer & { + alertsCount?: number; +}; diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts deleted file mode 100644 index 8703e995b4446..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 { - ENTITY_DEFINITION_ID, - ENTITY_DISPLAY_NAME, - ENTITY_ID, - ENTITY_IDENTITY_FIELDS, - ENTITY_LAST_SEEN, -} from '@kbn/observability-shared-plugin/common'; -import type { Entity } from '../entities'; -import { parseIdentityFieldValuesToKql } from './parse_identity_field_values_to_kql'; - -const commonEntityFields = { - [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', - [ENTITY_ID]: '1', - [ENTITY_DISPLAY_NAME]: 'entity_name', - [ENTITY_DEFINITION_ID]: 'entity_definition_id', - alertCount: 3, -}; - -describe('parseIdentityFieldValuesToKql', () => { - it('should return the value when identityFields is a single string', () => { - const entity: Entity = { - 'agent.name': 'node', - [ENTITY_IDENTITY_FIELDS]: 'service.name', - 'service.name': 'my-service', - 'entity.type': 'service', - ...commonEntityFields, - }; - - const result = parseIdentityFieldValuesToKql({ entity }); - expect(result).toEqual('service.name: "my-service"'); - }); - - it('should return values when identityFields is an array of strings', () => { - const entity: Entity = { - 'agent.name': 'node', - [ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'], - 'service.name': 'my-service', - 'entity.type': 'service', - 'service.environment': 'staging', - ...commonEntityFields, - }; - - const result = parseIdentityFieldValuesToKql({ entity }); - expect(result).toEqual('service.name: "my-service" AND service.environment: "staging"'); - }); - - it('should return an empty string if identityFields is empty string', () => { - const entity: Entity = { - 'agent.name': 'node', - [ENTITY_IDENTITY_FIELDS]: '', - 'service.name': 'my-service', - 'entity.type': 'service', - ...commonEntityFields, - }; - - const result = parseIdentityFieldValuesToKql({ entity }); - expect(result).toEqual(''); - }); - it('should return an empty array if identityFields is empty array', () => { - const entity: Entity = { - 'agent.name': 'node', - [ENTITY_IDENTITY_FIELDS]: [], - 'service.name': 'my-service', - 'entity.type': 'service', - ...commonEntityFields, - }; - - const result = parseIdentityFieldValuesToKql({ entity }); - expect(result).toEqual(''); - }); - - it('should ignore fields that are not present in the entity', () => { - const entity: Entity = { - [ENTITY_IDENTITY_FIELDS]: ['host.name', 'foo.bar'], - 'host.name': 'my-host', - 'entity.type': 'host', - 'cloud.provider': null, - ...commonEntityFields, - }; - - const result = parseIdentityFieldValuesToKql({ entity }); - expect(result).toEqual('host.name: "my-host"'); - }); -}); diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts deleted file mode 100644 index 2e3f3dadd4109..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { ENTITY_IDENTITY_FIELDS } from '@kbn/observability-shared-plugin/common'; -import { Entity } from '../entities'; - -type Operator = 'AND'; -export function parseIdentityFieldValuesToKql({ - entity, - operator = 'AND', -}: { - entity: Entity; - operator?: Operator; -}) { - const mapping: string[] = []; - - const identityFields = entity[ENTITY_IDENTITY_FIELDS]; - - if (identityFields) { - const fields = [identityFields].flat(); - - fields.forEach((field) => { - if (field in entity) { - mapping.push(`${[field]}: "${entity[field as keyof Entity]}"`); - } - }); - } - - return mapping.join(` ${operator} `); -} diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts b/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts new file mode 100644 index 0000000000000..172da5d62cf49 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts @@ -0,0 +1,13 @@ +/* + * 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 { unflattenObject } from '@kbn/observability-utils/object/unflatten_object'; +import { Entity, InventoryEntityLatest } from '../entities'; + +export function unflattenEntity(entity: Entity) { + return unflattenObject(entity) as InventoryEntityLatest; +} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx index ba1b992ff62c1..a5845a7b42dcf 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx @@ -8,20 +8,21 @@ import React from 'react'; import rison from '@kbn/rison'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Entity } from '../../../common/entities'; +import type { Entity } from '../../../common/entities'; +import { unflattenEntity } from '../../../common/utils/unflatten_entity'; import { useKibana } from '../../hooks/use_kibana'; -import { parseIdentityFieldValuesToKql } from '../../../common/utils/parse_identity_field_values_to_kql'; export function AlertsBadge({ entity }: { entity: Entity }) { const { services: { http: { basePath }, + entityManager, }, } = useKibana(); const activeAlertsHref = basePath.prepend( `/app/observability/alerts?_a=${rison.encode({ - kuery: parseIdentityFieldValuesToKql({ entity }), + kuery: entityManager.entityClient.asKqlFilter(unflattenEntity(entity)), status: 'active', })}` ); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index ae14647492c5c..d9ccd6358e7c2 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -6,11 +6,10 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; -import { ENTITY_DISPLAY_NAME } from '@kbn/observability-shared-plugin/common'; import React from 'react'; import type { Entity } from '../../../../common/entities'; import { EntityIcon } from '../../entity_icon'; -import { useDetailViewRedirect } from './use_detail_view_redirect'; +import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; interface EntityNameProps { entity: Entity; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts similarity index 56% rename from x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/use_detail_view_redirect.ts rename to x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index ba5d533590601..ce1d0e5056e20 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -7,19 +7,18 @@ import { ASSET_DETAILS_LOCATOR_ID, AssetDetailsLocatorParams, - ENTITY_IDENTITY_FIELDS, - ENTITY_TYPE, - SERVICE_ENVIRONMENT, SERVICE_OVERVIEW_LOCATOR_ID, ServiceOverviewParams, } from '@kbn/observability-shared-plugin/common'; import { useCallback } from 'react'; import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; -import { Entity } from '../../../../common/entities'; -import { useKibana } from '../../../hooks/use_kibana'; +import { castArray } from 'lodash'; +import { Entity } from '../../common/entities'; +import { unflattenEntity } from '../../common/utils/unflatten_entity'; +import { useKibana } from './use_kibana'; -const KUBERNETES_DASHBOARDS: Record = { +const KUBERNETES_DASHBOARDS_IDS: Record = { kubernetes_cluster_ecs: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', kubernetes_cron_job_ecs: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', kubernetes_daemon_set_ecs: 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', @@ -31,57 +30,66 @@ const KUBERNETES_DASHBOARDS: Record = { }; export const useDetailViewRedirect = () => { - const { services } = useKibana(); + const { + services: { share, entityManager }, + } = useKibana(); - const assetDetailsLocator = - services.share?.url.locators.get(ASSET_DETAILS_LOCATOR_ID); - const dashboardLocator = - services.share?.url.locators.get(DASHBOARD_APP_LOCATOR); - const serviceOverviewLocator = services.share?.url.locators.get( - SERVICE_OVERVIEW_LOCATOR_ID - ); + const locators = share.url.locators; + const assetDetailsLocator = locators.get(ASSET_DETAILS_LOCATOR_ID); + const dashboardLocator = locators.get(DASHBOARD_APP_LOCATOR); + const serviceOverviewLocator = locators.get(SERVICE_OVERVIEW_LOCATOR_ID); - const getIdentityValue = (entity: Entity) => { - const identityField = Array.isArray(entity[ENTITY_IDENTITY_FIELDS]) - ? entity[ENTITY_IDENTITY_FIELDS][0] - : entity[ENTITY_IDENTITY_FIELDS]; - return entity[identityField]; - }; + const getIdentityValue = useCallback( + (entity: Entity) => { + const identityField = castArray(entity.entity.identityFields)[0]; + return entityManager.entityClient.getIdentityFieldsValue(unflattenEntity(entity))[ + identityField + ]; + }, + [entityManager.entityClient] + ); const getDetailViewRedirectUrl = useCallback( (entity: Entity) => { - const type = entity[ENTITY_TYPE]; const identityValue = getIdentityValue(entity); - switch (type) { + switch (entity.entity.type) { case 'host': case 'container': return assetDetailsLocator?.getRedirectUrl({ assetId: identityValue, - assetType: type, + assetType: entity.entity.type, }); case 'service': return serviceOverviewLocator?.getRedirectUrl({ serviceName: identityValue, - environment: [entity[SERVICE_ENVIRONMENT] || undefined].flat()[0], + environment: entity.service?.environment, }); default: return undefined; } }, - [assetDetailsLocator, serviceOverviewLocator] + [assetDetailsLocator, getIdentityValue, serviceOverviewLocator] ); const getDashboardRedirectUrl = useCallback( (entity: Entity) => { - const type = entity[ENTITY_TYPE]; - const dashboardId = KUBERNETES_DASHBOARDS[type]; + const type = entity.entity.type; + const dashboardId = KUBERNETES_DASHBOARDS_IDS[type]; - return dashboardId ? dashboardLocator?.getRedirectUrl({ dashboardId }) : undefined; + return dashboardId + ? dashboardLocator?.getRedirectUrl({ + dashboardId, + query: { + language: 'kuery', + query: entityManager.entityClient.asKqlFilter(unflattenEntity(entity)), + }, + }) + : undefined; }, - [dashboardLocator] + [dashboardLocator, entityManager.entityClient] ); const getEntityRedirectUrl = useCallback( From 023b5314faecb09284d208b5845948b2358dcffa Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 23 Oct 2024 17:00:39 +0200 Subject: [PATCH 03/17] Create entity_client functions --- .../public/lib/entity_client.test.ts | 9 ------ .../public/lib/entity_client.ts | 2 +- .../common/utils/unflatten_entity.ts | 2 +- .../public/hooks/use_detail_view_redirect.ts | 31 ++++++++++--------- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts index 47e9eebde4ce1..9f349e4a7649b 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts @@ -9,9 +9,6 @@ import { EntityClient, EntityLatest } from './entity_client'; import { coreMock } from '@kbn/core/public/mocks'; const commonEntityFields: EntityLatest = { - agent: { - name: 'node', - }, entity: { lastSeenTimestamp: '2023-10-09T00:00:00Z', id: '1', @@ -30,7 +27,6 @@ describe('EntityClient', () => { describe('asKqlFilter', () => { it('should return the value when identityFields is a single string', () => { const entityLatest: EntityLatest = { - ...commonEntityFields, entity: { ...commonEntityFields.entity, identityFields: ['service.name', 'service.environment'], @@ -47,7 +43,6 @@ describe('EntityClient', () => { it('should return values when identityFields is an array of strings', () => { const entityLatest: EntityLatest = { - ...commonEntityFields, entity: { ...commonEntityFields.entity, identityFields: ['service.name', 'service.environment'], @@ -73,7 +68,6 @@ describe('EntityClient', () => { it('should ignore fields that are not present in the entity', () => { const entityLatest: EntityLatest = { - ...commonEntityFields, entity: { ...commonEntityFields.entity, identityFields: ['host.name', 'foo.bar'], @@ -91,7 +85,6 @@ describe('EntityClient', () => { describe('getIdentityFieldsValue', () => { it('should return identity fields values', () => { const serviceEntity: EntityLatest = { - ...commonEntityFields, entity: { ...commonEntityFields.entity, identityFields: ['service.name', 'service.environment'], @@ -109,7 +102,6 @@ describe('EntityClient', () => { it('should return identity fields values when indentity field is an array of string', () => { const serviceEntity: EntityLatest = { - ...commonEntityFields, entity: { ...commonEntityFields.entity, identityFields: ['service.name', 'service.environment'], @@ -129,7 +121,6 @@ describe('EntityClient', () => { it('should return identity fields when field is in the root', () => { const serviceEntity: EntityLatest = { - ...commonEntityFields, entity: { ...commonEntityFields.entity, identityFields: ['name'], diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.ts index a9288c06b54f7..57c13d511eb35 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.ts @@ -13,7 +13,7 @@ import { createRepositoryClient, isHttpFetchError, } from '@kbn/server-route-repository-client'; -import { KueryNode, nodeTypes, toKqlExpression } from '@kbn/es-query'; +import { type KueryNode, nodeTypes, toKqlExpression } from '@kbn/es-query'; import { entityLatestSchema } from '@kbn/entities-schema'; import { DisableManagedEntityResponse, diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts b/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts index 172da5d62cf49..758d185a5753b 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts @@ -6,7 +6,7 @@ */ import { unflattenObject } from '@kbn/observability-utils/object/unflatten_object'; -import { Entity, InventoryEntityLatest } from '../entities'; +import type { Entity, InventoryEntityLatest } from '../entities'; export function unflattenEntity(entity: Entity) { return unflattenObject(entity) as InventoryEntityLatest; diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index ce1d0e5056e20..e841bac19ad02 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -7,14 +7,18 @@ import { ASSET_DETAILS_LOCATOR_ID, AssetDetailsLocatorParams, + CONTAINER_ID, + ENTITY_TYPE, + HOST_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, SERVICE_OVERVIEW_LOCATOR_ID, ServiceOverviewParams, } from '@kbn/observability-shared-plugin/common'; import { useCallback } from 'react'; import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; -import { castArray } from 'lodash'; -import { Entity } from '../../common/entities'; +import type { Entity } from '../../common/entities'; import { unflattenEntity } from '../../common/utils/unflatten_entity'; import { useKibana } from './use_kibana'; @@ -40,31 +44,30 @@ export const useDetailViewRedirect = () => { const serviceOverviewLocator = locators.get(SERVICE_OVERVIEW_LOCATOR_ID); const getIdentityValue = useCallback( - (entity: Entity) => { - const identityField = castArray(entity.entity.identityFields)[0]; - return entityManager.entityClient.getIdentityFieldsValue(unflattenEntity(entity))[ - identityField - ]; - }, + (entity: Entity) => entityManager.entityClient.getIdentityFieldsValue(unflattenEntity(entity)), [entityManager.entityClient] ); const getDetailViewRedirectUrl = useCallback( (entity: Entity) => { + const type = entity[ENTITY_TYPE]; const identityValue = getIdentityValue(entity); - switch (entity.entity.type) { + switch (type) { case 'host': + return assetDetailsLocator?.getRedirectUrl({ + assetId: identityValue[HOST_NAME], + assetType: type, + }); case 'container': return assetDetailsLocator?.getRedirectUrl({ - assetId: identityValue, - assetType: entity.entity.type, + assetId: identityValue[CONTAINER_ID], + assetType: type, }); - case 'service': return serviceOverviewLocator?.getRedirectUrl({ - serviceName: identityValue, - environment: entity.service?.environment, + serviceName: identityValue[SERVICE_NAME], + environment: identityValue[SERVICE_ENVIRONMENT], }); default: From f7c2dfc8770b91ba69c443564adc406cb06f47d3 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 23 Oct 2024 17:20:27 +0200 Subject: [PATCH 04/17] Fix entityLatest flatten object --- .../es/utils/esql_result_to_plain_objects.ts | 4 ++- .../public/lib/entity_client.ts | 3 +- .../inventory/common/entities.ts | 32 ++++++++++++++++++- .../public/hooks/use_detail_view_redirect.ts | 2 +- .../server/routes/has_data/get_has_data.ts | 1 - 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts index ad48bcb311b25..693efee838c93 100644 --- a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts +++ b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts @@ -13,7 +13,9 @@ export function esqlResultToPlainObjects>( return result.values.map((row) => { return row.reduce>((acc, value, index) => { const column = result.columns[index]; - acc[column.name] = value; + if (!/(text$|keyword$)/.test(column.name)) { + acc[column.name] = value; + } return acc; }, {}); }) as T[]; diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.ts index 57c13d511eb35..325a638cde973 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.ts @@ -15,6 +15,7 @@ import { } from '@kbn/server-route-repository-client'; import { type KueryNode, nodeTypes, toKqlExpression } from '@kbn/es-query'; import { entityLatestSchema } from '@kbn/entities-schema'; +import { castArray } from 'lodash'; import { DisableManagedEntityResponse, EnableManagedEntityResponse, @@ -110,7 +111,7 @@ export class EntityClient { throw new Error('Identity fields are missing'); } - return identityFields.reduce((acc, field) => { + return castArray(identityFields).reduce((acc, field) => { const value = field.split('.').reduce((obj: any, part: string) => { return obj && typeof obj === 'object' ? (obj as Record)[part] : undefined; }, entityLatest); diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 3ae52ef8ff0c6..f4fb2fe6ea4a4 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -7,12 +7,18 @@ import { z } from '@kbn/zod'; import { entityLatestSchema } from '@kbn/entities-schema'; import { + AGENT_NAME, + CLOUD_PROVIDER, + CONTAINER_ID, ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, ENTITY_ID, ENTITY_IDENTITY_FIELDS, ENTITY_LAST_SEEN, ENTITY_TYPE, + HOST_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, } from '@kbn/observability-shared-plugin/common'; import { isRight } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; @@ -59,7 +65,7 @@ export const entityTypesRt = new t.Type( (arr) => arr.join() ); -export interface Entity { +export interface BaseEntity { [ENTITY_LAST_SEEN]: string; [ENTITY_ID]: string; [ENTITY_TYPE]: string; @@ -70,6 +76,30 @@ export interface Entity { [key: string]: any; } +/** + * These types are based on service, host and container from the built in definition. + */ +export interface ServiceEntity extends BaseEntity { + [ENTITY_TYPE]: 'service'; + [SERVICE_NAME]: string; + [SERVICE_ENVIRONMENT]?: string | string[] | null; + [AGENT_NAME]: string | string[] | null; +} + +export interface HostEntity extends BaseEntity { + [ENTITY_TYPE]: 'host'; + [HOST_NAME]: string; + [CLOUD_PROVIDER]: string | string[] | null; +} + +export interface ContainerEntity extends BaseEntity { + [ENTITY_TYPE]: 'container'; + [CONTAINER_ID]: string; + [CLOUD_PROVIDER]: string | string[] | null; +} + +export type Entity = ServiceEntity | HostEntity | ContainerEntity; + export type InventoryEntityLatest = z.infer & { alertsCount?: number; }; diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index e841bac19ad02..6b61415bda6dd 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -79,7 +79,7 @@ export const useDetailViewRedirect = () => { const getDashboardRedirectUrl = useCallback( (entity: Entity) => { - const type = entity.entity.type; + const type = entity[ENTITY_TYPE]; const dashboardId = KUBERNETES_DASHBOARDS_IDS[type]; return dashboardId diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts index 27ba8c0fe46c3..c1e4a82c343b0 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts @@ -26,7 +26,6 @@ export async function getHasData({ }); const totalCount = esqlResultToPlainObjects(esqlResults)?.[0]._count ?? 0; - return { hasData: totalCount > 0 }; } catch (e) { logger.error(e); From d3bf95890eb7ad02a74bd28aef62d69f86bff4fd Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 23 Oct 2024 17:36:46 +0200 Subject: [PATCH 05/17] Fix redirection to service overview --- .../inventory/common/entities.ts | 11 ++++---- .../public/hooks/use_detail_view_redirect.ts | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index f4fb2fe6ea4a4..d2eacf24b6ca4 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -5,7 +5,7 @@ * 2.0. */ import { z } from '@kbn/zod'; -import { entityLatestSchema } from '@kbn/entities-schema'; +import { ENTITY_LATEST, entitiesAliasPattern, entityLatestSchema } from '@kbn/entities-schema'; import { AGENT_NAME, CLOUD_PROVIDER, @@ -36,11 +36,10 @@ export const defaultEntitySortField: EntityColumnIds = 'alertsCount'; export const MAX_NUMBER_OF_ENTITIES = 500; -export const ENTITIES_LATEST_ALIAS = '.entities.v1.latest*'; -// entitiesAliasPattern({ -// type: '*', -// dataset: ENTITY_LATEST, -// }); +export const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ + type: '*', + dataset: ENTITY_LATEST, +}); const entityArrayRt = t.array(t.string); export const entityTypesRt = new t.Type( diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index 6b61415bda6dd..340db91dd973c 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -7,17 +7,16 @@ import { ASSET_DETAILS_LOCATOR_ID, AssetDetailsLocatorParams, - CONTAINER_ID, + ENTITY_IDENTITY_FIELDS, ENTITY_TYPE, - HOST_NAME, SERVICE_ENVIRONMENT, - SERVICE_NAME, SERVICE_OVERVIEW_LOCATOR_ID, ServiceOverviewParams, } from '@kbn/observability-shared-plugin/common'; import { useCallback } from 'react'; import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; +import { castArray } from 'lodash'; import type { Entity } from '../../common/entities'; import { unflattenEntity } from '../../common/utils/unflatten_entity'; import { useKibana } from './use_kibana'; @@ -43,38 +42,45 @@ export const useDetailViewRedirect = () => { const dashboardLocator = locators.get(DASHBOARD_APP_LOCATOR); const serviceOverviewLocator = locators.get(SERVICE_OVERVIEW_LOCATOR_ID); - const getIdentityValue = useCallback( - (entity: Entity) => entityManager.entityClient.getIdentityFieldsValue(unflattenEntity(entity)), + const getSingleIdentityFieldValue = useCallback( + (entity: Entity) => { + const identityField = castArray(entity[ENTITY_IDENTITY_FIELDS])[0]; + return entityManager.entityClient.getIdentityFieldsValue(unflattenEntity(entity))[ + identityField + ]; + }, [entityManager.entityClient] ); const getDetailViewRedirectUrl = useCallback( (entity: Entity) => { const type = entity[ENTITY_TYPE]; - const identityValue = getIdentityValue(entity); + const identityValue = getSingleIdentityFieldValue(entity); switch (type) { case 'host': return assetDetailsLocator?.getRedirectUrl({ - assetId: identityValue[HOST_NAME], + assetId: identityValue, assetType: type, }); case 'container': return assetDetailsLocator?.getRedirectUrl({ - assetId: identityValue[CONTAINER_ID], + assetId: identityValue, assetType: type, }); case 'service': return serviceOverviewLocator?.getRedirectUrl({ - serviceName: identityValue[SERVICE_NAME], - environment: identityValue[SERVICE_ENVIRONMENT], + serviceName: identityValue, + // service.environemnt is not part of entity.identityFields + // we need to manually get its value + environment: [entity[SERVICE_ENVIRONMENT] || undefined].flat()[0], }); default: return undefined; } }, - [assetDetailsLocator, getIdentityValue, serviceOverviewLocator] + [assetDetailsLocator, getSingleIdentityFieldValue, serviceOverviewLocator] ); const getDashboardRedirectUrl = useCallback( From 592039b20748cb7777fcda3b33d6d9c46248f93f Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 23 Oct 2024 17:39:32 +0200 Subject: [PATCH 06/17] Fix redirection to service overview --- .../public/hooks/use_detail_view_redirect.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index 340db91dd973c..5dce846be2124 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -17,6 +17,7 @@ import { useCallback } from 'react'; import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; +import { Exception } from 'handlebars'; import type { Entity } from '../../common/entities'; import { unflattenEntity } from '../../common/utils/unflatten_entity'; import { useKibana } from './use_kibana'; @@ -44,7 +45,14 @@ export const useDetailViewRedirect = () => { const getSingleIdentityFieldValue = useCallback( (entity: Entity) => { - const identityField = castArray(entity[ENTITY_IDENTITY_FIELDS])[0]; + const identityFields = castArray(entity[ENTITY_IDENTITY_FIELDS]); + if (identityFields.length > 1) { + throw new Exception( + `Multiple identity fields are not supported for ${entity[ENTITY_TYPE]}` + ); + } + + const identityField = identityFields[0]; return entityManager.entityClient.getIdentityFieldsValue(unflattenEntity(entity))[ identityField ]; @@ -59,15 +67,12 @@ export const useDetailViewRedirect = () => { switch (type) { case 'host': - return assetDetailsLocator?.getRedirectUrl({ - assetId: identityValue, - assetType: type, - }); case 'container': return assetDetailsLocator?.getRedirectUrl({ assetId: identityValue, assetType: type, }); + case 'service': return serviceOverviewLocator?.getRedirectUrl({ serviceName: identityValue, From 07a00506e56a38d4aababbef31db16d8df813278 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Thu, 24 Oct 2024 12:52:11 +0200 Subject: [PATCH 07/17] Add tests --- .../es/utils/esql_result_to_plain_objects.ts | 12 +++++-- .../inventory/common/entities.ts | 32 +------------------ .../entities/get_identify_fields.test.ts | 2 +- 3 files changed, 12 insertions(+), 34 deletions(-) diff --git a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts index 693efee838c93..96049f75ef156 100644 --- a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts +++ b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts @@ -13,9 +13,17 @@ export function esqlResultToPlainObjects>( return result.values.map((row) => { return row.reduce>((acc, value, index) => { const column = result.columns[index]; - if (!/(text$|keyword$)/.test(column.name)) { - acc[column.name] = value; + + if (!column) { + return acc; + } + + // Removes the type suffix from the column name + const name = column.name.replace(/\.(text|keyword)$/, ''); + if (!acc[name]) { + acc[name] = value; } + return acc; }, {}); }) as T[]; diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index d2eacf24b6ca4..8bccea2222000 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -7,18 +7,12 @@ import { z } from '@kbn/zod'; import { ENTITY_LATEST, entitiesAliasPattern, entityLatestSchema } from '@kbn/entities-schema'; import { - AGENT_NAME, - CLOUD_PROVIDER, - CONTAINER_ID, ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, ENTITY_ID, ENTITY_IDENTITY_FIELDS, ENTITY_LAST_SEEN, ENTITY_TYPE, - HOST_NAME, - SERVICE_ENVIRONMENT, - SERVICE_NAME, } from '@kbn/observability-shared-plugin/common'; import { isRight } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; @@ -64,7 +58,7 @@ export const entityTypesRt = new t.Type( (arr) => arr.join() ); -export interface BaseEntity { +export interface Entity { [ENTITY_LAST_SEEN]: string; [ENTITY_ID]: string; [ENTITY_TYPE]: string; @@ -75,30 +69,6 @@ export interface BaseEntity { [key: string]: any; } -/** - * These types are based on service, host and container from the built in definition. - */ -export interface ServiceEntity extends BaseEntity { - [ENTITY_TYPE]: 'service'; - [SERVICE_NAME]: string; - [SERVICE_ENVIRONMENT]?: string | string[] | null; - [AGENT_NAME]: string | string[] | null; -} - -export interface HostEntity extends BaseEntity { - [ENTITY_TYPE]: 'host'; - [HOST_NAME]: string; - [CLOUD_PROVIDER]: string | string[] | null; -} - -export interface ContainerEntity extends BaseEntity { - [ENTITY_TYPE]: 'container'; - [CONTAINER_ID]: string; - [CLOUD_PROVIDER]: string | string[] | null; -} - -export type Entity = ServiceEntity | HostEntity | ContainerEntity; - export type InventoryEntityLatest = z.infer & { alertsCount?: number; }; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts index ffd5ba9c6f855..62d77c08fd27a 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts @@ -30,7 +30,7 @@ describe('getIdentityFields', () => { it('should return a Map with unique entity types and their respective identity fields', () => { const serviceEntity: Entity = { 'agent.name': 'node', - 'entity.identity_fields': ['service.name', 'service.environment'], + [ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'], 'service.name': 'my-service', 'entity.type': 'service', ...commonEntityFields, From 9b19596800a12094014e2f5ada4ad12bd97543b1 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Thu, 24 Oct 2024 13:01:38 +0200 Subject: [PATCH 08/17] Add tests --- .../utils/esql_result_to_plan_object.test.ts | 66 +++++++ .../hooks/use_detail_view_redirect.test.ts | 168 ++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plan_object.test.ts create mode 100644 x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts diff --git a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plan_object.test.ts b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plan_object.test.ts new file mode 100644 index 0000000000000..4557d0ba0bdd5 --- /dev/null +++ b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plan_object.test.ts @@ -0,0 +1,66 @@ +/* + * 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 type { ESQLSearchResponse } from '@kbn/es-types'; +import { esqlResultToPlainObjects } from './esql_result_to_plain_objects'; + +describe('esqlResultToPlainObjects', () => { + it('should return an empty array for an empty result', () => { + const result: ESQLSearchResponse = { + columns: [], + values: [], + }; + const output = esqlResultToPlainObjects(result); + expect(output).toEqual([]); + }); + + it('should return plain objects', () => { + const result: ESQLSearchResponse = { + columns: [{ name: 'name', type: 'keyword' }], + values: [['Foo Bar']], + }; + const output = esqlResultToPlainObjects(result); + expect(output).toEqual([{ name: 'Foo Bar' }]); + }); + + it('should return columns without "text" or "keyword" in their names', () => { + const result: ESQLSearchResponse = { + columns: [ + { name: 'name.text', type: 'text' }, + { name: 'age', type: 'keyword' }, + ], + values: [ + ['Foo Bar', 30], + ['Foo Qux', 25], + ], + }; + const output = esqlResultToPlainObjects(result); + expect(output).toEqual([ + { name: 'Foo Bar', age: 30 }, + { name: 'Foo Qux', age: 25 }, + ]); + }); + + it('should handle mixed columns correctly', () => { + const result: ESQLSearchResponse = { + columns: [ + { name: 'name', type: 'text' }, + { name: 'name.text', type: 'text' }, + { name: 'age', type: 'keyword' }, + ], + values: [ + ['Foo Bar', 'Foo Bar', 30], + ['Foo Qux', 'Foo Qux', 25], + ], + }; + const output = esqlResultToPlainObjects(result); + expect(output).toEqual([ + { name: 'Foo Bar', age: 30 }, + { name: 'Foo Qux', age: 25 }, + ]); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts new file mode 100644 index 0000000000000..039e87d95cf21 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -0,0 +1,168 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useDetailViewRedirect } from './use_detail_view_redirect'; +import { useKibana } from './use_kibana'; +import { + AGENT_NAME, + CLOUD_PROVIDER, + CONTAINER_ID, + ENTITY_DEFINITION_ID, + ENTITY_DISPLAY_NAME, + ENTITY_ID, + ENTITY_IDENTITY_FIELDS, + ENTITY_LAST_SEEN, + ENTITY_TYPE, + HOST_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, +} from '@kbn/observability-shared-plugin/common'; +import { unflattenEntity } from '../../common/utils/unflatten_entity'; +import type { Entity } from '../../common/entities'; + +jest.mock('./use_kibana'); +jest.mock('../../common/utils/unflatten_entity'); + +const useKibanaMock = useKibana as jest.Mock; +const unflattenEntityMock = unflattenEntity as jest.Mock; + +const commonEntityFields: Partial = { + [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', + [ENTITY_ID]: '1', + [ENTITY_DISPLAY_NAME]: 'entity_name', + [ENTITY_DEFINITION_ID]: 'entity_definition_id', +}; + +describe('useDetailViewRedirect', () => { + const mockGetIdentityFieldsValue = jest.fn(); + const mockAsKqlFilter = jest.fn(); + const mockGetRedirectUrl = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + useKibanaMock.mockReturnValue({ + services: { + share: { + url: { + locators: { + get: jest.fn().mockReturnValue({ + getRedirectUrl: mockGetRedirectUrl, + }), + }, + }, + }, + entityManager: { + entityClient: { + getIdentityFieldsValue: mockGetIdentityFieldsValue, + asKqlFilter: mockAsKqlFilter, + }, + }, + }, + }); + + unflattenEntityMock.mockImplementation((entity) => entity); + }); + + it('getEntityRedirectUrl should return the correct URL for host entity', () => { + const entity: Entity = { + ...(commonEntityFields as Entity), + [ENTITY_IDENTITY_FIELDS]: [HOST_NAME], + [ENTITY_TYPE]: 'host', + [HOST_NAME]: 'host-1', + [CLOUD_PROVIDER]: null, + }; + + mockGetIdentityFieldsValue.mockReturnValue({ [HOST_NAME]: 'host-1' }); + mockGetRedirectUrl.mockReturnValue('asset-details-url'); + + const { result } = renderHook(() => useDetailViewRedirect()); + const url = result.current.getEntityRedirectUrl(entity); + + expect(url).toBe('asset-details-url'); + expect(mockGetRedirectUrl).toHaveBeenCalledWith({ assetId: 'host-1', assetType: 'host' }); + }); + + it('getEntityRedirectUrl should return the correct URL for container entity', () => { + const entity: Entity = { + ...(commonEntityFields as Entity), + [ENTITY_IDENTITY_FIELDS]: [CONTAINER_ID], + [ENTITY_TYPE]: 'container', + [CONTAINER_ID]: 'container-1', + [CLOUD_PROVIDER]: null, + }; + + mockGetIdentityFieldsValue.mockReturnValue({ [CONTAINER_ID]: 'container-1' }); + mockGetRedirectUrl.mockReturnValue('asset-details-url'); + + const { result } = renderHook(() => useDetailViewRedirect()); + const url = result.current.getEntityRedirectUrl(entity); + + expect(url).toBe('asset-details-url'); + expect(mockGetRedirectUrl).toHaveBeenCalledWith({ + assetId: 'container-1', + assetType: 'container', + }); + }); + + it('getEntityRedirectUrl should return the correct URL for service entity', () => { + const entity: Entity = { + ...(commonEntityFields as Entity), + [ENTITY_IDENTITY_FIELDS]: [SERVICE_NAME], + [ENTITY_TYPE]: 'service', + [SERVICE_NAME]: 'service-1', + [SERVICE_ENVIRONMENT]: 'prod', + [AGENT_NAME]: 'node', + }; + mockGetIdentityFieldsValue.mockReturnValue({ [SERVICE_NAME]: 'service-1' }); + mockGetRedirectUrl.mockReturnValue('service-overview-url'); + + const { result } = renderHook(() => useDetailViewRedirect()); + const url = result.current.getEntityRedirectUrl(entity); + + expect(url).toBe('service-overview-url'); + expect(mockGetRedirectUrl).toHaveBeenCalledWith({ + serviceName: 'service-1', + environment: 'prod', + }); + }); + + [ + ['kubernetes_cluster_ecs', 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c'], + ['kubernetes_cron_job_ecs', 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013'], + ['kubernetes_daemon_set_ecs', 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013'], + ['kubernetes_deployment_ecs', 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013'], + ['kubernetes_job_ecs', 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013'], + ['kubernetes_node_ecs', 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013'], + ['kubernetes_pod_ecs', 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013'], + ['kubernetes_stateful_set_ecs', 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013'], + ].forEach(([entityType, dashboardId]) => { + it(`getEntityRedirectUrl should return the correct URL for ${entityType} entity`, () => { + const entity: Entity = { + ...(commonEntityFields as Entity), + [ENTITY_IDENTITY_FIELDS]: ['some.field'], + [ENTITY_TYPE]: entityType, + }; + + mockAsKqlFilter.mockReturnValue('kql-query'); + mockGetRedirectUrl.mockReturnValue('dashboard-url'); + + const { result } = renderHook(() => useDetailViewRedirect()); + const url = result.current.getEntityRedirectUrl(entity); + + expect(url).toBe('dashboard-url'); + expect(mockGetRedirectUrl).toHaveBeenCalledWith({ + dashboardId, + query: { + language: 'kuery', + query: 'kql-query', + }, + }); + }); + }); +}); From 584a3b245d999c1993dd52b562c638d4ffb4fda8 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 25 Oct 2024 17:12:56 +0200 Subject: [PATCH 09/17] Refactoring and tests --- .../src/lib/entities/kubernetes/index.ts | 6 +- .../public/lib/entity_client.test.ts | 74 ++++++-- .../public/lib/entity_client.ts | 14 +- .../apm/common/entities/types.ts | 12 -- .../asset_details/hooks/use_entity_summary.ts | 7 +- .../infra/server/routes/entities/index.ts | 6 +- .../inventory/common/entities.ts | 11 +- .../entity_name/entity_name.test.tsx | 175 +++++------------- .../public/components/entity_icon/index.tsx | 11 +- .../hooks/use_detail_view_redirect.test.ts | 26 ++- .../public/hooks/use_detail_view_redirect.ts | 23 ++- .../metrics_data_access/common/constants.ts | 1 + .../common/entity/entity_types.ts | 26 ++- .../common/entity/index.ts | 2 +- .../observability_shared/common/index.ts | 2 +- 15 files changed, 195 insertions(+), 201 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/apm/common/entities/types.ts diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts index 36d7f8caf9601..db95dcf4155bc 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts @@ -55,9 +55,9 @@ export class K8sEntity extends Serializable { super({ ...fields, 'entity.type': entityTypeWithSchema, - 'entity.definitionId': `builtin_${entityTypeWithSchema}`, - 'entity.identityFields': identityFields, - 'entity.displayName': getDisplayName({ identityFields, fields }), + 'entity.definition_id': `builtin_${entityTypeWithSchema}`, + 'entity.identity_fields': identityFields, + 'entity.display_name': getDisplayName({ identityFields, fields }), }); } } diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts index 9f349e4a7649b..1e85059707f9d 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts @@ -10,10 +10,10 @@ import { coreMock } from '@kbn/core/public/mocks'; const commonEntityFields: EntityLatest = { entity: { - lastSeenTimestamp: '2023-10-09T00:00:00Z', + last_seen_timestamp: '2023-10-09T00:00:00Z', id: '1', - displayName: 'entity_name', - definitionId: 'entity_definition_id', + display_name: 'entity_name', + definition_id: 'entity_definition_id', } as EntityLatest['entity'], }; @@ -25,11 +25,11 @@ describe('EntityClient', () => { }); describe('asKqlFilter', () => { - it('should return the value when identityFields is a single string', () => { + it('should return the value when indentity_fields is a single string', () => { const entityLatest: EntityLatest = { entity: { ...commonEntityFields.entity, - identityFields: ['service.name', 'service.environment'], + identity_fields: ['service.name', 'service.environment'], type: 'service', }, service: { @@ -41,11 +41,11 @@ describe('EntityClient', () => { expect(result).toEqual('service.name: my-service'); }); - it('should return values when identityFields is an array of strings', () => { + it('should return values when indentity_fields is composed by multiple fields', () => { const entityLatest: EntityLatest = { entity: { ...commonEntityFields.entity, - identityFields: ['service.name', 'service.environment'], + identity_fields: ['service.name', 'service.environment'], type: 'service', }, service: { @@ -58,6 +58,25 @@ describe('EntityClient', () => { expect(result).toEqual('(service.name: my-service AND service.environment: staging)'); }); + it('should return identity fields values when an indentity field value is an array', () => { + const entityLatest: EntityLatest = { + entity: { + ...commonEntityFields.entity, + identity_fields: ['service.name', 'service.environment'], + type: 'service', + }, + service: { + name: 'my-service', + environment: ['prod', 'staging', 'dev'], + }, + }; + + const result = entityClient.asKqlFilter(entityLatest); + expect(result).toEqual( + '(service.name: my-service AND (service.environment: prod OR service.environment: staging OR service.environment: dev))' + ); + }); + it('should throw an error when identity fields are missing', () => { const entityLatest: EntityLatest = { ...commonEntityFields, @@ -70,7 +89,7 @@ describe('EntityClient', () => { const entityLatest: EntityLatest = { entity: { ...commonEntityFields.entity, - identityFields: ['host.name', 'foo.bar'], + identity_fields: ['host.name', 'foo.bar'], }, host: { name: 'my-host', @@ -84,10 +103,10 @@ describe('EntityClient', () => { describe('getIdentityFieldsValue', () => { it('should return identity fields values', () => { - const serviceEntity: EntityLatest = { + const entityLatest: EntityLatest = { entity: { ...commonEntityFields.entity, - identityFields: ['service.name', 'service.environment'], + identity_fields: ['service.name', 'service.environment'], type: 'service', }, service: { @@ -95,16 +114,16 @@ describe('EntityClient', () => { }, }; - expect(entityClient.getIdentityFieldsValue(serviceEntity)).toEqual({ + expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({ 'service.name': 'my-service', }); }); - it('should return identity fields values when indentity field is an array of string', () => { - const serviceEntity: EntityLatest = { + it('should return identity fields values when indentity_fields is composed by multiple fields', () => { + const entityLatest: EntityLatest = { entity: { ...commonEntityFields.entity, - identityFields: ['service.name', 'service.environment'], + identity_fields: ['service.name', 'service.environment'], type: 'service', }, service: { @@ -113,23 +132,42 @@ describe('EntityClient', () => { }, }; - expect(entityClient.getIdentityFieldsValue(serviceEntity)).toEqual({ + expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({ 'service.name': 'my-service', 'service.environment': 'staging', }); }); + it('should return identity fields values when an indentity field value is an array', () => { + const entityLatest: EntityLatest = { + entity: { + ...commonEntityFields.entity, + identity_fields: ['service.name', 'service.environment'], + type: 'service', + }, + service: { + name: 'my-service', + environment: ['prod', 'staging', 'dev'], + }, + }; + + expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({ + 'service.name': 'my-service', + 'service.environment': ['prod', 'staging', 'dev'], + }); + }); + it('should return identity fields when field is in the root', () => { - const serviceEntity: EntityLatest = { + const entityLatest: EntityLatest = { entity: { ...commonEntityFields.entity, - identityFields: ['name'], + identity_fields: ['name'], type: 'service', }, name: 'foo', }; - expect(entityClient.getIdentityFieldsValue(serviceEntity)).toEqual({ + expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({ name: 'foo', }); }); diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.ts index 325a638cde973..15e7808dced22 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.ts @@ -93,9 +93,15 @@ export class EntityClient { asKqlFilter(entityLatest: EntityLatest) { const identityFieldsValue = this.getIdentityFieldsValue(entityLatest); - const nodes: KueryNode[] = Object.entries(identityFieldsValue).map(([identityField, value]) => - nodeTypes.function.buildNode('is', identityField, value) - ); + const nodes: KueryNode[] = Object.entries(identityFieldsValue).map(([identityField, value]) => { + if (Array.isArray(value)) { + return nodeTypes.function.buildNode( + 'or', + value.map((v) => nodeTypes.function.buildNode('is', identityField, v)) + ); + } + return nodeTypes.function.buildNode('is', identityField, value); + }); if (nodes.length === 0) return ''; @@ -105,7 +111,7 @@ export class EntityClient { } getIdentityFieldsValue(entityLatest: EntityLatest) { - const { identityFields } = entityLatest.entity; + const { identity_fields: identityFields } = entityLatest.entity; if (!identityFields) { throw new Error('Identity fields are missing'); diff --git a/x-pack/plugins/observability_solution/apm/common/entities/types.ts b/x-pack/plugins/observability_solution/apm/common/entities/types.ts deleted file mode 100644 index 9775b1e32eae6..0000000000000 --- a/x-pack/plugins/observability_solution/apm/common/entities/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * 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 EntityDataStreamType { - METRICS = 'metrics', - TRACES = 'traces', - LOGS = 'logs', -} diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts index 349b8e13ae7ab..d723f6165500d 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts @@ -6,10 +6,13 @@ */ import * as z from '@kbn/zod'; -import { EntityDataStreamType, EntityType } from '@kbn/observability-shared-plugin/common'; +import { EntityDataStreamType, MANAGED_ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import { useFetcher } from '../../../hooks/use_fetcher'; -const EntityTypeSchema = z.union([z.literal(EntityType.HOST), z.literal(EntityType.CONTAINER)]); +const EntityTypeSchema = z.union([ + z.literal(MANAGED_ENTITY_TYPE.HOST), + z.literal(MANAGED_ENTITY_TYPE.CONTAINER), +]); const EntityDataStreamSchema = z.union([ z.literal(EntityDataStreamType.METRICS), z.literal(EntityDataStreamType.LOGS), diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts index cb169f83f171d..9ec2b0530ec31 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { METRICS_APP_ID } from '@kbn/deeplinks-observability/constants'; import { entityCentricExperience } from '@kbn/observability-plugin/common'; import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { MANAGED_ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import { getInfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client'; import { InfraBackendLibs } from '../../lib/infra_types'; import { getDataStreamTypes } from './get_data_stream_types'; @@ -22,7 +23,10 @@ export const initEntitiesConfigurationRoutes = (libs: InfraBackendLibs) => { path: '/api/infra/entities/{entityType}/{entityId}/summary', validate: { params: schema.object({ - entityType: schema.oneOf([schema.literal('host'), schema.literal('container')]), + entityType: schema.oneOf([ + schema.literal(MANAGED_ENTITY_TYPE.HOST), + schema.literal(MANAGED_ENTITY_TYPE.CONTAINER), + ]), entityId: schema.string(), }), }, diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 8bccea2222000..3ae52ef8ff0c6 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -5,7 +5,7 @@ * 2.0. */ import { z } from '@kbn/zod'; -import { ENTITY_LATEST, entitiesAliasPattern, entityLatestSchema } from '@kbn/entities-schema'; +import { entityLatestSchema } from '@kbn/entities-schema'; import { ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, @@ -30,10 +30,11 @@ export const defaultEntitySortField: EntityColumnIds = 'alertsCount'; export const MAX_NUMBER_OF_ENTITIES = 500; -export const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ - type: '*', - dataset: ENTITY_LATEST, -}); +export const ENTITIES_LATEST_ALIAS = '.entities.v1.latest*'; +// entitiesAliasPattern({ +// type: '*', +// dataset: ENTITY_LATEST, +// }); const entityArrayRt = t.array(t.string); export const entityTypesRt = new t.Type( diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx index 2e4f0c319edfc..d5d08ed415a40 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx @@ -5,148 +5,65 @@ * 2.0. */ -import { type KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; -import * as useKibana from '../../../hooks/use_kibana'; -import { EntityName } from '.'; -import type { Entity } from '../../../../common/entities'; -import { render, screen } from '@testing-library/react'; import React from 'react'; -import { ASSET_DETAILS_LOCATOR_ID } from '@kbn/observability-shared-plugin/common/locators/infra/asset_details_locator'; +import { render, screen } from '@testing-library/react'; +import { EntityName } from '.'; +import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; +import { Entity } from '../../../../common/entities'; +import { + ENTITY_DEFINITION_ID, + ENTITY_DISPLAY_NAME, + ENTITY_ID, + ENTITY_IDENTITY_FIELDS, + ENTITY_LAST_SEEN, + ENTITY_TYPE, +} from '@kbn/observability-shared-plugin/common'; + +jest.mock('../../../hooks/use_detail_view_redirect'); + +const useDetailViewRedirectMock = useDetailViewRedirect as jest.Mock; describe('EntityName', () => { - jest.spyOn(useKibana, 'useKibana').mockReturnValue({ - services: { - share: { - url: { - locators: { - get: (locatorId: string) => { - return { - getRedirectUrl: (params: { [key: string]: any }) => { - if (locatorId === ASSET_DETAILS_LOCATOR_ID) { - return `assets_url/${params.assetType}/${params.assetId}`; - } - return `services_url/${params.serviceName}?environment=${params.environment}`; - }, - }; - }, - }, - }, - }, - }, - } as unknown as KibanaReactContextValue); + const mockEntity: Entity = { + [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', + [ENTITY_ID]: '1', + [ENTITY_DISPLAY_NAME]: 'entity_name', + [ENTITY_DEFINITION_ID]: 'entity_definition_id', + [ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'], + [ENTITY_TYPE]: 'service', + }; - afterAll(() => { + beforeEach(() => { jest.clearAllMocks(); }); - it('returns host link', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'entity.id': '1', - 'entity.type': 'host', - 'entity.display_name': 'foo', - 'entity.identity_fields': 'host.name', - 'host.name': 'foo', - 'entity.definition_id': 'host', - 'cloud.provider': null, - }; - render(); - expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( - 'assets_url/host/foo' - ); - expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); - }); + it('should render the entity name correctly', () => { + useDetailViewRedirectMock.mockReturnValue({ + getEntityRedirectUrl: jest.fn().mockReturnValue(null), + }); - it('returns container link', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'entity.id': '1', - 'entity.type': 'container', - 'entity.display_name': 'foo', - 'entity.identity_fields': 'container.id', - 'container.id': 'foo', - 'entity.definition_id': 'container', - 'cloud.provider': null, - }; - render(); - expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( - 'assets_url/container/foo' - ); - expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); - }); + render(); - it('returns service link without environment', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'entity.id': '1', - 'entity.type': 'service', - 'entity.display_name': 'foo', - 'entity.identity_fields': 'service.name', - 'service.name': 'foo', - 'entity.definition_id': 'service', - 'agent.name': 'bar', - }; - render(); - expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( - 'services_url/foo?environment=undefined' - ); - expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); + expect(screen.getByText('entity_name')).toBeInTheDocument(); }); - it('returns service link with environment', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'entity.id': '1', - 'entity.type': 'service', - 'entity.display_name': 'foo', - 'entity.identity_fields': 'service.name', - 'service.name': 'foo', - 'entity.definition_id': 'service', - 'agent.name': 'bar', - 'service.environment': 'baz', - }; - render(); - expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( - 'services_url/foo?environment=baz' - ); - expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); - }); + it('should a link when getEntityRedirectUrl returns a URL', () => { + useDetailViewRedirectMock.mockReturnValue({ + getEntityRedirectUrl: jest.fn().mockReturnValue('http://foo.bar'), + }); - it('returns service link with first environment when it is an array', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'entity.id': '1', - 'entity.type': 'service', - 'entity.display_name': 'foo', - 'entity.identity_fields': 'service.name', - 'service.name': 'foo', - 'entity.definition_id': 'service', - 'agent.name': 'bar', - 'service.environment': ['baz', 'bar', 'foo'], - }; - render(); - expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( - 'services_url/foo?environment=baz' - ); - expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); + render(); + + expect(screen.getByRole('link')).toHaveAttribute('href', 'http://foo.bar'); }); - it('returns service link identity fields is an array', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'entity.id': '1', - 'entity.type': 'service', - 'entity.display_name': 'foo', - 'entity.identity_fields': ['service.name', 'service.environment'], - 'service.name': 'foo', - 'entity.definition_id': 'service', - 'agent.name': 'bar', - 'service.environment': 'baz', - }; - render(); - expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( - 'services_url/foo?environment=baz' - ); - expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); + it('should not render a link when getEntityRedirectUrl returns null', () => { + useDetailViewRedirectMock.mockReturnValue({ + getEntityRedirectUrl: jest.fn().mockReturnValue(null), + }); + + render(); + + expect(screen.queryByRole('link')).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx index a62f0026ddfa0..5206f65f45820 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx @@ -6,7 +6,12 @@ */ import React from 'react'; -import { AGENT_NAME, CLOUD_PROVIDER, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { + AGENT_NAME, + CLOUD_PROVIDER, + ENTITY_TYPE, + MANAGED_ENTITY_TYPE, +} from '@kbn/observability-shared-plugin/common'; import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-icons'; import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import type { AgentName } from '@kbn/elastic-agent-utils'; @@ -27,7 +32,7 @@ export function EntityIcon({ entity }: EntityIconProps) { const entityType = entity[ENTITY_TYPE]; const defaultIconSize = euiThemeVars.euiSizeL; - if (entityType === 'host' || entityType === 'container') { + if (entityType === MANAGED_ENTITY_TYPE.HOST || entityType === MANAGED_ENTITY_TYPE.CONTAINER) { const cloudProvider = getSingleValue( entity[CLOUD_PROVIDER] as NotNullableCloudProvider | NotNullableCloudProvider[] ); @@ -49,7 +54,7 @@ export function EntityIcon({ entity }: EntityIconProps) { ); } - if (entityType === 'service') { + if (entityType === MANAGED_ENTITY_TYPE.SERVICE) { const agentName = getSingleValue(entity[AGENT_NAME] as AgentName | AgentName[]); return ; } diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts index 039e87d95cf21..c822ae2d8fe23 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -19,6 +19,7 @@ import { ENTITY_LAST_SEEN, ENTITY_TYPE, HOST_NAME, + MANAGED_ENTITY_TYPE, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '@kbn/observability-shared-plugin/common'; @@ -133,14 +134,23 @@ describe('useDetailViewRedirect', () => { }); [ - ['kubernetes_cluster_ecs', 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c'], - ['kubernetes_cron_job_ecs', 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013'], - ['kubernetes_daemon_set_ecs', 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013'], - ['kubernetes_deployment_ecs', 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013'], - ['kubernetes_job_ecs', 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013'], - ['kubernetes_node_ecs', 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013'], - ['kubernetes_pod_ecs', 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013'], - ['kubernetes_stateful_set_ecs', 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013'], + [MANAGED_ENTITY_TYPE.KUBERNETES.CLUSTER.ECS, 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c'], + [MANAGED_ENTITY_TYPE.KUBERNETES.CRONJOB.ECS, 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013'], + [ + MANAGED_ENTITY_TYPE.KUBERNETES.DAEMONSET.ECS, + 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', + ], + [ + MANAGED_ENTITY_TYPE.KUBERNETES.DEPLOYMENT.ECS, + 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', + ], + [MANAGED_ENTITY_TYPE.KUBERNETES.JOB.ECS, 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013'], + [MANAGED_ENTITY_TYPE.KUBERNETES.NODE.ECS, 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013'], + [MANAGED_ENTITY_TYPE.KUBERNETES.POD.ECS, 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013'], + [ + MANAGED_ENTITY_TYPE.KUBERNETES.STATEFULSET.ECS, + 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', + ], ].forEach(([entityType, dashboardId]) => { it(`getEntityRedirectUrl should return the correct URL for ${entityType} entity`, () => { const entity: Entity = { diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index 5dce846be2124..bc4924cc4330d 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -9,6 +9,7 @@ import { AssetDetailsLocatorParams, ENTITY_IDENTITY_FIELDS, ENTITY_TYPE, + MANAGED_ENTITY_TYPE, SERVICE_ENVIRONMENT, SERVICE_OVERVIEW_LOCATOR_ID, ServiceOverviewParams, @@ -23,14 +24,16 @@ import { unflattenEntity } from '../../common/utils/unflatten_entity'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record = { - kubernetes_cluster_ecs: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', - kubernetes_cron_job_ecs: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', - kubernetes_daemon_set_ecs: 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', - kubernetes_deployment_ecs: 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', - kubernetes_job_ecs: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', - kubernetes_node_ecs: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', - kubernetes_pod_ecs: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', - kubernetes_stateful_set_ecs: 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', + [MANAGED_ENTITY_TYPE.KUBERNETES.CLUSTER.ECS]: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', + [MANAGED_ENTITY_TYPE.KUBERNETES.CRONJOB.ECS]: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', + [MANAGED_ENTITY_TYPE.KUBERNETES.DAEMONSET.ECS]: 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', + [MANAGED_ENTITY_TYPE.KUBERNETES.DEPLOYMENT.ECS]: + 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', + [MANAGED_ENTITY_TYPE.KUBERNETES.JOB.ECS]: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', + [MANAGED_ENTITY_TYPE.KUBERNETES.NODE.ECS]: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', + [MANAGED_ENTITY_TYPE.KUBERNETES.POD.ECS]: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', + [MANAGED_ENTITY_TYPE.KUBERNETES.STATEFULSET.ECS]: + 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', }; export const useDetailViewRedirect = () => { @@ -66,8 +69,8 @@ export const useDetailViewRedirect = () => { const identityValue = getSingleIdentityFieldValue(entity); switch (type) { - case 'host': - case 'container': + case MANAGED_ENTITY_TYPE.HOST: + case MANAGED_ENTITY_TYPE.CONTAINER: return assetDetailsLocator?.getRedirectUrl({ assetId: identityValue, assetType: type, diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/constants.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/constants.ts index d801cc214ecaf..227188d359509 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/constants.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/constants.ts @@ -9,5 +9,6 @@ export const TIMESTAMP_FIELD = '@timestamp'; export const HOST_FIELD = 'host.name'; export const CONTAINER_FIELD = 'container.id'; export const POD_FIELD = 'kubernetes.pod.uid'; +export const KUBENRNETES_CONTAINER_FIELD = 'kubernetes.container.id'; export const METRICS_EXPLORER_API_MAX_METRICS = 20; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts b/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts index b905f542d3473..fdd5b9b5ed957 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts @@ -5,7 +5,25 @@ * 2.0. */ -export enum EntityType { - HOST = 'host', - CONTAINER = 'container', -} +const createKubernetesEntity = (base: T) => ({ + ECS: `kubernetes_${base}_ecs` as const, + SEMCONV: `kubernetes_${base}_semconv` as const, +}); + +export const MANAGED_ENTITY_TYPE = { + HOST: 'host', + CONTAINER: 'container', + SERVICE: 'service', + KUBERNETES: { + CLUSTER: createKubernetesEntity('cluster'), + CONTAINER: createKubernetesEntity('container'), + CRONJOB: createKubernetesEntity('cron_job'), + DAEMONSET: createKubernetesEntity('daemon_set'), + DEPLOYMENT: createKubernetesEntity('deployment'), + JOB: createKubernetesEntity('job'), + NAMESPACE: createKubernetesEntity('namespace'), + NODE: createKubernetesEntity('node'), + POD: createKubernetesEntity('pod'), + STATEFULSET: createKubernetesEntity('stateful_set'), + }, +} as const; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts index 27bef43d5ff7a..8d8fac054f471 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { EntityType } from './entity_types'; +export { MANAGED_ENTITY_TYPE } from './entity_types'; export { EntityDataStreamType } from './entity_data_stream_types'; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/index.ts index b673b5d12e862..dcfefc09bdbf1 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/index.ts @@ -219,4 +219,4 @@ export { export { COMMON_OBSERVABILITY_GROUPING } from './embeddable_grouping'; -export { EntityType, EntityDataStreamType } from './entity'; +export { MANAGED_ENTITY_TYPE, EntityDataStreamType } from './entity'; From e5929dbf308fc96e8d26400a759dcd41edbf1630 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 25 Oct 2024 17:33:11 +0200 Subject: [PATCH 10/17] Clean up --- .../inventory/common/entities.ts | 11 +++++------ .../metrics_data_access/common/constants.ts | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 3ae52ef8ff0c6..8bccea2222000 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -5,7 +5,7 @@ * 2.0. */ import { z } from '@kbn/zod'; -import { entityLatestSchema } from '@kbn/entities-schema'; +import { ENTITY_LATEST, entitiesAliasPattern, entityLatestSchema } from '@kbn/entities-schema'; import { ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, @@ -30,11 +30,10 @@ export const defaultEntitySortField: EntityColumnIds = 'alertsCount'; export const MAX_NUMBER_OF_ENTITIES = 500; -export const ENTITIES_LATEST_ALIAS = '.entities.v1.latest*'; -// entitiesAliasPattern({ -// type: '*', -// dataset: ENTITY_LATEST, -// }); +export const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ + type: '*', + dataset: ENTITY_LATEST, +}); const entityArrayRt = t.array(t.string); export const entityTypesRt = new t.Type( diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/constants.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/constants.ts index 227188d359509..d801cc214ecaf 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/constants.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/constants.ts @@ -9,6 +9,5 @@ export const TIMESTAMP_FIELD = '@timestamp'; export const HOST_FIELD = 'host.name'; export const CONTAINER_FIELD = 'container.id'; export const POD_FIELD = 'kubernetes.pod.uid'; -export const KUBENRNETES_CONTAINER_FIELD = 'kubernetes.container.id'; export const METRICS_EXPLORER_API_MAX_METRICS = 20; From 6ed10afe2b2c6a3b9f9c8d425d18916816a65843 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:58:15 +0000 Subject: [PATCH 11/17] [CI] Auto-commit changed files from 'node scripts/yarn_deduplicate' --- .../plugins/observability_solution/inventory/tsconfig.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json index 67de9919c6324..bd77df478cad1 100644 --- a/x-pack/plugins/observability_solution/inventory/tsconfig.json +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -52,6 +52,9 @@ "@kbn/rule-data-utils", "@kbn/spaces-plugin", "@kbn/cloud-plugin", - "@kbn/storybook" + "@kbn/storybook", + "@kbn/zod", + "@kbn/dashboard-plugin", + "@kbn/deeplinks-analytics" ] } From 43dcb699e81b2f97100f6bc1b5a8c622cd500785 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 25 Oct 2024 18:02:56 +0200 Subject: [PATCH 12/17] fix after rebase --- .../components/entities_grid/entity_name/index.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index d9ccd6358e7c2..e8db7013f8cb3 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -6,7 +6,9 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; +import { ENTITY_DISPLAY_NAME } from '@kbn/observability-shared-plugin/common'; +import { useKibana } from '../../../hooks/use_kibana'; import type { Entity } from '../../../../common/entities'; import { EntityIcon } from '../../entity_icon'; import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; @@ -46,11 +48,7 @@ export function EntityName({ entity }: EntityNameProps) { return href ? ( // eslint-disable-next-line @elastic/eui/href-or-on-click - + {entityName} ) : ( From 72a8c0e08ea9e1ca40f112a7a4cb5bf76c96d733 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 28 Oct 2024 09:20:21 +0100 Subject: [PATCH 13/17] Fix build --- .../apm/common/es_fields/entities.ts | 12 ----- .../asset_details/hooks/use_entity_summary.ts | 7 +-- .../tabs/processes/processes.tsx | 4 +- .../infra/server/routes/entities/index.ts | 6 +-- .../alerts_badge/alerts_badge.test.tsx | 44 ++++++++++++++----- .../public/components/entity_icon/index.tsx | 6 +-- .../hooks/use_detail_view_redirect.test.ts | 27 ++++-------- .../public/hooks/use_detail_view_redirect.ts | 24 +++++----- .../common/entity/entity_types.ts | 6 +-- .../common/entity/index.ts | 2 +- .../observability_shared/common/index.ts | 2 +- 11 files changed, 67 insertions(+), 73 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/apm/common/es_fields/entities.ts diff --git a/x-pack/plugins/observability_solution/apm/common/es_fields/entities.ts b/x-pack/plugins/observability_solution/apm/common/es_fields/entities.ts deleted file mode 100644 index 28e4a3ec79165..0000000000000 --- a/x-pack/plugins/observability_solution/apm/common/es_fields/entities.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * 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 const ENTITY_METRICS_LATENCY = 'entity.metrics.latency'; -export const ENTITY_METRICS_LOG_ERROR_RATE = 'entity.metrics.logErrorRate'; -export const ENTITY_METRICS_LOG_RATE = 'entity.metrics.logRate'; -export const ENTITY_METRICS_THROUGHPUT = 'entity.metrics.throughput'; -export const ENTITY_METRICS_FAILED_TRANSACTION_RATE = 'entity.metrics.failedTransactionRate'; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts index d723f6165500d..e62defaac6d4b 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_entity_summary.ts @@ -6,13 +6,10 @@ */ import * as z from '@kbn/zod'; -import { EntityDataStreamType, MANAGED_ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { EntityDataStreamType, ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; import { useFetcher } from '../../../hooks/use_fetcher'; -const EntityTypeSchema = z.union([ - z.literal(MANAGED_ENTITY_TYPE.HOST), - z.literal(MANAGED_ENTITY_TYPE.CONTAINER), -]); +const EntityTypeSchema = z.union([z.literal(ENTITY_TYPES.HOST), z.literal(ENTITY_TYPES.CONTAINER)]); const EntityDataStreamSchema = z.union([ z.literal(EntityDataStreamType.METRICS), z.literal(EntityDataStreamType.LOGS), diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/processes/processes.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/processes/processes.tsx index 4e8fc1e3badb1..2177cd0509085 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/processes/processes.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/processes/processes.tsx @@ -22,8 +22,8 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { getFieldByType } from '@kbn/metrics-data-access-plugin/common'; import { decodeOrThrow } from '@kbn/io-ts-utils'; -import { EntityType } from '@kbn/observability-shared-plugin/common'; import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; import { useSourceContext } from '../../../../containers/metrics_source'; import { isPending, useFetcher } from '../../../../hooks/use_fetcher'; import { parseSearchString } from './parse_search_string'; @@ -58,7 +58,7 @@ export const Processes = () => { const { request$ } = useRequestObservable(); const { isActiveTab } = useTabSwitcherContext(); const { dataStreams, status: dataStreamsStatus } = useEntitySummary({ - entityType: EntityType.HOST, + entityType: ENTITY_TYPES.HOST, entityId: asset.name, }); const addMetricsCalloutId: AddMetricsCalloutKey = 'hostProcesses'; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts index 9ec2b0530ec31..1a8707678e8f7 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { METRICS_APP_ID } from '@kbn/deeplinks-observability/constants'; import { entityCentricExperience } from '@kbn/observability-plugin/common'; import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; -import { MANAGED_ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; import { getInfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client'; import { InfraBackendLibs } from '../../lib/infra_types'; import { getDataStreamTypes } from './get_data_stream_types'; @@ -24,8 +24,8 @@ export const initEntitiesConfigurationRoutes = (libs: InfraBackendLibs) => { validate: { params: schema.object({ entityType: schema.oneOf([ - schema.literal(MANAGED_ENTITY_TYPE.HOST), - schema.literal(MANAGED_ENTITY_TYPE.CONTAINER), + schema.literal(ENTITY_TYPES.HOST), + schema.literal(ENTITY_TYPES.CONTAINER), ]), entityId: schema.string(), }), diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx index 60124e7813bc4..b5244cb29f7fc 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx @@ -5,22 +5,35 @@ * 2.0. */ import React from 'react'; -import { type KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; import { render, screen } from '@testing-library/react'; import { AlertsBadge } from './alerts_badge'; -import * as useKibana from '../../hooks/use_kibana'; +import { useKibana } from '../../hooks/use_kibana'; import type { Entity } from '../../../common/entities'; +jest.mock('../../hooks/use_kibana'); +const useKibanaMock = useKibana as jest.Mock; + describe('AlertsBadge', () => { - jest.spyOn(useKibana, 'useKibana').mockReturnValue({ - services: { - http: { - basePath: { - prepend: (path: string) => path, + const mockAsKqlFilter = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + useKibanaMock.mockReturnValue({ + services: { + http: { + basePath: { + prepend: (path: string) => path, + }, + }, + entityManager: { + entityClient: { + asKqlFilter: mockAsKqlFilter, + }, }, }, - }, - } as unknown as KibanaReactContextValue); + }); + }); afterAll(() => { jest.clearAllMocks(); @@ -38,9 +51,11 @@ describe('AlertsBadge', () => { 'cloud.provider': null, alertsCount: 1, }; + mockAsKqlFilter.mockReturnValue('host.name: foo'); + render(); expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( - '/app/observability/alerts?_a=(kuery:\'host.name: "foo"\',status:active)' + "/app/observability/alerts?_a=(kuery:'host.name: foo',status:active)" ); expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('1'); }); @@ -57,9 +72,11 @@ describe('AlertsBadge', () => { 'cloud.provider': null, alertsCount: 5, }; + mockAsKqlFilter.mockReturnValue('service.name: bar'); + render(); expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( - '/app/observability/alerts?_a=(kuery:\'service.name: "bar"\',status:active)' + "/app/observability/alerts?_a=(kuery:'service.name: bar',status:active)" ); expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('5'); }); @@ -77,9 +94,12 @@ describe('AlertsBadge', () => { 'cloud.provider': null, alertsCount: 2, }; + + mockAsKqlFilter.mockReturnValue('service.name: bar AND service.environment: prod'); + render(); expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( - '/app/observability/alerts?_a=(kuery:\'service.name: "bar" AND service.environment: "prod"\',status:active)' + "/app/observability/alerts?_a=(kuery:'service.name: bar AND service.environment: prod',status:active)" ); expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('2'); }); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx index 5206f65f45820..48b21779d2e38 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx @@ -10,7 +10,7 @@ import { AGENT_NAME, CLOUD_PROVIDER, ENTITY_TYPE, - MANAGED_ENTITY_TYPE, + ENTITY_TYPES, } from '@kbn/observability-shared-plugin/common'; import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-icons'; import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; @@ -32,7 +32,7 @@ export function EntityIcon({ entity }: EntityIconProps) { const entityType = entity[ENTITY_TYPE]; const defaultIconSize = euiThemeVars.euiSizeL; - if (entityType === MANAGED_ENTITY_TYPE.HOST || entityType === MANAGED_ENTITY_TYPE.CONTAINER) { + if (entityType === ENTITY_TYPES.HOST || entityType === ENTITY_TYPES.CONTAINER) { const cloudProvider = getSingleValue( entity[CLOUD_PROVIDER] as NotNullableCloudProvider | NotNullableCloudProvider[] ); @@ -54,7 +54,7 @@ export function EntityIcon({ entity }: EntityIconProps) { ); } - if (entityType === MANAGED_ENTITY_TYPE.SERVICE) { + if (entityType === ENTITY_TYPES.SERVICE) { const agentName = getSingleValue(entity[AGENT_NAME] as AgentName | AgentName[]); return ; } diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts index c822ae2d8fe23..37d68e53914dd 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -19,7 +19,7 @@ import { ENTITY_LAST_SEEN, ENTITY_TYPE, HOST_NAME, - MANAGED_ENTITY_TYPE, + ENTITY_TYPES, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '@kbn/observability-shared-plugin/common'; @@ -134,23 +134,14 @@ describe('useDetailViewRedirect', () => { }); [ - [MANAGED_ENTITY_TYPE.KUBERNETES.CLUSTER.ECS, 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c'], - [MANAGED_ENTITY_TYPE.KUBERNETES.CRONJOB.ECS, 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013'], - [ - MANAGED_ENTITY_TYPE.KUBERNETES.DAEMONSET.ECS, - 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', - ], - [ - MANAGED_ENTITY_TYPE.KUBERNETES.DEPLOYMENT.ECS, - 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', - ], - [MANAGED_ENTITY_TYPE.KUBERNETES.JOB.ECS, 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013'], - [MANAGED_ENTITY_TYPE.KUBERNETES.NODE.ECS, 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013'], - [MANAGED_ENTITY_TYPE.KUBERNETES.POD.ECS, 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013'], - [ - MANAGED_ENTITY_TYPE.KUBERNETES.STATEFULSET.ECS, - 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', - ], + [ENTITY_TYPES.KUBERNETES.CLUSTER.ecs, 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c'], + [ENTITY_TYPES.KUBERNETES.CRONJOB.ecs, 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013'], + [ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs, 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013'], + [ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs, 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013'], + [ENTITY_TYPES.KUBERNETES.JOB.ecs, 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013'], + [ENTITY_TYPES.KUBERNETES.NODE.ecs, 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013'], + [ENTITY_TYPES.KUBERNETES.POD.ecs, 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013'], + [ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs, 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013'], ].forEach(([entityType, dashboardId]) => { it(`getEntityRedirectUrl should return the correct URL for ${entityType} entity`, () => { const entity: Entity = { diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index bc4924cc4330d..d73ec6187662b 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -9,7 +9,7 @@ import { AssetDetailsLocatorParams, ENTITY_IDENTITY_FIELDS, ENTITY_TYPE, - MANAGED_ENTITY_TYPE, + ENTITY_TYPES, SERVICE_ENVIRONMENT, SERVICE_OVERVIEW_LOCATOR_ID, ServiceOverviewParams, @@ -24,16 +24,14 @@ import { unflattenEntity } from '../../common/utils/unflatten_entity'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record = { - [MANAGED_ENTITY_TYPE.KUBERNETES.CLUSTER.ECS]: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', - [MANAGED_ENTITY_TYPE.KUBERNETES.CRONJOB.ECS]: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', - [MANAGED_ENTITY_TYPE.KUBERNETES.DAEMONSET.ECS]: 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', - [MANAGED_ENTITY_TYPE.KUBERNETES.DEPLOYMENT.ECS]: - 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', - [MANAGED_ENTITY_TYPE.KUBERNETES.JOB.ECS]: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', - [MANAGED_ENTITY_TYPE.KUBERNETES.NODE.ECS]: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', - [MANAGED_ENTITY_TYPE.KUBERNETES.POD.ECS]: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', - [MANAGED_ENTITY_TYPE.KUBERNETES.STATEFULSET.ECS]: - 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', + [ENTITY_TYPES.KUBERNETES.CLUSTER.ecs]: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', + [ENTITY_TYPES.KUBERNETES.CRONJOB.ecs]: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', + [ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs]: 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', + [ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs]: 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', + [ENTITY_TYPES.KUBERNETES.JOB.ecs]: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', + [ENTITY_TYPES.KUBERNETES.NODE.ecs]: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', + [ENTITY_TYPES.KUBERNETES.POD.ecs]: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', + [ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs]: 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', }; export const useDetailViewRedirect = () => { @@ -69,8 +67,8 @@ export const useDetailViewRedirect = () => { const identityValue = getSingleIdentityFieldValue(entity); switch (type) { - case MANAGED_ENTITY_TYPE.HOST: - case MANAGED_ENTITY_TYPE.CONTAINER: + case ENTITY_TYPES.HOST: + case ENTITY_TYPES.CONTAINER: return assetDetailsLocator?.getRedirectUrl({ assetId: identityValue, assetType: type, diff --git a/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts b/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts index fdd5b9b5ed957..4d8be9efc59c6 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts @@ -6,11 +6,11 @@ */ const createKubernetesEntity = (base: T) => ({ - ECS: `kubernetes_${base}_ecs` as const, - SEMCONV: `kubernetes_${base}_semconv` as const, + ecs: `kubernetes_${base}_ecs` as const, + semconv: `kubernetes_${base}_semconv` as const, }); -export const MANAGED_ENTITY_TYPE = { +export const ENTITY_TYPES = { HOST: 'host', CONTAINER: 'container', SERVICE: 'service', diff --git a/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts index 8d8fac054f471..adc07a2931b60 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/entity/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { MANAGED_ENTITY_TYPE } from './entity_types'; +export { ENTITY_TYPES } from './entity_types'; export { EntityDataStreamType } from './entity_data_stream_types'; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/index.ts index dcfefc09bdbf1..b4b7731d166b7 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/index.ts @@ -219,4 +219,4 @@ export { export { COMMON_OBSERVABILITY_GROUPING } from './embeddable_grouping'; -export { MANAGED_ENTITY_TYPE, EntityDataStreamType } from './entity'; +export { ENTITY_TYPES, EntityDataStreamType } from './entity'; From 753d52f2b8305ae137c38e3ddbd84e5379ba0c3f Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 30 Oct 2024 09:56:04 +0100 Subject: [PATCH 14/17] Fix typo in the file name --- ...o_plan_object.test.ts => esql_result_to_plain_objects.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename x-pack/packages/observability/observability_utils/es/utils/{esql_result_to_plan_object.test.ts => esql_result_to_plain_objects.test.ts} (100%) diff --git a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plan_object.test.ts b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.test.ts similarity index 100% rename from x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plan_object.test.ts rename to x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.test.ts From 76f572a3fc02e00558b34fc068ce8f7364e21d13 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 30 Oct 2024 14:02:43 +0100 Subject: [PATCH 15/17] CR fixes --- .../public/lib/entity_client.test.ts | 78 +++++-------------- .../public/lib/entity_client.ts | 12 +-- 2 files changed, 24 insertions(+), 66 deletions(-) diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts index 1e85059707f9d..dbaf1205cdf98 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts @@ -5,16 +5,16 @@ * 2.0. */ -import { EntityClient, EntityLatest } from './entity_client'; +import { EntityClient, EnitityInstance } from './entity_client'; import { coreMock } from '@kbn/core/public/mocks'; -const commonEntityFields: EntityLatest = { +const commonEntityFields: EnitityInstance = { entity: { last_seen_timestamp: '2023-10-09T00:00:00Z', id: '1', display_name: 'entity_name', definition_id: 'entity_definition_id', - } as EntityLatest['entity'], + } as EnitityInstance['entity'], }; describe('EntityClient', () => { @@ -25,8 +25,8 @@ describe('EntityClient', () => { }); describe('asKqlFilter', () => { - it('should return the value when indentity_fields is a single string', () => { - const entityLatest: EntityLatest = { + it('should return the kql filter', () => { + const entityLatest: EnitityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -41,8 +41,8 @@ describe('EntityClient', () => { expect(result).toEqual('service.name: my-service'); }); - it('should return values when indentity_fields is composed by multiple fields', () => { - const entityLatest: EntityLatest = { + it('should return the kql filter when indentity_fields is composed by multiple fields', () => { + const entityLatest: EnitityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -58,35 +58,8 @@ describe('EntityClient', () => { expect(result).toEqual('(service.name: my-service AND service.environment: staging)'); }); - it('should return identity fields values when an indentity field value is an array', () => { - const entityLatest: EntityLatest = { - entity: { - ...commonEntityFields.entity, - identity_fields: ['service.name', 'service.environment'], - type: 'service', - }, - service: { - name: 'my-service', - environment: ['prod', 'staging', 'dev'], - }, - }; - - const result = entityClient.asKqlFilter(entityLatest); - expect(result).toEqual( - '(service.name: my-service AND (service.environment: prod OR service.environment: staging OR service.environment: dev))' - ); - }); - - it('should throw an error when identity fields are missing', () => { - const entityLatest: EntityLatest = { - ...commonEntityFields, - }; - - expect(() => entityClient.asKqlFilter(entityLatest)).toThrow('Identity fields are missing'); - }); - it('should ignore fields that are not present in the entity', () => { - const entityLatest: EntityLatest = { + const entityLatest: EnitityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['host.name', 'foo.bar'], @@ -103,7 +76,7 @@ describe('EntityClient', () => { describe('getIdentityFieldsValue', () => { it('should return identity fields values', () => { - const entityLatest: EntityLatest = { + const entityLatest: EnitityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -120,7 +93,7 @@ describe('EntityClient', () => { }); it('should return identity fields values when indentity_fields is composed by multiple fields', () => { - const entityLatest: EntityLatest = { + const entityLatest: EnitityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -138,27 +111,8 @@ describe('EntityClient', () => { }); }); - it('should return identity fields values when an indentity field value is an array', () => { - const entityLatest: EntityLatest = { - entity: { - ...commonEntityFields.entity, - identity_fields: ['service.name', 'service.environment'], - type: 'service', - }, - service: { - name: 'my-service', - environment: ['prod', 'staging', 'dev'], - }, - }; - - expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({ - 'service.name': 'my-service', - 'service.environment': ['prod', 'staging', 'dev'], - }); - }); - it('should return identity fields when field is in the root', () => { - const entityLatest: EntityLatest = { + const entityLatest: EnitityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['name'], @@ -171,5 +125,15 @@ describe('EntityClient', () => { name: 'foo', }); }); + + it('should throw an error when identity fields are missing', () => { + const entityLatest: EnitityInstance = { + ...commonEntityFields, + }; + + expect(() => entityClient.getIdentityFieldsValue(entityLatest)).toThrow( + 'Identity fields are missing' + ); + }); }); }); diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.ts index 15e7808dced22..08794873ba930 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.ts @@ -39,7 +39,7 @@ type CreateEntityDefinitionQuery = QueryParamOf< ClientRequestParamsOf >; -export type EntityLatest = z.infer; +export type EnitityInstance = z.infer; export class EntityClient { public readonly repositoryClient: EntityManagerRepositoryClient['fetch']; @@ -90,16 +90,10 @@ export class EntityClient { } } - asKqlFilter(entityLatest: EntityLatest) { + asKqlFilter(entityLatest: EnitityInstance) { const identityFieldsValue = this.getIdentityFieldsValue(entityLatest); const nodes: KueryNode[] = Object.entries(identityFieldsValue).map(([identityField, value]) => { - if (Array.isArray(value)) { - return nodeTypes.function.buildNode( - 'or', - value.map((v) => nodeTypes.function.buildNode('is', identityField, v)) - ); - } return nodeTypes.function.buildNode('is', identityField, value); }); @@ -110,7 +104,7 @@ export class EntityClient { return toKqlExpression(kqlExpression); } - getIdentityFieldsValue(entityLatest: EntityLatest) { + getIdentityFieldsValue(entityLatest: EnitityInstance) { const { identity_fields: identityFields } = entityLatest.entity; if (!identityFields) { From 7a5b222826eddc3c9c70b5a40c2526b770fd491d Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 1 Nov 2024 10:08:52 +0100 Subject: [PATCH 16/17] Include k8s cluster dashboard redirection --- .../inventory/public/hooks/use_detail_view_redirect.test.ts | 1 + .../inventory/public/hooks/use_detail_view_redirect.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts index 37d68e53914dd..cf4993f871880 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -135,6 +135,7 @@ describe('useDetailViewRedirect', () => { [ [ENTITY_TYPES.KUBERNETES.CLUSTER.ecs, 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c'], + [ENTITY_TYPES.KUBERNETES.CLUSTER.semconv, 'kubernetes_otel-cluster-overview'], [ENTITY_TYPES.KUBERNETES.CRONJOB.ecs, 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013'], [ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs, 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013'], [ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs, 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013'], diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index d73ec6187662b..9be627fd6b655 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -25,6 +25,7 @@ import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record = { [ENTITY_TYPES.KUBERNETES.CLUSTER.ecs]: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', + [ENTITY_TYPES.KUBERNETES.CLUSTER.semconv]: 'kubernetes_otel-cluster-overview', [ENTITY_TYPES.KUBERNETES.CRONJOB.ecs]: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', [ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs]: 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', [ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs]: 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', From 64ec3519939a597d2889299abc14aab9d3927a37 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 4 Nov 2024 10:00:47 +0100 Subject: [PATCH 17/17] Clean up --- .../inventory/public/hooks/use_detail_view_redirect.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index 9be627fd6b655..23380dc3704de 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -18,7 +18,6 @@ import { useCallback } from 'react'; import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; -import { Exception } from 'handlebars'; import type { Entity } from '../../common/entities'; import { unflattenEntity } from '../../common/utils/unflatten_entity'; import { useKibana } from './use_kibana'; @@ -49,9 +48,7 @@ export const useDetailViewRedirect = () => { (entity: Entity) => { const identityFields = castArray(entity[ENTITY_IDENTITY_FIELDS]); if (identityFields.length > 1) { - throw new Exception( - `Multiple identity fields are not supported for ${entity[ENTITY_TYPE]}` - ); + throw new Error(`Multiple identity fields are not supported for ${entity[ENTITY_TYPE]}`); } const identityField = identityFields[0];