From c4c790e6edbd969962a292f5e9827ba126453948 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Thu, 26 Jun 2025 11:03:03 +0200 Subject: [PATCH] Add entity definitions --- .../common/saved_object_contants.ts | 2 + .../observability_navigation/common/types.ts | 75 +- .../observability_navigation/public/index.ts | 8 +- .../observability_navigation/public/plugin.ts | 2 +- .../observability_navigation/server/plugin.ts | 31 +- .../server/routes/index.ts | 2 + .../enrich_entity_definitions.ts | 45 + .../get_entity_associations.ts | 59 + .../get_entity_definitions.ts | 55 + .../internal/entity_definitions/route.ts | 63 + .../internal/setup/get_installed_packages.ts | 33 + .../internal/setup/get_navigation_items.ts | 219 ++++ .../server/routes/internal/setup/has_data.ts | 30 + .../server/routes/internal/setup/route.ts | 191 +--- .../server/routes/types.ts | 22 +- .../server/saved_objects/entity_definition.ts | 422 +++++++ .../server/saved_objects/index.ts | 2 + .../server/saved_objects/metric_definition.ts | 1012 +++++++++++++++++ .../saved_objects/navigation_overrides.ts | 18 +- .../components/entity_table/index.tsx | 74 -- .../components/page_content/page_content.tsx | 8 +- .../public/pages/metrics/dashboard/index.tsx | 14 +- .../infra/public/pages/metrics/index.tsx | 4 +- .../plugins/infra/public/plugin.ts | 23 +- 24 files changed, 2121 insertions(+), 293 deletions(-) create mode 100644 x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/enrich_entity_definitions.ts create mode 100644 x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/get_entity_associations.ts create mode 100644 x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/get_entity_definitions.ts create mode 100644 x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/route.ts create mode 100644 x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/get_installed_packages.ts create mode 100644 x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/get_navigation_items.ts create mode 100644 x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/has_data.ts create mode 100644 x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/entity_definition.ts create mode 100644 x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/metric_definition.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/components/entity_table/index.tsx diff --git a/x-pack/platform/plugins/shared/observability_navigation/common/saved_object_contants.ts b/x-pack/platform/plugins/shared/observability_navigation/common/saved_object_contants.ts index 6238d02c42029..da6ecd412b4b2 100644 --- a/x-pack/platform/plugins/shared/observability_navigation/common/saved_object_contants.ts +++ b/x-pack/platform/plugins/shared/observability_navigation/common/saved_object_contants.ts @@ -6,3 +6,5 @@ */ export const OBSERVABILITY_NAVIGATION_OVERRIDES = 'observability-navigation-overrides'; +export const OBSERVABILITY_ENTITY_DEFINITIONS = 'observability-entity-definitions'; +export const OBSERVABILITY_METRIC_DEFINITIONS = 'observability-metric-definitions'; diff --git a/x-pack/platform/plugins/shared/observability_navigation/common/types.ts b/x-pack/platform/plugins/shared/observability_navigation/common/types.ts index a0bd7e4b7b917..f33a7c8235da3 100644 --- a/x-pack/platform/plugins/shared/observability_navigation/common/types.ts +++ b/x-pack/platform/plugins/shared/observability_navigation/common/types.ts @@ -8,8 +8,9 @@ interface NavigationItemBase { id: string; title: string; - entityType?: string; + entityId?: string; dashboardId?: string; + order?: number; } export interface DynamicNavigationItem extends NavigationItemBase { @@ -19,3 +20,75 @@ export interface DynamicNavigationItem extends NavigationItemBase { export interface ObservabilityDynamicNavigation extends NavigationItemBase { subItems?: DynamicNavigationItem[]; } + +/* + * 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 interface Attribute { + ref: string; + requirement_level?: + | string + | { + conditionally_required: string; + }; +} + +export interface RelationShipAttributeMapping { + source_attribute: string; + target_attribute: string; +} + +export interface Relationship { + type: string; + target: string; + brief?: string; + attribute_mapping?: RelationShipAttributeMapping; +} + +export interface EntityDefinition { + id: string; + type: string; + stability: string; + name: string; + brief?: string; + attributes?: Attribute[]; + + // custom attributes + relationships?: Relationship[]; +} + +export interface MetricDefinition { + id: string; + type: 'metric'; + metric_name: string; + stability: 'development' | 'stable' | 'experimental'; + + brief?: string; + entity_associations?: string[]; + note?: string; + instrument: 'gauge' | 'counter' | 'updowncounter'; + unit?: string; + attributes?: Attribute[]; +} + +export type MetricDefinitionsResponse = Pick< + MetricDefinition, + 'id' | 'type' | 'instrument' | 'unit' +> & { metricName: string }; + +export interface EntityDefinitionsResponse { + id: string; + name: string; + attributes: string[]; + query?: string; + metrics: MetricDefinitionsResponse[]; + relationships: string[]; +} + +export interface EnrichedEntityDefinitionsResponse extends EntityDefinitionsResponse { + navigation?: ObservabilityDynamicNavigation; +} diff --git a/x-pack/platform/plugins/shared/observability_navigation/public/index.ts b/x-pack/platform/plugins/shared/observability_navigation/public/index.ts index 6c2ae04010fc8..b1a4e63b39f70 100644 --- a/x-pack/platform/plugins/shared/observability_navigation/public/index.ts +++ b/x-pack/platform/plugins/shared/observability_navigation/public/index.ts @@ -8,12 +8,18 @@ import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; import { Plugin } from './plugin'; import { ObservabilityNavigationPluginSetup, ObservabilityNavigationPluginStart } from './types'; -import { ObservabilityDynamicNavigation } from '../common/types'; +import { + ObservabilityDynamicNavigation, + EnrichedEntityDefinitionsResponse, + EntityDefinitionsResponse, +} from '../common/types'; export type { ObservabilityNavigationPluginSetup, ObservabilityNavigationPluginStart, ObservabilityDynamicNavigation, + EnrichedEntityDefinitionsResponse, + EntityDefinitionsResponse, }; export const plugin: PluginInitializer< diff --git a/x-pack/platform/plugins/shared/observability_navigation/public/plugin.ts b/x-pack/platform/plugins/shared/observability_navigation/public/plugin.ts index 6852ce5f2f7d4..71c601f50dbf1 100644 --- a/x-pack/platform/plugins/shared/observability_navigation/public/plugin.ts +++ b/x-pack/platform/plugins/shared/observability_navigation/public/plugin.ts @@ -55,7 +55,7 @@ const createObservabilityNavigationItemsObservable = once( ): Observable => { return from( repositoryClient - .fetch('GET /internal/observability_navigation', { + .fetch('GET /internal/observability/navigation', { signal: new AbortController().signal, }) .then( diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/plugin.ts b/x-pack/platform/plugins/shared/observability_navigation/server/plugin.ts index d6d01ed53abec..550264fa489e3 100644 --- a/x-pack/platform/plugins/shared/observability_navigation/server/plugin.ts +++ b/x-pack/platform/plugins/shared/observability_navigation/server/plugin.ts @@ -11,6 +11,7 @@ import type { CoreStart, Plugin, Logger, + KibanaRequest, } from '@kbn/core/server'; import { registerRoutes } from '@kbn/server-route-repository'; import { mapValues } from 'lodash'; @@ -21,8 +22,18 @@ import { ObservabilityNavigationPluginStartDependencies, } from './types'; import { observabilityNavigationRouteRepository } from './routes'; -import { ObservabilityNavigationRouteHandlerResources } from './routes/types'; -import { navigationOverrides, createNavigationOverrides } from './saved_objects'; +import { + ObservabilityNavigationRouteHandlerResources, + RouteHandlerScopedClients, +} from './routes/types'; +import { + navigationOverrides, + createNavigationOverrides, + createEntityDefinitions, + createMetricDefinitions, + observabilityEntityDefinitions, + observabilityMetricDefinitions, +} from './saved_objects'; export class ObservabilityNavigationPlugin implements @@ -46,8 +57,12 @@ export class ObservabilityNavigationPlugin plugins: ObservabilityNavigationPluginSetupDependencies ) { core.savedObjects.registerType(navigationOverrides); + core.savedObjects.registerType(observabilityEntityDefinitions); + core.savedObjects.registerType(observabilityMetricDefinitions); createNavigationOverrides(core); + createEntityDefinitions(core); + createMetricDefinitions(core); const routeHandlerPlugins = mapValues(plugins, (value, key) => { return { @@ -78,6 +93,18 @@ export class ObservabilityNavigationPlugin repository: observabilityNavigationRouteRepository, dependencies: { plugins: withCore, + getScopedClients: async ({ + request, + }: { + request: KibanaRequest; + }): Promise => { + const [coreStart, plugin] = await core.getStartServices(); + return { + scopedClusterClient: coreStart.elasticsearch.client.asScoped(request), + soClient: coreStart.savedObjects.getScopedClient(request), + packageClient: plugin.fleet?.packageService.asScoped(request), + }; + }, }, logger: this.logger, runDevModeChecks: this.isDev, diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/routes/index.ts b/x-pack/platform/plugins/shared/observability_navigation/server/routes/index.ts index 127448e62c259..3db1249ef0966 100644 --- a/x-pack/platform/plugins/shared/observability_navigation/server/routes/index.ts +++ b/x-pack/platform/plugins/shared/observability_navigation/server/routes/index.ts @@ -6,9 +6,11 @@ */ import { internalSetupRoutes } from './internal/setup/route'; +import { internalEntityDefinitionRoutes } from './internal/entity_definitions/route'; export const observabilityNavigationRouteRepository = { ...internalSetupRoutes, + ...internalEntityDefinitionRoutes, }; export type ObservabilityNavigationRouteRepository = typeof observabilityNavigationRouteRepository; diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/enrich_entity_definitions.ts b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/enrich_entity_definitions.ts new file mode 100644 index 0000000000000..3eadd156eba89 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/enrich_entity_definitions.ts @@ -0,0 +1,45 @@ +/* + * 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 { + EnrichedEntityDefinitionsResponse, + EntityDefinitionsResponse, + ObservabilityDynamicNavigation, +} from '../../../../common/types'; + +export async function enrichEntityDefinitions( + entityDefinitions: EntityDefinitionsResponse[], + navigation: ObservabilityDynamicNavigation[] +): Promise { + const navigationItemsMap = new Map(); + + for (const item of navigation) { + if (item.entityId) { + navigationItemsMap.set(item.entityId, item); + } + for (const subItem of item.subItems ?? []) { + if (subItem.entityId) { + navigationItemsMap.set(subItem.entityId, subItem); + } + } + } + + return entityDefinitions.map((definition) => { + const navigationItem = navigationItemsMap.get(definition.id); + if (navigationItem) { + return { + ...definition, + navigation: { + id: navigationItem.id, + title: navigationItem.title, + dashboardId: navigationItem.dashboardId, + }, + }; + } + return definition; + }); +} diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/get_entity_associations.ts b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/get_entity_associations.ts new file mode 100644 index 0000000000000..7eab36ecb3533 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/get_entity_associations.ts @@ -0,0 +1,59 @@ +/* + * 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 { SavedObjectsClientContract } from '@kbn/core/server'; +import { OBSERVABILITY_METRIC_DEFINITIONS } from '../../../../common/saved_object_contants'; + +import { MetricDefinitionsResponse } from '../../../../common/types'; + +import { MetricDefinitionSavedObject } from '../../../saved_objects/metric_definition'; + +export async function getEntityAssociations({ + namespace, + + soClient, +}: { + namespace: string; + soClient: SavedObjectsClientContract; +}): Promise> { + const metricDefinitionSavedObject = await soClient.get( + OBSERVABILITY_METRIC_DEFINITIONS, + namespace + ); + + if (!metricDefinitionSavedObject) { + throw new Error(`Entity definition for type "${namespace}" not found`); + } + + const entityAssociationsMap = metricDefinitionSavedObject.attributes.groups.reduce( + (acc, definition) => { + for (const association of definition.entity_associations ?? []) { + if (!association) { + continue; + } + + const metricDefinition: MetricDefinitionsResponse = { + id: definition.id, + instrument: definition.instrument, + metricName: definition.metric_name, + unit: definition.unit, + type: definition.type, + }; + + const existing = acc.get(association); + if (existing) { + existing.push(metricDefinition); + } else { + acc.set(association, [metricDefinition]); + } + } + return acc; + }, + new Map() + ); + + return entityAssociationsMap; +} diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/get_entity_definitions.ts b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/get_entity_definitions.ts new file mode 100644 index 0000000000000..b9e114a518d42 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/get_entity_definitions.ts @@ -0,0 +1,55 @@ +/* + * 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 { SavedObjectsClientContract } from '@kbn/core/server'; +import { OBSERVABILITY_ENTITY_DEFINITIONS } from '../../../../common/saved_object_contants'; +import { EntityDefinitionsResponse } from '../../../../common/types'; +import { EntityDefinitionSavedObject } from '../../../saved_objects/entity_definition'; +import { getEntityAssociations } from './get_entity_associations'; + +export async function getEntityDefinitions({ + namespace, + soClient, +}: { + namespace: string; + soClient: SavedObjectsClientContract; +}): Promise { + const entityDefinitionSavedObject = await soClient.get( + OBSERVABILITY_ENTITY_DEFINITIONS, + namespace + ); + + if (!entityDefinitionSavedObject) { + throw new Error(`Entity definition for namespace "${namespace}" not found`); + } + + const entitiesRelationshipsMap = new Map(); + for (const definition of entityDefinitionSavedObject.attributes.groups) { + for (const relationship of definition.relationships ?? []) { + const existingRelationships = entitiesRelationshipsMap.get(relationship.target) || []; + existingRelationships.push(definition.id); + entitiesRelationshipsMap.set(relationship.target, existingRelationships); + } + } + + const entityAssociationsMap = await getEntityAssociations({ + namespace, + soClient, + }); + + return entityDefinitionSavedObject.attributes.groups.map((entityDefinition) => { + return { + id: entityDefinition.id, + name: entityDefinition.name, + attributes: + entityDefinition.attributes + ?.filter((attributes) => attributes.requirement_level !== 'opt_in') + .map((attribute) => attribute.ref) ?? [], + metrics: entityAssociationsMap.get(entityDefinition.name) ?? [], + relationships: entitiesRelationshipsMap.get(entityDefinition.id) ?? [], + }; + }); +} diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/route.ts b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/route.ts new file mode 100644 index 0000000000000..d2985c27f73d1 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/entity_definitions/route.ts @@ -0,0 +1,63 @@ +/* + * 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 { z } from '@kbn/zod'; +import { createServerRoute } from '../../../create_server_route'; +import { EntityDefinitionsResponse } from '../../../../common/types'; +import { getEntityDefinitions } from './get_entity_definitions'; +import { getNavigationItems } from '../setup/get_navigation_items'; +import { enrichEntityDefinitions } from './enrich_entity_definitions'; + +const entityDefinitionsRoute = createServerRoute({ + endpoint: 'GET /internal/observability/entity_definitions/{namespace}', + options: { + access: 'internal', + summary: 'Get entity definitions', + description: 'Fetches entity definitions', + availability: { + stability: 'experimental', + }, + }, + params: z.object({ + query: z.object({ + entityId: z.optional(z.string()), + }), + path: z.object({ + namespace: z.string(), + }), + }), + security: { + authz: { + enabled: false, + reason: 'The route is opted out of the authorization since it is a POC', + }, + }, + async handler({ params, request, getScopedClients }): Promise { + const { soClient, packageClient, scopedClusterClient } = await getScopedClients({ request }); + const { namespace } = params.path; + const { entityId } = params.query; + + const [entityDefinitions, navigationItems] = await Promise.all([ + getEntityDefinitions({ soClient, namespace }), + getNavigationItems({ + soClient, + packageClient, + scopedClusterClient, + includeEntityDefinitions: false, + }), + ]); + + return enrichEntityDefinitions( + entityId ? entityDefinitions.filter((def) => def.id === entityId) : entityDefinitions, + navigationItems + ); + }, +}); + +export const internalEntityDefinitionRoutes = { + ...entityDefinitionsRoute, +}; diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/get_installed_packages.ts b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/get_installed_packages.ts new file mode 100644 index 0000000000000..ce4802336fa62 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/get_installed_packages.ts @@ -0,0 +1,33 @@ +/* + * 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 { IScopedClusterClient } from '@kbn/core/server'; +import { PackageClient } from '@kbn/fleet-plugin/server'; +import { hasData } from './has_data'; + +export async function getInstalledPackages({ + packageClient, + scopedClusterClient, + ensureInstalled = false, +}: { + packageClient: PackageClient; + scopedClusterClient: IScopedClusterClient; + ensureInstalled?: boolean; +}) { + if (ensureInstalled) { + const hasOtelData = await hasData({ scopedClusterClient }); + + if (hasOtelData) { + await packageClient.ensureInstalledPackage({ pkgName: 'kubernetes_otel' }); + } + } + + return Promise.all([ + packageClient.getInstallation('kubernetes'), + packageClient.getInstallation('kubernetes_otel'), + ]); +} diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/get_navigation_items.ts b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/get_navigation_items.ts new file mode 100644 index 0000000000000..73708829a76a7 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/get_navigation_items.ts @@ -0,0 +1,219 @@ +/* + * 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 { PackageClient } from '@kbn/fleet-plugin/server'; +import { IScopedClusterClient, SavedObjectsClientContract } from '@kbn/core/server'; +import { NavigationOverridesSavedObject } from '../../../saved_objects/navigation_overrides'; +import { OBSERVABILITY_NAVIGATION_OVERRIDES } from '../../../../common/saved_object_contants'; +import { ObservabilityDynamicNavigation } from '../../../../common/types'; +import { getEntityDefinitions } from '../entity_definitions/get_entity_definitions'; +import { getInstalledPackages } from './get_installed_packages'; + +const KUBERNETES = 'kubernetes'; +const DOCKER = 'docker'; + +export async function getNavigationItems({ + packageClient, + soClient, + scopedClusterClient, + includeEntityDefinitions = true, +}: { + packageClient?: PackageClient; + soClient: SavedObjectsClientContract; + scopedClusterClient: IScopedClusterClient; + includeEntityDefinitions?: boolean; +}): Promise { + const [...installedPackages] = packageClient + ? await getInstalledPackages({ packageClient, scopedClusterClient, ensureInstalled: true }) + : []; + + const [...navigationOverrides] = await Promise.all([ + soClient.get(OBSERVABILITY_NAVIGATION_OVERRIDES, KUBERNETES), + soClient.get(OBSERVABILITY_NAVIGATION_OVERRIDES, DOCKER), + ]); + + if (installedPackages.length === 0 && !navigationOverrides) { + return []; + } + + // Mock data simulating the installed package's items returned by installedPackage.installed_kibana + const mockKubernetesInstalledPackage = installedPackages.some((pkg) => pkg?.name === KUBERNETES) + ? [ + { + id: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', + entityId: 'entity.k8s.cronjob', + sideNavTitle: 'Cron jobs', + sideNavOrder: 900, + type: 'dashboard', + }, + { + id: 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', + entityId: 'entity.k8s.statefulset', + sideNavTitle: 'Stateful sets', + sideNavOrder: 600, + type: 'dashboard', + }, + + { + id: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', + entityId: 'entity.k8s.pod', + sideNavTitle: 'Pods', + sideNavOrder: 300, + type: 'dashboard', + }, + { + id: 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', + entityId: 'entity.k8s.deployment', + sideNavTitle: 'Deployments', + sideNavOrder: 400, + type: 'dashboard', + }, + { + id: 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', + entityId: 'entity.k8s.daemonset', + sideNavTitle: 'Daemon sets', + sideNavOrder: 700, + type: 'dashboard', + }, + { + id: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', + entityId: 'entity.k8s.job', + sideNavTitle: 'Jobs', + sideNavOrder: 800, + type: 'dashboard', + }, + { + id: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', + entityId: 'entity.k8s.node', + sideNavTitle: 'Nodes', + sideNavOrder: 200, + type: 'dashboard', + }, + { + id: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', + entityId: 'entity.k8s.cluster', + sideNavTitle: 'Cluster', + sideNavOrder: 100, + type: 'dashboard', + }, + ] + : []; + + // Mock data simulating the installed package's items returned by installedPackage.installed_kibana + const mockKubernetesOtelInstalledPackage = installedPackages.some( + (pkg) => pkg?.name === `${KUBERNETES}_otel` + ) + ? [ + { + id: 'kubernetes_otel-cluster-overview', + entityId: 'entity.k8s.cluster', + sideNavTitle: 'Cluster', + sideNavOrder: 100, + type: 'dashboard', + }, + ] + : []; + + const entityDefinitions = await getEntityDefinitions({ namespace: KUBERNETES, soClient }); + + const navigationFromEntityDefinitions = entityDefinitions.map((entity, index) => ({ + id: `kubernetes_otel-cluster-overview`, + entityId: entity.id, + sideNavTitle: (entity.name.split('.').at(-1) ?? entity.name).replace(/^./, (c) => + c.toUpperCase() + ), + sideNavOrder: (index || 1) * 100, + type: 'dashboard', + })); + + const navigationsCombined = Array.from( + new Map( + [ + ...navigationFromEntityDefinitions, + ...mockKubernetesInstalledPackage, + ...mockKubernetesOtelInstalledPackage, + ] + .filter((p) => !!p.sideNavTitle) + .map((item) => [item.entityId, item]) + ).values() + ); + const integrationNavigation = + navigationsCombined.length > 0 + ? [ + { + id: `${KUBERNETES.toLowerCase().replace(/[\.\s]/g, '-')}`, + title: KUBERNETES, + subItems: navigationsCombined.map((item) => { + return { + id: `${item.sideNavTitle.toLowerCase().replace(/[\.\s]/g, '-')}`, + title: item.sideNavTitle, + entityId: item.entityId, + dashboardId: item.id, + order: item.sideNavOrder, + }; + }), + }, + ] + : []; + + return ( + mergeNavigationItems( + integrationNavigation, + navigationOverrides.flatMap((item) => item.attributes) + ) ?? [] + ); +} + +function mergeNavigationItems( + integrationNavigation: ObservabilityDynamicNavigation[], + navigationOverridesItems: NavigationOverridesSavedObject[] +): ObservabilityDynamicNavigation[] { + const overrideMap = new Map(); + + // Flatten all override items into a map by id + for (const override of navigationOverridesItems.flatMap((o) => o.navigation ?? [])) { + overrideMap.set(override.id, override); + } + + const resultMap = new Map(); + + for (const integrationItem of integrationNavigation) { + const overrideItem = overrideMap.get(integrationItem.id); + + if (!overrideItem) { + resultMap.set(integrationItem.id, integrationItem); + continue; + } + + const subItemMap = new Map< + string | undefined, + NonNullable[number] + >(); + + for (const subItem of integrationItem.subItems ?? []) { + subItemMap.set(subItem.entityId ?? subItem.id, subItem); + } + + for (const subItem of overrideItem.subItems ?? []) { + subItemMap.set(subItem.entityId ?? subItem.id, subItem); + } + + resultMap.set(integrationItem.id, { + ...integrationItem, + ...overrideItem, + subItems: + Array.from(subItemMap.values()).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) ?? [], + }); + + overrideMap.delete(integrationItem.id); + } + + for (const [id, overrideItem] of overrideMap.entries()) { + resultMap.set(id, overrideItem); + } + + return Array.from(resultMap.values()).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) ?? []; +} diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/has_data.ts b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/has_data.ts new file mode 100644 index 0000000000000..6b20672a59df0 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/has_data.ts @@ -0,0 +1,30 @@ +/* + * 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 { IScopedClusterClient } from '@kbn/core/server'; +export async function hasData({ + scopedClusterClient, +}: { + scopedClusterClient: IScopedClusterClient; +}) { + const otelData = await scopedClusterClient.asCurrentUser.search({ + index: 'metrics-*.otel-*', + ignore_unavailable: true, + allow_no_indices: true, + track_total_hits: true, + terminate_after: 1, + size: 0, + query: { + bool: { + filter: [{ term: { ['data_stream.dataset']: 'k8sclusterreceiver.otel' } }], + }, + }, + }); + + const totalHits = otelData?.hits?.total; + return typeof totalHits === 'number' ? totalHits !== 0 : totalHits?.value !== 0; +} diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/route.ts b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/route.ts index 4d856e2bbee85..348a56ddd4a51 100644 --- a/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/route.ts +++ b/x-pack/platform/plugins/shared/observability_navigation/server/routes/internal/setup/route.ts @@ -5,16 +5,12 @@ * 2.0. */ -import { NavigationOverridesSavedObject } from '../../../saved_objects/navigation_overrides'; -import { OBSERVABILITY_NAVIGATION_OVERRIDES } from '../../../../common/saved_object_contants'; import { ObservabilityDynamicNavigation } from '../../../../common/types'; import { createServerRoute } from '../../../create_server_route'; - -const KUBERNETES = 'kubernetes'; -const DOCKER = 'docker'; +import { getNavigationItems } from './get_navigation_items'; const infraSideNavRoute = createServerRoute({ - endpoint: 'GET /internal/observability_navigation', + endpoint: 'GET /internal/observability/navigation', options: { access: 'internal', summary: 'Get observabilty dynamic side navigation', @@ -29,188 +25,17 @@ const infraSideNavRoute = createServerRoute({ reason: 'The route is opted out of the authorization since it is a POC', }, }, - async handler({ request, plugins, context }): Promise { - const [fleetStart, core] = await Promise.all([plugins.fleet?.start(), context.core]); - const packageClient = fleetStart?.packageService.asScoped(request); + async handler({ request, plugins, getScopedClients }): Promise { + const { packageClient, soClient, scopedClusterClient } = await getScopedClients({ request }); - const otelData = await core.elasticsearch.client.asCurrentUser.search({ - index: 'metrics-*.otel-*', - ignore_unavailable: true, - allow_no_indices: true, - track_total_hits: true, - terminate_after: 1, - size: 0, - query: { - bool: { - filter: [{ term: { ['data_stream.dataset']: 'k8sclusterreceiver.otel' } }], - }, - }, + return getNavigationItems({ + packageClient, + soClient, + scopedClusterClient, }); - - const totalHits = otelData?.hits?.total; - const hasOtelData = typeof totalHits === 'number' ? totalHits !== 0 : totalHits?.value !== 0; - - const [installedPackage, ...navigationOverrides] = await Promise.all([ - packageClient?.getInstallation(KUBERNETES), - core.savedObjects.client.get( - OBSERVABILITY_NAVIGATION_OVERRIDES, - KUBERNETES - ), - core.savedObjects.client.get( - OBSERVABILITY_NAVIGATION_OVERRIDES, - DOCKER - ), - ]); - - const otelPackageInstalled = installedPackage ? [installedPackage] : []; - - if (hasOtelData && !installedPackage) { - // We will just install the otel package if it is not installed for now - await packageClient?.ensureInstalledPackage({ pkgName: 'kubernetes_otel' }); - const installedOtelKubernetes = await packageClient?.getInstallation('kubernetes_otel'); - if (installedOtelKubernetes) otelPackageInstalled.push(installedOtelKubernetes); - } - - if ((!installedPackage || otelPackageInstalled.length === 0) && !navigationOverrides) { - return []; - } - - // It will come from the entities definiition later - // For now we will just use the entities defined in the semconv - const k8sEntitiesSemConv = [ - { - id: 'entity.k8s.cluster', // -> Use to map to entityType? - type: 'entity', - stability: 'development', - name: 'k8s.cluster', - brief: 'A Kubernetes Cluster.', - attributes: [{ ref: 'k8s.cluster.name' }, { ref: 'k8s.cluster.uid' }], - }, - { - id: 'entity.k8s.node', - type: 'entity', - stability: 'development', - name: 'k8s.node', - brief: 'A Kubernetes Node object.', - attributes: [ - { ref: 'k8s.node.name' }, - { ref: 'k8s.node.uid' }, - { - ref: 'k8s.node.label', - requirement_level: 'opt_in', - }, - { - ref: 'k8s.node.annotation', - requirement_level: 'opt_in', - }, - ], - }, - ]; - const otelMenuItems = k8sEntitiesSemConv.map((entity, index) => ({ - // id: `kubernetes_otel-${entity.id}`, - id: `kubernetes_otel-cluster-overview`, - entityType: entity.id, - sideNavTitle: entity.brief, - sideNavOrder: (index || 1) * 100, - type: 'dashboard', - })); - - // Mock data simulating the installed package's items returned by installedPackage.installed_kibana - const mockInstalledPackage = - installedPackage && otelPackageInstalled.length > 0 && hasOtelData - ? [ - { - id: 'kubernetes_otel-cluster-overview', - entityType: 'k8s.overview', - sideNavTitle: 'Overview (Otel)', - sideNavOrder: 100, - type: 'dashboard', - }, - ] - : []; - - const otelNavigationItemsSorted = - [...otelMenuItems, ...mockInstalledPackage] - .filter((p) => !!p.sideNavTitle) - .sort((a, b) => (a.sideNavOrder ?? 0) - (b.sideNavOrder ?? 0)) ?? []; - - const integrationNavigation = - otelNavigationItemsSorted.length > 0 - ? [ - { - id: `${KUBERNETES.toLowerCase().replace(/[\.\s]/g, '-')}`, - title: KUBERNETES, - subItems: otelNavigationItemsSorted.map((item) => { - return { - id: `${item.sideNavTitle.toLowerCase().replace(/[\.\s]/g, '-')}`, - title: item.sideNavTitle, - entityType: item.entityType, - dashboardId: item.id, - }; - }), - }, - ] - : []; - - return ( - mergeNavigationItems( - integrationNavigation, - navigationOverrides.flatMap((item) => item.attributes) - ) ?? [] - ); }, }); -function mergeNavigationItems( - integrationNavigation: ObservabilityDynamicNavigation[], - navigationOverridesItems: NavigationOverridesSavedObject[] -): ObservabilityDynamicNavigation[] { - const overrideMap = new Map(); - - // Flatten all override items into a map by id - for (const override of navigationOverridesItems.flatMap((o) => o.navigation ?? [])) { - overrideMap.set(override.id, override); - } - - const resultMap = new Map(); - - for (const integrationItem of integrationNavigation) { - const overrideItem = overrideMap.get(integrationItem.id); - - if (!overrideItem) { - resultMap.set(integrationItem.id, integrationItem); - continue; - } - - const subItemMap = new Map< - string | undefined, - NonNullable[number] - >(); - - for (const subItem of integrationItem.subItems ?? []) { - subItemMap.set(subItem.entityType, subItem); - } - - for (const subItem of overrideItem.subItems ?? []) { - subItemMap.set(subItem.entityType, subItem); - } - - resultMap.set(integrationItem.id, { - ...integrationItem, - ...overrideItem, - subItems: Array.from(subItemMap.values()), - }); - - overrideMap.delete(integrationItem.id); - } - - for (const [id, overrideItem] of overrideMap.entries()) { - resultMap.set(id, overrideItem); - } - - return Array.from(resultMap.values()); -} - export const internalSetupRoutes = { ...infraSideNavRoute, }; diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/routes/types.ts b/x-pack/platform/plugins/shared/observability_navigation/server/routes/types.ts index 0ea5cb8d7dca0..2960cab952786 100644 --- a/x-pack/platform/plugins/shared/observability_navigation/server/routes/types.ts +++ b/x-pack/platform/plugins/shared/observability_navigation/server/routes/types.ts @@ -9,9 +9,11 @@ import type { CoreSetup, CoreStart, IScopedClusterClient, + KibanaRequest, SavedObjectsClientContract, } from '@kbn/core/server'; import { DefaultRouteHandlerResources } from '@kbn/server-route-repository'; +import { PackageClient } from '@kbn/fleet-plugin/server'; import { ObservabilityNavigationPluginSetupDependencies, ObservabilityNavigationPluginStartDependencies, @@ -50,10 +52,24 @@ type PluginContractResolveDependenciesSetup = { }; }; -export interface ObservabilityNavigationRouteHandlerResources - extends Omit { - context: ObservabilityNavigationRequestHandlerContext; +type GetScopedClients = ({ + request, +}: { + request: KibanaRequest; +}) => Promise; + +export interface RouteDependencies { + getScopedClients: GetScopedClients; plugins: PluginContractResolveCore & PluginContractResolveDependenciesSetup & PluginContractResolveDependenciesStart; } + +export interface RouteHandlerScopedClients { + scopedClusterClient: IScopedClusterClient; + soClient: SavedObjectsClientContract; + packageClient?: PackageClient; +} + +export type ObservabilityNavigationRouteHandlerResources = RouteDependencies & + DefaultRouteHandlerResources; diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/entity_definition.ts b/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/entity_definition.ts new file mode 100644 index 0000000000000..00dcd9fc4c06b --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/entity_definition.ts @@ -0,0 +1,422 @@ +/* + * 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 { CoreSetup, SavedObjectsClient, SavedObjectsType } from '@kbn/core/server'; +import { i18n } from '@kbn/i18n'; +import { EntityDefinition } from '../../common/types'; +import { OBSERVABILITY_ENTITY_DEFINITIONS } from '../../common/saved_object_contants'; + +export interface EntityDefinitionSavedObject { + groups: EntityDefinition[]; +} + +const entityDefinitionMapping: SavedObjectsType['mappings'] = { + dynamic: false, + properties: { + groups: { + type: 'nested', + dynamic: false, + properties: { + id: { + type: 'keyword', + }, + type: { + type: 'keyword', + }, + stability: { + type: 'keyword', + }, + name: { + type: 'keyword', + }, + query: { + type: 'text', + }, + brief: { + type: 'text', + }, + attributes: { + type: 'nested', + dynamic: false, + properties: { + ref: { + type: 'keyword', + }, + requirement_level: { + type: 'keyword', + }, + }, + }, + // not part of semconv + relationships: { + type: 'nested', + dynamic: false, + properties: { + type: { + type: 'keyword', + }, + target: { + type: 'keyword', + }, + brief: { + type: 'text', + }, + attribute_mapping: { + type: 'object', + dynamic: false, + properties: { + source_attribute: { type: 'keyword' }, + target_attribute: { type: 'keyword' }, + }, + }, + }, + }, + }, + }, + }, +}; + +export const observabilityEntityDefinitions: SavedObjectsType = { + name: OBSERVABILITY_ENTITY_DEFINITIONS, + hidden: false, + namespaceType: 'multiple', + mappings: entityDefinitionMapping, + management: { + importableAndExportable: true, + icon: 'apmApp', + getTitle: () => + i18n.translate('xpack.entityDefinitions.overrides.title', { + defaultMessage: 'Entity definition', + }), + }, +}; + +export async function createEntityDefinitions(core: CoreSetup) { + const [coreStart] = await core.getStartServices(); + + const savedObjectsClient = new SavedObjectsClient( + coreStart.savedObjects.createInternalRepository() + ); + + await Promise.all([ + savedObjectsClient.create( + OBSERVABILITY_ENTITY_DEFINITIONS, + { + groups: [ + { + id: 'entity.k8s.cluster', + type: 'entity', + stability: 'development', + name: 'k8s.cluster', + brief: 'A Kubernetes Cluster.', + attributes: [{ ref: 'k8s.cluster.name' }, { ref: 'k8s.cluster.uid' }], + }, + { + id: 'entity.k8s.node', + type: 'entity', + stability: 'development', + name: 'k8s.node', + brief: 'A Kubernetes Node object.', + attributes: [ + { ref: 'k8s.node.name' }, + { ref: 'k8s.node.uid' }, + { ref: 'k8s.node.label', requirement_level: 'opt_in' }, + { ref: 'k8s.node.annotation', requirement_level: 'opt_in' }, + ], + relationships: [ + { + type: 'belongs_to', + target: 'entity.k8s.cluster', + brief: 'The cluster this node belongs to.', + }, + ], + }, + { + id: 'entity.k8s.namespace', + type: 'entity', + stability: 'development', + name: 'k8s.namespace', + brief: 'A Kubernetes Namespace.', + attributes: [ + { ref: 'k8s.namespace.name' }, + { ref: 'k8s.namespace.label', requirement_level: 'opt_in' }, + { ref: 'k8s.namespace.annotation', requirement_level: 'opt_in' }, + ], + relationships: [ + { + type: 'belongs_to', + target: 'entity.k8s.cluster', + brief: 'The cluster this namespace belongs to.', + }, + ], + }, + { + id: 'entity.k8s.pod', + type: 'entity', + stability: 'development', + name: 'k8s.pod', + brief: 'A Kubernetes Pod object.', + attributes: [ + { ref: 'k8s.pod.uid' }, + { ref: 'k8s.pod.name' }, + { ref: 'k8s.pod.label', requirement_level: 'opt_in' }, + { ref: 'k8s.pod.annotation', requirement_level: 'opt_in' }, + ], + relationships: [ + { + type: 'scheduled_on', + target: 'entity.k8s.node', + brief: 'The node this pod is scheduled on.', + attribute_mapping: { + source_attribute: 'k8s.pod.node.name', + target_attribute: 'k8s.node.name', + }, + }, + { + type: 'belongs_to', + target: 'entity.k8s.namespace', + brief: 'The namespace this pod belongs to.', + attribute_mapping: { + source_attribute: 'k8s.pod.namespace', + target_attribute: 'k8s.namespace.name', + }, + }, + ], + }, + { + id: 'entity.k8s.container', + type: 'entity', + stability: 'development', + name: 'k8s.container', + brief: + 'A container in a [PodTemplate](https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates).', + attributes: [ + { ref: 'k8s.container.name' }, + { ref: 'k8s.container.restart_count' }, + { ref: 'k8s.container.status.last_terminated_reason' }, + ], + relationships: [ + { + type: 'part_of', + target: 'entity.k8s.pod', + brief: 'The pod this container is part of.', + attribute_mapping: { + source_attribute: 'k8s.container.pod.uid', + target_attribute: 'k8s.pod.uid', + }, + }, + ], + }, + { + id: 'entity.k8s.replicaset', + type: 'entity', + stability: 'development', + name: 'k8s.replicaset', + brief: 'A Kubernetes ReplicaSet object.', + attributes: [ + { ref: 'k8s.replicaset.uid' }, + { ref: 'k8s.replicaset.name' }, + { ref: 'k8s.replicaset.label', requirement_level: 'opt_in' }, + { ref: 'k8s.replicaset.annotation', requirement_level: 'opt_in' }, + ], + relationships: [ + { + type: 'managed_by', + target: 'entity.k8s.deployment', + brief: 'The deployment managing this ReplicaSet.', + attribute_mapping: { + source_attribute: 'k8s.replicaset.owner.name', + target_attribute: 'k8s.deployment.name', + }, + }, + ], + }, + { + id: 'entity.k8s.deployment', + type: 'entity', + stability: 'development', + name: 'k8s.deployment', + brief: 'A Kubernetes Deployment object.', + attributes: [ + { ref: 'k8s.deployment.uid' }, + { ref: 'k8s.deployment.name' }, + { ref: 'k8s.deployment.label', requirement_level: 'opt_in' }, + { ref: 'k8s.deployment.annotation', requirement_level: 'opt_in' }, + ], + relationships: [ + { + type: 'belongs_to', + target: 'entity.k8s.namespace', + brief: 'The namespace this deployment belongs to.', + attribute_mapping: { + source_attribute: 'k8s.deployment.namespace', + target_attribute: 'k8s.namespace.name', + }, + }, + ], + }, + { + id: 'entity.k8s.statefulset', + type: 'entity', + stability: 'development', + name: 'k8s.statefulset', + brief: 'A Kubernetes StatefulSet object.', + attributes: [ + { ref: 'k8s.statefulset.uid' }, + { ref: 'k8s.statefulset.name' }, + { ref: 'k8s.statefulset.label', requirement_level: 'opt_in' }, + { ref: 'k8s.statefulset.annotation', requirement_level: 'opt_in' }, + ], + relationships: [ + { + type: 'belongs_to', + target: 'entity.k8s.namespace', + brief: 'The namespace this statefulset belongs to.', + attribute_mapping: { + source_attribute: 'k8s.statefulset.namespace', + target_attribute: 'k8s.namespace.name', + }, + }, + ], + }, + { + id: 'entity.k8s.daemonset', + type: 'entity', + stability: 'development', + name: 'k8s.daemonset', + brief: 'A Kubernetes DaemonSet object.', + attributes: [ + { ref: 'k8s.daemonset.uid' }, + { ref: 'k8s.daemonset.name' }, + { ref: 'k8s.daemonset.label', requirement_level: 'opt_in' }, + { ref: 'k8s.daemonset.annotation', requirement_level: 'opt_in' }, + ], + relationships: [ + { + type: 'belongs_to', + target: 'entity.k8s.namespace', + brief: 'The namespace this daemonset belongs to.', + attribute_mapping: { + source_attribute: 'k8s.daemonset.namespace', + target_attribute: 'k8s.namespace.name', + }, + }, + ], + }, + { + id: 'entity.k8s.job', + type: 'entity', + stability: 'development', + name: 'k8s.job', + brief: 'A Kubernetes Job object.', + attributes: [ + { ref: 'k8s.job.uid' }, + { ref: 'k8s.job.name' }, + { ref: 'k8s.job.label', requirement_level: 'opt_in' }, + { ref: 'k8s.job.annotation', requirement_level: 'opt_in' }, + ], + relationships: [ + { + type: 'belongs_to', + target: 'entity.k8s.namespace', + brief: 'The namespace this job belongs to.', + attribute_mapping: { + source_attribute: 'k8s.job.namespace', + target_attribute: 'k8s.namespace.name', + }, + }, + ], + }, + { + id: 'entity.k8s.cronjob', + type: 'entity', + stability: 'development', + name: 'k8s.cronjob', + brief: 'A Kubernetes CronJob object.', + attributes: [ + { ref: 'k8s.cronjob.uid' }, + { ref: 'k8s.cronjob.name' }, + { ref: 'k8s.cronjob.label', requirement_level: 'opt_in' }, + { ref: 'k8s.cronjob.annotation', requirement_level: 'opt_in' }, + ], + relationships: [ + { + type: 'belongs_to', + target: 'entity.k8s.namespace', + brief: 'The namespace this cronjob belongs to.', + attribute_mapping: { + source_attribute: 'k8s.cronjob.namespace', + target_attribute: 'k8s.namespace.name', + }, + }, + ], + }, + { + id: 'entity.k8s.replicationcontroller', + type: 'entity', + stability: 'development', + name: 'k8s.replicationcontroller', + brief: 'A Kubernetes ReplicationController object.', + attributes: [ + { ref: 'k8s.replicationcontroller.uid' }, + { ref: 'k8s.replicationcontroller.name' }, + ], + relationships: [ + { + type: 'belongs_to', + target: 'entity.k8s.namespace', + brief: 'The namespace this replicationcontroller belongs to.', + attribute_mapping: { + source_attribute: 'k8s.replicationcontroller.namespace', + target_attribute: 'k8s.namespace.name', + }, + }, + ], + }, + { + id: 'entity.k8s.hpa', + type: 'entity', + stability: 'development', + name: 'k8s.hpa', + brief: 'A Kubernetes HorizontalPodAutoscaler object.', + attributes: [ + { ref: 'k8s.hpa.uid' }, + { ref: 'k8s.hpa.name' }, + { ref: 'k8s.hpa.scaletargetref.kind', requirement_level: 'recommended' }, + { ref: 'k8s.hpa.scaletargetref.name', requirement_level: 'recommended' }, + { ref: 'k8s.hpa.scaletargetref.api_version', requirement_level: 'recommended' }, + ], + }, + { + id: 'entity.k8s.resourcequota', + type: 'entity', + stability: 'development', + name: 'k8s.resourcequota', + brief: 'A Kubernetes ResourceQuota object.', + attributes: [{ ref: 'k8s.resourcequota.uid' }, { ref: 'k8s.resourcequota.name' }], + relationships: [ + { + type: 'belongs_to', + target: 'entity.k8s.namespace', + brief: 'The namespace this resourcequota belongs to.', + attribute_mapping: { + source_attribute: 'k8s.resourcequota.namespace', + target_attribute: 'k8s.namespace.name', + }, + }, + ], + }, + // k8s.volume and k8s.service are not included in the entity definitions + ], + }, + { + id: 'kubernetes', + overwrite: true, + } + ), + ]); +} diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/index.ts b/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/index.ts index a7e4d7f68fcca..c32ad5f2c4689 100644 --- a/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/index.ts +++ b/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/index.ts @@ -5,3 +5,5 @@ * 2.0. */ export { navigationOverrides, createNavigationOverrides } from './navigation_overrides'; +export { createEntityDefinitions, observabilityEntityDefinitions } from './entity_definition'; +export { createMetricDefinitions, observabilityMetricDefinitions } from './metric_definition'; diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/metric_definition.ts b/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/metric_definition.ts new file mode 100644 index 0000000000000..0696c82a01440 --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/metric_definition.ts @@ -0,0 +1,1012 @@ +/* + * 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 { CoreSetup, SavedObjectsClient, SavedObjectsType } from '@kbn/core/server'; +import { i18n } from '@kbn/i18n'; +import { OBSERVABILITY_METRIC_DEFINITIONS } from '../../common/saved_object_contants'; +import { MetricDefinition } from '../../common/types'; + +export interface MetricDefinitionSavedObject { + groups: MetricDefinition[]; +} + +export const metricDefinitionSavedObjectMapping: SavedObjectsType['mappings'] = { + dynamic: false, + properties: { + id: { type: 'keyword' }, + type: { type: 'keyword' }, + metric_name: { type: 'keyword' }, + stability: { type: 'keyword' }, + brief: { type: 'text' }, + entity_associations: { + type: 'nested', + dynamic: false, + properties: { + association: { type: 'keyword' }, + }, + }, + note: { type: 'text' }, + instrument: { type: 'keyword' }, + unit: { type: 'keyword' }, + }, +}; + +export const observabilityMetricDefinitions: SavedObjectsType = { + name: OBSERVABILITY_METRIC_DEFINITIONS, + hidden: false, + namespaceType: 'multiple', + mappings: metricDefinitionSavedObjectMapping, + management: { + importableAndExportable: true, + icon: 'apmApp', + getTitle: () => + i18n.translate('xpack.entityDefinitions.overrides.title', { + defaultMessage: 'Metric definition', + }), + }, +}; + +export async function createMetricDefinitions(core: CoreSetup) { + const [coreStart] = await core.getStartServices(); + + const savedObjectsClient = new SavedObjectsClient( + coreStart.savedObjects.createInternalRepository() + ); + + await Promise.all([ + savedObjectsClient.create( + OBSERVABILITY_METRIC_DEFINITIONS, + { + groups: [ + { + id: 'metric.k8s.pod.uptime', + type: 'metric', + metric_name: 'k8s.pod.uptime', + stability: 'development', + brief: 'The time the Pod has been running', + entity_associations: ['k8s.pod'], + note: 'Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available.\nThe actual accuracy would depend on the instrumentation and operating system.', + instrument: 'gauge', + unit: 's', + }, + { + id: 'metric.k8s.pod.cpu.time', + type: 'metric', + metric_name: 'k8s.pod.cpu.time', + stability: 'development', + brief: 'Total CPU time consumed', + entity_associations: ['k8s.pod'], + note: 'Total CPU time consumed by the specific Pod on all available CPU cores', + instrument: 'counter', + unit: 's', + }, + { + id: 'metric.k8s.pod.cpu.usage', + type: 'metric', + metric_name: 'k8s.pod.cpu.usage', + stability: 'development', + brief: + "Pod's CPU usage, measured in cpus. Range from 0 to the number of allocatable CPUs", + note: 'CPU usage of the specific Pod on all available CPU cores, averaged over the sample window', + instrument: 'gauge', + unit: '{cpu}', + }, + { + id: 'metric.k8s.pod.memory.usage', + type: 'metric', + metric_name: 'k8s.pod.memory.usage', + stability: 'development', + brief: 'Memory usage of the Pod', + entity_associations: ['k8s.pod'], + note: 'Total memory usage of the Pod', + instrument: 'gauge', + unit: 'By', + }, + { + id: 'metric.k8s.pod.network.io', + type: 'metric', + metric_name: 'k8s.pod.network.io', + stability: 'development', + brief: 'Network bytes for the Pod', + instrument: 'counter', + unit: 'By', + entity_associations: ['k8s.pod'], + attributes: [ + { + ref: 'network.interface.name', + }, + { + ref: 'network.io.direction', + }, + ], + }, + { + id: 'metric.k8s.pod.network.errors', + type: 'metric', + metric_name: 'k8s.pod.network.errors', + stability: 'development', + brief: 'Pod network errors', + instrument: 'counter', + entity_associations: ['k8s.pod'], + unit: '{error}', + attributes: [ + { + ref: 'network.interface.name', + }, + { + ref: 'network.io.direction', + }, + ], + }, + { + id: 'metric.k8s.container.status.state', + type: 'metric', + metric_name: 'k8s.container.status.state', + stability: 'experimental', + brief: 'Describes the number of K8s containers that are currently in a given state', + note: 'All possible container states will be reported at each time interval to avoid missing metrics.\nOnly the value corresponding to the current state will be non-zero.', + instrument: 'updowncounter', + unit: '{container}', + entity_associations: ['k8s.container'], + attributes: [ + { + ref: 'k8s.container.status.state', + requirement_level: 'required', + }, + ], + }, + { + id: 'metric.k8s.container.status.reason', + type: 'metric', + metric_name: 'k8s.container.status.reason', + stability: 'experimental', + brief: + 'Describes the number of K8s containers that are currently in a state for a given reason', + instrument: 'updowncounter', + unit: '{container}', + entity_associations: ['k8s.container'], + note: 'All possible container state reasons will be reported at each time interval to avoid missing metrics.\nOnly the value corresponding to the current state reason will be non-zero.', + attributes: [ + { + ref: 'k8s.container.status.reason', + requirement_level: 'required', + }, + ], + }, + { + id: 'metric.k8s.node.uptime', + type: 'metric', + metric_name: 'k8s.node.uptime', + stability: 'development', + brief: 'The time the Node has been running', + note: 'Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available.\nThe actual accuracy would depend on the instrumentation and operating system.', + instrument: 'gauge', + unit: 's', + entity_associations: ['k8s.node'], + }, + { + id: 'metric.k8s.node.allocatable.cpu', + type: 'metric', + metric_name: 'k8s.node.allocatable.cpu', + stability: 'development', + brief: 'Amount of cpu allocatable on the node', + entity_associations: ['k8s.node'], + instrument: 'updowncounter', + unit: '{cpu}', + }, + { + id: 'metric.k8s.node.allocatable.ephemeral_storage', + type: 'metric', + metric_name: 'k8s.node.allocatable.ephemeral_storage', + stability: 'development', + brief: 'Amount of ephemeral-storage allocatable on the node', + entity_associations: ['k8s.node'], + instrument: 'updowncounter', + unit: 'By', + }, + { + id: 'metric.k8s.node.allocatable.memory', + type: 'metric', + metric_name: 'k8s.node.allocatable.memory', + stability: 'development', + brief: 'Amount of memory allocatable on the node', + entity_associations: ['k8s.node'], + instrument: 'updowncounter', + unit: 'By', + }, + { + id: 'metric.k8s.node.allocatable.pods', + type: 'metric', + metric_name: 'k8s.node.allocatable.pods', + stability: 'development', + brief: 'Amount of pods allocatable on the node', + entity_associations: ['k8s.node'], + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.node.cpu.time', + type: 'metric', + metric_name: 'k8s.node.cpu.time', + stability: 'development', + brief: 'Total CPU time consumed', + note: 'Total CPU time consumed by the specific Node on all available CPU cores', + instrument: 'counter', + entity_associations: ['k8s.node'], + unit: 's', + }, + { + id: 'metric.k8s.node.cpu.usage', + type: 'metric', + metric_name: 'k8s.node.cpu.usage', + stability: 'development', + brief: + "Node's CPU usage, measured in cpus. Range from 0 to the number of allocatable CPUs", + note: 'CPU usage of the specific Node on all available CPU cores, averaged over the sample window', + instrument: 'gauge', + entity_associations: ['k8s.node'], + unit: '{cpu}', + }, + { + id: 'metric.k8s.node.memory.usage', + type: 'metric', + metric_name: 'k8s.node.memory.usage', + stability: 'development', + brief: 'Memory usage of the Node', + note: 'Total memory usage of the Node', + instrument: 'gauge', + entity_associations: ['k8s.node'], + unit: 'By', + }, + { + id: 'metric.k8s.node.network.io', + type: 'metric', + metric_name: 'k8s.node.network.io', + stability: 'development', + brief: 'Network bytes for the Node', + instrument: 'counter', + unit: 'By', + entity_associations: ['k8s.node'], + attributes: [ + { + ref: 'network.interface.name', + }, + { + ref: 'network.io.direction', + }, + ], + }, + { + id: 'metric.k8s.node.network.errors', + type: 'metric', + metric_name: 'k8s.node.network.errors', + stability: 'development', + brief: 'Node network errors', + instrument: 'counter', + unit: '{error}', + entity_associations: ['k8s.node'], + attributes: [ + { + ref: 'network.interface.name', + }, + { + ref: 'network.io.direction', + }, + ], + }, + { + id: 'metric.k8s.deployment.desired_pods', + type: 'metric', + metric_name: 'k8s.deployment.desired_pods', + stability: 'development', + brief: 'Number of desired replica pods in this deployment', + entity_associations: ['k8s.deployment'], + note: 'This metric aligns with the `replicas` field of the\n[K8s DeploymentSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#deploymentspec-v1-apps).', + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.deployment.available_pods', + type: 'metric', + metric_name: 'k8s.deployment.available_pods', + stability: 'development', + entity_associations: ['k8s.deployment'], + brief: + 'Total number of available replica pods (ready for at least minReadySeconds) targeted by this deployment', + note: 'This metric aligns with the `availableReplicas` field of the\n[K8s DeploymentStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#deploymentstatus-v1-apps).', + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.replicaset.desired_pods', + type: 'metric', + metric_name: 'k8s.replicaset.desired_pods', + stability: 'development', + brief: 'Number of desired replica pods in this replicaset', + entity_associations: ['k8s.replicaset'], + note: 'This metric aligns with the `replicas` field of the\n[K8s ReplicaSetSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#replicasetspec-v1-apps).', + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.replicaset.available_pods', + type: 'metric', + metric_name: 'k8s.replicaset.available_pods', + stability: 'development', + entity_associations: ['k8s.replicaset'], + brief: + 'Total number of available replica pods (ready for at least minReadySeconds) targeted by this replicaset', + note: 'This metric aligns with the `availableReplicas` field of the\n[K8s ReplicaSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#replicasetstatus-v1-apps).', + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.replicationcontroller.desired_pods', + type: 'metric', + metric_name: 'k8s.replicationcontroller.desired_pods', + stability: 'development', + brief: 'Number of desired replica pods in this replication controller', + entity_associations: ['k8s.replicationcontroller'], + note: 'This metric aligns with the `replicas` field of the\n[K8s ReplicationControllerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#replicationcontrollerspec-v1-core)', + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.replicationcontroller.available_pods', + type: 'metric', + metric_name: 'k8s.replicationcontroller.available_pods', + stability: 'development', + entity_associations: ['k8s.replicationcontroller'], + brief: + 'Total number of available replica pods (ready for at least minReadySeconds) targeted by this replication controller', + note: 'This metric aligns with the `availableReplicas` field of the\n[K8s ReplicationControllerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#replicationcontrollerstatus-v1-core)', + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.statefulset.desired_pods', + type: 'metric', + metric_name: 'k8s.statefulset.desired_pods', + stability: 'development', + brief: 'Number of desired replica pods in this statefulset', + entity_associations: ['k8s.statefulset'], + note: 'This metric aligns with the `replicas` field of the\n[K8s StatefulSetSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#statefulsetspec-v1-apps).', + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.statefulset.ready_pods', + type: 'metric', + metric_name: 'k8s.statefulset.ready_pods', + stability: 'development', + entity_associations: ['k8s.statefulset'], + brief: 'The number of replica pods created for this statefulset with a Ready Condition', + note: 'This metric aligns with the `readyReplicas` field of the\n[K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#statefulsetstatus-v1-apps).', + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.statefulset.current_pods', + type: 'metric', + metric_name: 'k8s.statefulset.current_pods', + stability: 'development', + entity_associations: ['k8s.statefulset'], + brief: + 'The number of replica pods created by the statefulset controller from the statefulset version indicated by currentRevision', + note: 'This metric aligns with the `currentReplicas` field of the\n[K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#statefulsetstatus-v1-apps).', + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.statefulset.updated_pods', + type: 'metric', + metric_name: 'k8s.statefulset.updated_pods', + stability: 'development', + entity_associations: ['k8s.statefulset'], + brief: + 'Number of replica pods created by the statefulset controller from the statefulset version indicated by updateRevision', + note: 'This metric aligns with the `updatedReplicas` field of the\n[K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#statefulsetstatus-v1-apps).', + instrument: 'updowncounter', + unit: '{pod}', + }, + { + id: 'metric.k8s.hpa.desired_pods', + type: 'metric', + metric_name: 'k8s.hpa.desired_pods', + stability: 'development', + brief: + 'Desired number of replica pods managed by this horizontal pod autoscaler, as last calculated by the autoscaler', + note: 'This metric aligns with the `desiredReplicas` field of the\n[K8s HorizontalPodAutoscalerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#horizontalpodautoscalerstatus-v2-autoscaling)', + instrument: 'updowncounter', + entity_associations: ['k8s.hpa'], + unit: '{pod}', + }, + { + id: 'metric.k8s.hpa.current_pods', + type: 'metric', + metric_name: 'k8s.hpa.current_pods', + stability: 'development', + brief: + 'Current number of replica pods managed by this horizontal pod autoscaler, as last seen by the autoscaler', + note: 'This metric aligns with the `currentReplicas` field of the\n[K8s HorizontalPodAutoscalerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#horizontalpodautoscalerstatus-v2-autoscaling)', + instrument: 'updowncounter', + entity_associations: ['k8s.hpa'], + unit: '{pod}', + }, + { + id: 'metric.k8s.hpa.max_pods', + type: 'metric', + metric_name: 'k8s.hpa.max_pods', + stability: 'development', + brief: + 'The upper limit for the number of replica pods to which the autoscaler can scale up', + note: 'This metric aligns with the `maxReplicas` field of the\n[K8s HorizontalPodAutoscalerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#horizontalpodautoscalerspec-v2-autoscaling)', + instrument: 'updowncounter', + entity_associations: ['k8s.hpa'], + unit: '{pod}', + }, + { + id: 'metric.k8s.hpa.min_pods', + type: 'metric', + metric_name: 'k8s.hpa.min_pods', + stability: 'development', + brief: + 'The lower limit for the number of replica pods to which the autoscaler can scale down', + note: 'This metric aligns with the `minReplicas` field of the\n[K8s HorizontalPodAutoscalerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#horizontalpodautoscalerspec-v2-autoscaling)', + instrument: 'updowncounter', + entity_associations: ['k8s.hpa'], + unit: '{pod}', + }, + { + id: 'metric.k8s.daemonset.current_scheduled_nodes', + type: 'metric', + metric_name: 'k8s.daemonset.current_scheduled_nodes', + stability: 'development', + brief: + 'Number of nodes that are running at least 1 daemon pod and are supposed to run the daemon pod', + note: 'This metric aligns with the `currentNumberScheduled` field of the\n[K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#daemonsetstatus-v1-apps).', + instrument: 'updowncounter', + entity_associations: ['k8s.daemonset'], + unit: '{node}', + }, + { + id: 'metric.k8s.daemonset.desired_scheduled_nodes', + type: 'metric', + metric_name: 'k8s.daemonset.desired_scheduled_nodes', + stability: 'development', + brief: + 'Number of nodes that should be running the daemon pod (including nodes currently running the daemon pod)', + note: 'This metric aligns with the `desiredNumberScheduled` field of the\n[K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#daemonsetstatus-v1-apps).', + instrument: 'updowncounter', + entity_associations: ['k8s.daemonset'], + unit: '{node}', + }, + { + id: 'metric.k8s.daemonset.misscheduled_nodes', + type: 'metric', + metric_name: 'k8s.daemonset.misscheduled_nodes', + stability: 'development', + brief: + 'Number of nodes that are running the daemon pod, but are not supposed to run the daemon pod', + note: 'This metric aligns with the `numberMisscheduled` field of the\n[K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#daemonsetstatus-v1-apps).', + instrument: 'updowncounter', + entity_associations: ['k8s.daemonset'], + unit: '{node}', + }, + { + id: 'metric.k8s.daemonset.ready_nodes', + type: 'metric', + metric_name: 'k8s.daemonset.ready_nodes', + stability: 'development', + brief: + 'Number of nodes that should be running the daemon pod and have one or more of the daemon pod running and ready', + note: 'This metric aligns with the `numberReady` field of the\n[K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#daemonsetstatus-v1-apps).', + instrument: 'updowncounter', + entity_associations: ['k8s.daemonset'], + unit: '{node}', + }, + { + id: 'metric.k8s.job.active_pods', + type: 'metric', + metric_name: 'k8s.job.active_pods', + stability: 'development', + brief: 'The number of pending and actively running pods for a job', + instrument: 'updowncounter', + unit: '{pod}', + entity_associations: ['k8s.job'], + note: 'This metric aligns with the `active` field of the\n[K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#jobstatus-v1-batch).', + }, + { + id: 'metric.k8s.job.failed_pods', + type: 'metric', + metric_name: 'k8s.job.failed_pods', + stability: 'development', + brief: 'The number of pods which reached phase Failed for a job', + instrument: 'updowncounter', + unit: '{pod}', + entity_associations: ['k8s.job'], + note: 'This metric aligns with the `failed` field of the\n[K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#jobstatus-v1-batch).', + }, + { + id: 'metric.k8s.job.successful_pods', + type: 'metric', + metric_name: 'k8s.job.successful_pods', + stability: 'development', + brief: 'The number of pods which reached phase Succeeded for a job', + instrument: 'updowncounter', + unit: '{pod}', + entity_associations: ['k8s.job'], + note: 'This metric aligns with the `succeeded` field of the\n[K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#jobstatus-v1-batch).', + }, + { + id: 'metric.k8s.job.desired_successful_pods', + type: 'metric', + metric_name: 'k8s.job.desired_successful_pods', + stability: 'development', + brief: 'The desired number of successfully finished pods the job should be run with', + instrument: 'updowncounter', + unit: '{pod}', + entity_associations: ['k8s.job'], + note: 'This metric aligns with the `completions` field of the\n[K8s JobSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#jobspec-v1-batch)..', + }, + { + id: 'metric.k8s.job.max_parallel_pods', + type: 'metric', + metric_name: 'k8s.job.max_parallel_pods', + stability: 'development', + brief: 'The max desired number of pods the job should run at any given time', + instrument: 'updowncounter', + unit: '{pod}', + entity_associations: ['k8s.job'], + note: 'This metric aligns with the `parallelism` field of the\n[K8s JobSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#jobspec-v1-batch).', + }, + { + id: 'metric.k8s.cronjob.active_jobs', + type: 'metric', + metric_name: 'k8s.cronjob.active_jobs', + stability: 'development', + brief: 'The number of actively running jobs for a cronjob', + instrument: 'updowncounter', + unit: '{job}', + entity_associations: ['k8s.cronjob'], + note: 'This metric aligns with the `active` field of the\n[K8s CronJobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#cronjobstatus-v1-batch).', + }, + { + id: 'metric.k8s.namespace.phase', + type: 'metric', + metric_name: 'k8s.namespace.phase', + stability: 'development', + brief: 'Describes number of K8s namespaces that are currently in a given phase.', + instrument: 'updowncounter', + unit: '{namespace}', + entity_associations: ['k8s.namespace'], + attributes: [ + { + ref: 'k8s.namespace.phase', + requirement_level: 'required', + }, + ], + }, + { + id: 'metric.k8s.container.cpu.limit', + type: 'metric', + metric_name: 'k8s.container.cpu.limit', + stability: 'development', + brief: 'Maximum CPU resource limit set for the container', + entity_associations: ['k8s.container'], + note: 'See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core for details.', + instrument: 'updowncounter', + unit: '{cpu}', + }, + { + id: 'metric.k8s.container.cpu.request', + type: 'metric', + metric_name: 'k8s.container.cpu.request', + stability: 'development', + brief: 'CPU resource requested for the container', + entity_associations: ['k8s.container'], + note: 'See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core for details.', + instrument: 'updowncounter', + unit: '{cpu}', + }, + { + id: 'metric.k8s.container.memory.limit', + type: 'metric', + metric_name: 'k8s.container.memory.limit', + stability: 'development', + brief: 'Maximum memory resource limit set for the container', + entity_associations: ['k8s.container'], + note: 'See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core for details.', + instrument: 'updowncounter', + unit: 'By', + }, + { + id: 'metric.k8s.container.memory.request', + type: 'metric', + metric_name: 'k8s.container.memory.request', + stability: 'development', + brief: 'Memory resource requested for the container', + entity_associations: ['k8s.container'], + note: 'See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core for details.', + instrument: 'updowncounter', + unit: 'By', + }, + { + id: 'metric.k8s.container.storage.limit', + type: 'metric', + metric_name: 'k8s.container.storage.limit', + stability: 'development', + brief: 'Maximum storage resource limit set for the container', + entity_associations: ['k8s.container'], + note: 'See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core for details.', + instrument: 'updowncounter', + unit: 'By', + }, + { + id: 'metric.k8s.container.storage.request', + type: 'metric', + metric_name: 'k8s.container.storage.request', + stability: 'development', + brief: 'Storage resource requested for the container', + entity_associations: ['k8s.container'], + note: 'See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core for details.', + instrument: 'updowncounter', + unit: 'By', + }, + { + id: 'metric.k8s.container.ephemeral_storage.limit', + type: 'metric', + metric_name: 'k8s.container.ephemeral_storage.limit', + stability: 'development', + brief: 'Maximum ephemeral storage resource limit set for the container', + entity_associations: ['k8s.container'], + note: 'See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core for details.', + instrument: 'updowncounter', + unit: 'By', + }, + { + id: 'metric.k8s.container.ephemeral_storage.request', + type: 'metric', + metric_name: 'k8s.container.ephemeral_storage.request', + stability: 'development', + brief: 'Ephemeral storage resource requested for the container', + entity_associations: ['k8s.container'], + note: 'See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core for details.', + instrument: 'updowncounter', + unit: 'By', + }, + { + id: 'metric.k8s.container.restart.count', + type: 'metric', + metric_name: 'k8s.container.restart.count', + stability: 'development', + brief: + 'Describes how many times the container has restarted (since the last counter reset)', + instrument: 'updowncounter', + unit: '{restart}', + entity_associations: ['k8s.container'], + note: 'This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0\nat any time depending on how your kubelet is configured to prune dead containers.\nIt is best to not depend too much on the exact value but rather look at it as\neither == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case\nyou can conclude there were restarts in the recent past, and not try and analyze the value beyond that.', + }, + { + id: 'metric.k8s.container.ready', + type: 'metric', + metric_name: 'k8s.container.ready', + stability: 'development', + brief: + 'Indicates whether the container is currently marked as ready to accept traffic,\nbased on its readiness probe (1 = ready, 0 = not ready)', + instrument: 'updowncounter', + unit: '{container}', + entity_associations: ['k8s.container'], + note: 'This metric SHOULD reflect the value of the `ready` field in the\n[K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#containerstatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.cpu.limit.hard', + type: 'metric', + metric_name: 'k8s.resourcequota.cpu.limit.hard', + stability: 'development', + brief: + 'The CPU limits in a specific namespace.\nThe value represents the configured quota limit of the resource in the namespace.', + instrument: 'updowncounter', + unit: '{cpu}', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `hard` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.cpu.limit.used', + type: 'metric', + metric_name: 'k8s.resourcequota.cpu.limit.used', + stability: 'development', + brief: + 'The CPU limits in a specific namespace.\nThe value represents the current observed total usage of the resource in the namespace.', + instrument: 'updowncounter', + unit: '{cpu}', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `used` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.cpu.request.hard', + type: 'metric', + metric_name: 'k8s.resourcequota.cpu.request.hard', + stability: 'development', + brief: + 'The CPU requests in a specific namespace.\nThe value represents the configured quota limit of the resource in the namespace.', + instrument: 'updowncounter', + unit: '{cpu}', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `hard` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.cpu.request.used', + type: 'metric', + metric_name: 'k8s.resourcequota.cpu.request.used', + stability: 'development', + brief: + 'The CPU requests in a specific namespace.\nThe value represents the current observed total usage of the resource in the namespace.', + instrument: 'updowncounter', + unit: '{cpu}', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `used` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.memory.limit.hard', + type: 'metric', + metric_name: 'k8s.resourcequota.memory.limit.hard', + stability: 'development', + brief: + 'The memory limits in a specific namespace.\nThe value represents the configured quota limit of the resource in the namespace.', + instrument: 'updowncounter', + unit: 'By', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `hard` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.memory.limit.used', + type: 'metric', + metric_name: 'k8s.resourcequota.memory.limit.used', + stability: 'development', + brief: + 'The memory limits in a specific namespace.\nThe value represents the current observed total usage of the resource in the namespace.', + instrument: 'updowncounter', + unit: 'By', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `used` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.memory.request.hard', + type: 'metric', + metric_name: 'k8s.resourcequota.memory.request.hard', + stability: 'development', + brief: + 'The memory requests in a specific namespace.\nThe value represents the configured quota limit of the resource in the namespace.', + instrument: 'updowncounter', + unit: 'By', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `hard` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.memory.request.used', + type: 'metric', + metric_name: 'k8s.resourcequota.memory.request.used', + stability: 'development', + brief: + 'The memory requests in a specific namespace.\nThe value represents the current observed total usage of the resource in the namespace.', + instrument: 'updowncounter', + unit: 'By', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `used` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.hugepage_count.request.hard', + type: 'metric', + metric_name: 'k8s.resourcequota.hugepage_count.request.hard', + stability: 'development', + brief: + 'The huge page requests in a specific namespace.\nThe value represents the configured quota limit of the resource in the namespace.', + instrument: 'updowncounter', + unit: '{hugepage}', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `hard` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + attributes: [ + { + ref: 'k8s.hugepage.size', + requirement_level: 'required', + }, + ], + }, + { + id: 'metric.k8s.resourcequota.hugepage_count.request.used', + type: 'metric', + metric_name: 'k8s.resourcequota.hugepage_count.request.used', + stability: 'development', + brief: + 'The huge page requests in a specific namespace.\nThe value represents the current observed total usage of the resource in the namespace.', + instrument: 'updowncounter', + unit: '{hugepage}', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `used` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + attributes: [ + { + ref: 'k8s.hugepage.size', + requirement_level: 'required', + }, + ], + }, + { + id: 'metric.k8s.resourcequota.storage.request.hard', + type: 'metric', + metric_name: 'k8s.resourcequota.storage.request.hard', + stability: 'development', + brief: + 'The storage requests in a specific namespace.\nThe value represents the configured quota limit of the resource in the namespace.', + instrument: 'updowncounter', + unit: 'By', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `hard` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).\n\nThe `k8s.storageclass.name` should be required when a resource quota is defined for a specific\nstorage class.', + attributes: [ + { + ref: 'k8s.storageclass.name', + requirement_level: { + conditionally_required: + 'The `k8s.storageclass.name` should be required when a resource quota is defined for a specific\nstorage class.', + }, + }, + ], + }, + { + id: 'metric.k8s.resourcequota.storage.request.used', + type: 'metric', + metric_name: 'k8s.resourcequota.storage.request.used', + stability: 'development', + brief: + 'The storage requests in a specific namespace.\nThe value represents the current observed total usage of the resource in the namespace.', + instrument: 'updowncounter', + unit: 'By', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `used` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).\n\nThe `k8s.storageclass.name` should be required when a resource quota is defined for a specific\nstorage class.', + attributes: [ + { + ref: 'k8s.storageclass.name', + requirement_level: { + conditionally_required: + 'The `k8s.storageclass.name` should be required when a resource quota is defined for a specific\nstorage class.', + }, + }, + ], + }, + { + id: 'metric.k8s.resourcequota.persistentvolumeclaim_count.hard', + type: 'metric', + metric_name: 'k8s.resourcequota.persistentvolumeclaim_count.hard', + stability: 'development', + brief: + 'The total number of PersistentVolumeClaims that can exist in the namespace.\nThe value represents the configured quota limit of the resource in the namespace.', + instrument: 'updowncounter', + unit: '{persistentvolumeclaim}', + note: 'This metric is retrieved from the `hard` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).\n\nThe `k8s.storageclass.name` should be required when a resource quota is defined for a specific\nstorage class.', + attributes: [ + { + ref: 'k8s.storageclass.name', + requirement_level: { + conditionally_required: + 'The `k8s.storageclass.name` should be required when a resource quota is defined for a specific\nstorage class.', + }, + }, + ], + }, + { + id: 'metric.k8s.resourcequota.persistentvolumeclaim_count.used', + type: 'metric', + metric_name: 'k8s.resourcequota.persistentvolumeclaim_count.used', + stability: 'development', + brief: + 'The total number of PersistentVolumeClaims that can exist in the namespace.\nThe value represents the current observed total usage of the resource in the namespace.', + instrument: 'updowncounter', + unit: '{persistentvolumeclaim}', + note: 'This metric is retrieved from the `used` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).\n\nThe `k8s.storageclass.name` should be required when a resource quota is defined for a specific\nstorage class.', + attributes: [ + { + ref: 'k8s.storageclass.name', + requirement_level: { + conditionally_required: + 'The `k8s.storageclass.name` should be required when a resource quota is defined for a specific\nstorage class.', + }, + }, + ], + }, + { + id: 'metric.k8s.resourcequota.ephemeral_storage.request.hard', + type: 'metric', + metric_name: 'k8s.resourcequota.ephemeral_storage.request.hard', + stability: 'development', + brief: + 'The sum of local ephemeral storage requests in the namespace.\nThe value represents the configured quota limit of the resource in the namespace.', + instrument: 'updowncounter', + unit: 'By', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `hard` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.ephemeral_storage.request.used', + type: 'metric', + metric_name: 'k8s.resourcequota.ephemeral_storage.request.used', + stability: 'development', + brief: + 'The sum of local ephemeral storage requests in the namespace.\nThe value represents the current observed total usage of the resource in the namespace.', + instrument: 'updowncounter', + unit: 'By', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `used` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.ephemeral_storage.limit.hard', + type: 'metric', + metric_name: 'k8s.resourcequota.ephemeral_storage.limit.hard', + stability: 'development', + brief: + 'The sum of local ephemeral storage limits in the namespace.\nThe value represents the configured quota limit of the resource in the namespace.', + instrument: 'updowncounter', + unit: 'By', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `hard` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.ephemeral_storage.limit.used', + type: 'metric', + metric_name: 'k8s.resourcequota.ephemeral_storage.limit.used', + stability: 'development', + brief: + 'The sum of local ephemeral storage limits in the namespace.\nThe value represents the current observed total usage of the resource in the namespace.', + instrument: 'updowncounter', + unit: 'By', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `used` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + }, + { + id: 'metric.k8s.resourcequota.object_count.hard', + type: 'metric', + metric_name: 'k8s.resourcequota.object_count.hard', + stability: 'development', + brief: + 'The object count limits in a specific namespace.\nThe value represents the configured quota limit of the resource in the namespace.', + instrument: 'updowncounter', + unit: '{object}', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `hard` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + attributes: [ + { + ref: 'k8s.resourcequota.resource_name', + requirement_level: 'required', + }, + ], + }, + { + id: 'metric.k8s.resourcequota.object_count.used', + type: 'metric', + metric_name: 'k8s.resourcequota.object_count.used', + stability: 'development', + brief: + 'The object count limits in a specific namespace.\nThe value represents the current observed total usage of the resource in the namespace.', + instrument: 'updowncounter', + unit: '{object}', + entity_associations: ['k8s.resourcequota'], + note: 'This metric is retrieved from the `used` field of the\n[K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcequotastatus-v1-core).', + attributes: [ + { + ref: 'k8s.resourcequota.resource_name', + requirement_level: 'required', + }, + ], + }, + ], + }, + { + id: 'kubernetes', + overwrite: true, + } + ), + ]); +} diff --git a/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/navigation_overrides.ts b/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/navigation_overrides.ts index 6b7632ecd66db..3e34b40d1784d 100644 --- a/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/navigation_overrides.ts +++ b/x-pack/platform/plugins/shared/observability_navigation/server/saved_objects/navigation_overrides.ts @@ -31,6 +31,9 @@ const observabilityNavigationOverridesItemMapping: SavedObjectsType['mappings'][ dashboardId: { type: 'keyword', }, + order: { + type: 'keyword', + }, }; const observabilityNavigationOverridesMapping: SavedObjectsType['mappings'] = { @@ -83,10 +86,16 @@ export async function createNavigationOverrides(core: CoreSetup) { title: 'Kubernetes', subItems: [ { - id: 'pod', - title: 'Pods', - entityType: 'k8s.pod', - dashboardId: 'kubernetes_otel-cluster-overview', + id: 'volume', + title: 'Volumes', + order: 500, + dashboardId: 'kubernetes-3912d9a0-bcb2-11ec-b64f-7dd6e8e82013', + }, + { + id: 'dashboard', + title: 'Services', + order: 800, + dashboardId: 'kubernetes-ff1b3850-bcb1-11ec-b64f-7dd6e8e82013', }, ], }, @@ -104,7 +113,6 @@ export async function createNavigationOverrides(core: CoreSetup) { { id: 'docker', title: 'Docker', - entityType: 'docker', dashboardId: 'kubernetes_otel-cluster-overview', }, ], diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/components/entity_table/index.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/components/entity_table/index.tsx deleted file mode 100644 index 5b4840a6240b3..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/components/entity_table/index.tsx +++ /dev/null @@ -1,74 +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 React, { useCallback, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { lastValueFrom } from 'rxjs'; -import type { IKibanaSearchResponse } from '@kbn/search-types'; -import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; - -export const EntityTable = ({ - dashboardId, - entityType, -}: { - dashboardId: string; - entityType: string; -}) => { - const { - services: { data }, - } = useKibanaContextForPlugin(); - - const requestData = useCallback(async () => { - const entities = await lastValueFrom( - data.search.search<{}, IKibanaSearchResponse>({ - params: { - index: 'metrics-*', - body: { - query: { - bool: { - filter: [ - { - terms: { - 'data_stream.dataset': ['kubernetes.pod', entityType], - }, - }, - ], - }, - }, - aggs: { - entities: { - terms: { - field: 'kubernetes.pod.name', - size: 1000, - }, - }, - }, - }, - }, - }) - ); - }, [data, entityType]); - - useEffect(() => { - requestData(); - }, [requestData]); - - // Placeholder for any hooks or context you might need - return ( -
-

- {i18n.translate('xpack.infra.entityTable.h2.entityTableLabel', { - defaultMessage: 'Entity Table', - })} -

-

- {i18n.translate('xpack.infra.entityTable.p.thisIsAPlaceholderLabel', { - defaultMessage: 'This is a placeholder for the Entity Table component.', - })} -

-
- ); -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/components/page_content/page_content.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/components/page_content/page_content.tsx index 68bcb12bab3e6..c5285faee5d3b 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/components/page_content/page_content.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/components/page_content/page_content.tsx @@ -10,7 +10,13 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { useTimeRangeMetadataContext } from '../../../../../hooks/use_timerange_metadata'; import { RenderDashboard } from '../dashboard/render_dashboard'; -export const PageContent = ({ dashboardId }: { dashboardId: string }) => { +export const PageContent = ({ + dashboardId, + enitiyId, +}: { + dashboardId: string; + enitiyId?: string | null; +}) => { const { data, status } = useTimeRangeMetadataContext(); if (status === 'loading') { diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/index.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/index.tsx index c96516d7afd89..5cfc9458fb809 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/index.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/dashboard/index.tsx @@ -28,13 +28,13 @@ export const Dashboard = () => { } = useKibanaContextForPlugin(); const { search } = useLocation(); - const { entity, entitySubtype } = useParams<{ entity: string; entitySubtype?: string }>(); + const { namespace, name } = useParams<{ namespace: string; name?: string }>(); - const { dashboardId } = useMemo(() => { + const { dashboardId, entityId } = useMemo(() => { const query = new URLSearchParams(search); return { dashboardId: query.get('dashboardId') ?? '', - entityType: query.get('entityType'), + entityId: query.get('entityId'), }; }, [search]); @@ -42,12 +42,12 @@ export const Dashboard = () => { const kubernetesLinkProps = useLinkProps({ app: 'metrics', - pathname: entity, + pathname: namespace, }); const pageTitle = useMemo( - () => (entitySubtype ?? entity).replace(/-/g, ' ').replace(/^./, (c) => c.toUpperCase()), - [entity, entitySubtype] + () => (name ?? namespace).replace(/-/g, ' ').replace(/^./, (c) => c.toUpperCase()), + [namespace, name] ); useMetricsBreadcrumbs([ @@ -77,7 +77,7 @@ export const Dashboard = () => { data-test-subj="infraKubernetesPage" > - + diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/index.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/index.tsx index 882761d3933e6..c130779dec4f2 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/index.tsx @@ -130,8 +130,8 @@ export const InfrastructurePage = () => { - - + + diff --git a/x-pack/solutions/observability/plugins/infra/public/plugin.ts b/x-pack/solutions/observability/plugins/infra/public/plugin.ts index 4d5960f4ac8a5..9d0d97f2b376c 100644 --- a/x-pack/solutions/observability/plugins/infra/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/infra/public/plugin.ts @@ -181,13 +181,13 @@ export class Plugin implements InfraClientPluginClass { path: '/hosts', }, ...(navigation?.map((nav) => ({ - label: getDynamicNavigationTitle(nav.title), + label: formatDynamicNavigationPath(nav.title), app: 'metrics', path: getDynamicNavigationPath(nav), deepLinks: nav.subItems?.map((subNav) => { return { id: subNav.id, - title: getDynamicNavigationTitle(subNav.title), + title: formatDynamicNavigationPath(subNav.title), path: getDynamicNavigationPath(subNav, nav.title), }; }), @@ -269,12 +269,12 @@ export class Plugin implements InfraClientPluginClass { ...(navigation ?? []).map((nav) => { return { id: `dynamic_${nav.id}`, - title: getDynamicNavigationTitle(nav.title), + title: formatDynamicNavigationPath(nav.title), path: getDynamicNavigationPath(nav), deepLinks: (nav.subItems ?? []).map((subNav) => { return { id: `dynamic_${subNav.id}`, - title: getDynamicNavigationTitle(subNav.title), + title: formatDynamicNavigationPath(subNav.title), path: getDynamicNavigationPath(subNav, nav.id), }; }), @@ -404,11 +404,8 @@ const getLogsExplorerAccessible$ = (application: CoreStart['application']) => { ); }; -const getDynamicNavigationTitle = (title: string) => { - return title - .replace(/-/g, ' ') - .toLowerCase() - .replace(/^\w/, (c) => c.toUpperCase()); +const formatDynamicNavigationPath = (title: string) => { + return title.replace(/\s+/g, '-').toLowerCase(); }; const getDynamicNavigationPath = ( nav: DynamicNavigationItem | ObservabilityDynamicNavigation, @@ -416,15 +413,15 @@ const getDynamicNavigationPath = ( ) => { const url = new URL( `/entity/${ - parentTitle ? `${getDynamicNavigationTitle(parentTitle)}/` : '' - }${getDynamicNavigationTitle(nav.title)}`, + parentTitle ? `${formatDynamicNavigationPath(parentTitle)}/` : '' + }${formatDynamicNavigationPath(nav.title)}`, window.location.origin ); if (nav.dashboardId) { url.searchParams.set('dashboardId', nav.dashboardId); } - if (nav.entityType) { - url.searchParams.set('entityType', nav.entityType); + if (nav.entityId) { + url.searchParams.set('entityId', nav.entityId); } return `${url.pathname}${url.search}`; };