[ECO][Inventory v2] Hosts entity summary endpoint changes (#203617)

## Summary

Closes #202300

This PR changes the entity client function to v2 (`searchEntities`) in
`getLatestEntity`. After the change to use `v2.searchEntities` the
parameters are also updated to include the time range (`start` and `end`
are required)

## Testing
~- We can create some definitions manually- in the Kibana DEV tools: ~ -
Not needed after we merged the V2 PR

- In a local environment enable the entities feature flag ( it should be
a clean env as the entities should not be enabled before ):
<img width="1911" alt="image"
src="https://github.com/user-attachments/assets/75d6f77d-5039-41ca-80ca-34c3bf99844e"
/>

- Some hosts and containers are required - oblt cluster/metricbeat or
   - Create hosts using synthtrace: 
       ```
node scripts/synthtrace infra_hosts_with_apm_hosts
--scenarioOpts.numInstances=20
       ```
   - Create containers using synthtrace: 
       ```
       node scripts/synthtrace infra_docker_containers.ts
       ```

- In the UI 
- Open asset details view for hosts and containers and check the summary
endpoint response:
  ⚠️ Updated: 


![image](https://github.com/user-attachments/assets/27683b74-f0b5-43a0-9a8f-98cd2a61e68e)

- If the entities FF is disabled (default: no `logs` should be part of
the `sourceDataStreams`):


![image](https://github.com/user-attachments/assets/7b8851b4-514c-4fc7-ab84-720b2ccb16ae)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Sergi Romeu <sergi.romeu@elastic.co>
This commit is contained in:
jennypavlova 2025-01-09 14:17:37 +01:00 committed by GitHub
parent a54045841c
commit 13582aa458
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 101 additions and 50 deletions

View file

@ -12,16 +12,21 @@ import {
} from '@kbn/observability-shared-plugin/common';
import { useFetcher } from '../../../hooks/use_fetcher';
const EntityTypeSchema = z.union([
const EntityFilterTypeSchema = z.union([
z.literal(BUILT_IN_ENTITY_TYPES.HOST),
z.literal(BUILT_IN_ENTITY_TYPES.CONTAINER),
]);
const EntityTypeSchema = z.union([
z.literal(BUILT_IN_ENTITY_TYPES.HOST_V2),
z.literal(BUILT_IN_ENTITY_TYPES.CONTAINER_V2),
]);
const EntityDataStreamSchema = z.union([
z.literal(EntityDataStreamType.METRICS),
z.literal(EntityDataStreamType.LOGS),
]);
const EntitySummarySchema = z.object({
entityFilterType: EntityFilterTypeSchema,
entityType: EntityTypeSchema,
entityId: z.string(),
sourceDataStreams: z.array(EntityDataStreamSchema),
@ -32,9 +37,13 @@ export type EntitySummary = z.infer<typeof EntitySummarySchema>;
export function useEntitySummary({
entityType,
entityId,
from,
to,
}: {
entityType: string;
entityId: string;
from: string;
to: string;
}) {
const { data, status } = useFetcher(
async (callApi) => {
@ -44,11 +53,15 @@ export function useEntitySummary({
const response = await callApi(`/api/infra/entities/${entityType}/${entityId}/summary`, {
method: 'GET',
query: {
to,
from,
},
});
return EntitySummarySchema.parse(response);
},
[entityType, entityId]
[entityType, entityId, to, from]
);
return { dataStreams: data?.sourceDataStreams ?? [], status };

View file

@ -28,6 +28,7 @@ import { useAssetDetailsRenderPropsContext } from './use_asset_details_render_pr
import { useTabSwitcherContext } from './use_tab_switcher';
import { useEntitySummary } from './use_entity_summary';
import { isMetricsSignal } from '../utils/get_data_stream_types';
import { useDatePickerContext } from './use_date_picker';
type TabItem = NonNullable<Pick<EuiPageHeaderProps, 'tabs'>['tabs']>[number];
@ -144,9 +145,12 @@ const useFeatureFlagTabs = () => {
const useMetricsTabs = () => {
const { asset } = useAssetDetailsRenderPropsContext();
const { dateRange } = useDatePickerContext();
const { dataStreams } = useEntitySummary({
entityType: asset.type,
entityId: asset.id,
from: new Date(dateRange.from).toISOString(),
to: new Date(dateRange.to).toISOString(),
});
const isMetrics = isMetricsSignal(dataStreams);

View file

@ -31,15 +31,19 @@ import type { AddMetricsCalloutKey } from '../../add_metrics_callout/constants';
import { AddMetricsCallout } from '../../add_metrics_callout';
import { useEntitySummary } from '../../hooks/use_entity_summary';
import { isMetricsSignal } from '../../utils/get_data_stream_types';
import { useDatePickerContext } from '../../hooks/use_date_picker';
export const MetricsTemplate = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
({ children }, ref) => {
const { euiTheme } = useEuiTheme();
const { dateRange } = useDatePickerContext();
const { asset, renderMode } = useAssetDetailsRenderPropsContext();
const { scrollTo, setScrollTo } = useTabSwitcherContext();
const { dataStreams, status: dataStreamsStatus } = useEntitySummary({
entityType: asset.type,
entityId: asset.id,
from: new Date(dateRange.from).toISOString(),
to: new Date(dateRange.to).toISOString(),
});
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);

View file

@ -42,6 +42,8 @@ export const Overview = () => {
const { dataStreams, status: dataStreamsStatus } = useEntitySummary({
entityType: asset.type,
entityId: asset.id,
from: new Date(dateRange.from).toISOString(),
to: new Date(dateRange.to).toISOString(),
});
const addMetricsCalloutId: AddMetricsCalloutKey =
asset.type === 'host' ? 'hostOverview' : 'containerOverview';

View file

@ -60,6 +60,8 @@ export const Processes = () => {
const { dataStreams, status: dataStreamsStatus } = useEntitySummary({
entityType: BUILT_IN_ENTITY_TYPES.HOST,
entityId: asset.name,
from: new Date(getDateRangeInTimestamp().from).toISOString(),
to: new Date(getDateRangeInTimestamp().to).toISOString(),
});
const addMetricsCalloutId: AddMetricsCalloutKey = 'hostProcesses';
const [dismissedAddMetricsCallout, setDismissedAddMetricsCallout] = useLocalStorage(

View file

@ -27,6 +27,7 @@ import { OnboardingFlow } from '../../shared/templates/no_data_config';
import { PageTitleWithPopover } from '../header/page_title_with_popover';
import { useEntitySummary } from '../hooks/use_entity_summary';
import { isLogsSignal, isMetricsSignal } from '../utils/get_data_stream_types';
import { useDatePickerContext } from '../hooks/use_date_picker';
const DATA_AVAILABILITY_PER_TYPE: Partial<Record<InventoryItemType, string[]>> = {
host: [SYSTEM_INTEGRATION],
@ -37,10 +38,13 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => {
const { metadata, loading: metadataLoading } = useMetadataStateContext();
const { rightSideItems, tabEntries, breadcrumbs: headerBreadcrumbs } = usePageHeader(tabs, links);
const { asset } = useAssetDetailsRenderPropsContext();
const { getDateRangeInTimestamp } = useDatePickerContext();
const trackOnlyOnce = React.useRef(false);
const { dataStreams, status: entitySummaryStatus } = useEntitySummary({
entityType: asset.type,
entityId: asset.id,
from: new Date(getDateRangeInTimestamp().from).toISOString(),
to: new Date(getDateRangeInTimestamp().to).toISOString(),
});
const { isEntityCentricExperienceEnabled } = useEntityCentricExperienceSetting();
const { activeTabId } = useTabSwitcherContext();

View file

@ -20,8 +20,6 @@ jest.mock('./get_latest_entity', () => ({
getLatestEntity: jest.fn(),
}));
type EntityType = 'host' | 'container';
describe('getDataStreamTypes', () => {
let infraMetricsClient: jest.Mocked<InfraMetricsClient>;
let obsEsClient: jest.Mocked<ObservabilityElasticsearchClient>;
@ -40,12 +38,15 @@ describe('getDataStreamTypes', () => {
const params = {
entityId: 'entity123',
entityType: 'host' as EntityType,
entityType: 'built_in_hosts_from_ecs_data',
entityFilterType: 'host',
entityCentricExperienceEnabled: false,
infraMetricsClient,
obsEsClient,
entityManagerClient,
logger,
from: '2024-12-09T10:49:15Z',
to: '2024-12-10T10:49:15Z',
};
const result = await getDataStreamTypes(params);
@ -63,12 +64,15 @@ describe('getDataStreamTypes', () => {
const params = {
entityId: 'entity123',
entityType: 'container' as EntityType,
entityFilterType: 'container',
entityType: 'built_in_containers_from_ecs_data',
entityCentricExperienceEnabled: false,
infraMetricsClient,
obsEsClient,
entityManagerClient,
logger,
from: '2024-12-09T10:49:15Z',
to: '2024-12-10T10:49:15Z',
};
const result = await getDataStreamTypes(params);
@ -83,12 +87,15 @@ describe('getDataStreamTypes', () => {
const params = {
entityId: 'entity123',
entityType: 'host' as EntityType,
entityType: 'built_in_hosts_from_ecs_data',
entityFilterType: 'host',
entityCentricExperienceEnabled: true,
infraMetricsClient,
obsEsClient,
entityManagerClient,
logger,
from: '2024-12-09T10:49:15Z',
to: '2024-12-10T10:49:15Z',
};
const result = await getDataStreamTypes(params);
@ -96,11 +103,12 @@ describe('getDataStreamTypes', () => {
expect(result).toEqual(['metrics', 'logs']);
expect(getHasMetricsData).toHaveBeenCalled();
expect(getLatestEntity).toHaveBeenCalledWith({
inventoryEsClient: obsEsClient,
entityId: 'entity123',
entityType: 'host',
entityType: 'built_in_hosts_from_ecs_data',
entityManagerClient,
logger,
from: '2024-12-09T10:49:15Z',
to: '2024-12-10T10:49:15Z',
});
});
@ -110,12 +118,15 @@ describe('getDataStreamTypes', () => {
const params = {
entityId: 'entity123',
entityType: 'host' as EntityType,
entityType: 'built_in_hosts_from_ecs_data',
entityFilterType: 'host',
entityCentricExperienceEnabled: true,
infraMetricsClient,
obsEsClient,
entityManagerClient,
logger,
from: '2024-12-09T10:49:15Z',
to: '2024-12-10T10:49:15Z',
};
const result = await getDataStreamTypes(params);
@ -130,12 +141,15 @@ describe('getDataStreamTypes', () => {
const params = {
entityId: 'entity123',
entityType: 'host' as EntityType,
entityType: 'built_in_hosts_from_ecs_data',
entityFilterType: 'host',
entityCentricExperienceEnabled: true,
infraMetricsClient,
obsEsClient,
entityManagerClient,
logger,
from: '2024-12-09T10:49:15Z',
to: '2024-12-10T10:49:15Z',
};
const result = await getDataStreamTypes(params);

View file

@ -6,6 +6,7 @@
*/
import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common';
import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common';
import { EntityDataStreamType } from '@kbn/observability-shared-plugin/common';
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client';
@ -17,12 +18,15 @@ import { getLatestEntity } from './get_latest_entity';
interface Params {
entityId: string;
entityType: 'host' | 'container';
entityType: string;
entityFilterType: string;
entityCentricExperienceEnabled: boolean;
infraMetricsClient: InfraMetricsClient;
obsEsClient: ObservabilityElasticsearchClient;
entityManagerClient: EntityClient;
logger: Logger;
from: string;
to: string;
}
export async function getDataStreamTypes({
@ -30,14 +34,16 @@ export async function getDataStreamTypes({
entityId,
entityManagerClient,
entityType,
entityFilterType,
infraMetricsClient,
obsEsClient,
from,
to,
logger,
}: Params) {
const hasMetricsData = await getHasMetricsData({
infraMetricsClient,
entityId,
field: findInventoryFields(entityType).id,
field: findInventoryFields(entityFilterType as InventoryItemType).id,
});
const sourceDataStreams = new Set(hasMetricsData ? [EntityDataStreamType.METRICS] : []);
@ -47,11 +53,12 @@ export async function getDataStreamTypes({
}
const latestEntity = await getLatestEntity({
inventoryEsClient: obsEsClient,
entityId,
entityType,
entityManagerClient,
logger,
from,
to,
});
if (latestEntity) {

View file

@ -5,64 +5,55 @@
* 2.0.
*/
import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema';
import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
import { ENTITY_TYPE, SOURCE_DATA_STREAM_TYPE } from '@kbn/observability-shared-plugin/common';
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client';
import type { EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
import type { Logger } from '@kbn/logging';
const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({
type: '*',
dataset: ENTITY_LATEST,
});
import { isArray } from 'lodash';
interface EntitySourceResponse {
sourceDataStreamType?: string | string[];
}
export async function getLatestEntity({
inventoryEsClient,
entityId,
entityType,
entityManagerClient,
from,
to,
logger,
}: {
inventoryEsClient: ObservabilityElasticsearchClient;
entityType: 'host' | 'container';
entityType: string;
entityId: string;
entityManagerClient: EntityClient;
from: string;
to: string;
logger: Logger;
}): Promise<EntitySourceResponse | undefined> {
try {
const { definitions } = await entityManagerClient.getEntityDefinitions({
builtIn: true,
const entityDefinitionsSource = await entityManagerClient.v2.readSourceDefinitions({
type: entityType,
});
const hostOrContainerIdentityField = definitions[0]?.identityFields?.[0]?.field;
const hostOrContainerIdentityField = entityDefinitionsSource[0]?.identity_fields?.[0];
if (hostOrContainerIdentityField === undefined) {
return undefined;
}
const response = await inventoryEsClient.esql<
{
'source_data_stream.type'?: string | string;
},
{ transform: 'plain' }
>(
'get_latest_entities',
{
query: `FROM ${ENTITIES_LATEST_ALIAS}
| WHERE ${ENTITY_TYPE} == ?
| WHERE ${hostOrContainerIdentityField} == ?
| KEEP ${SOURCE_DATA_STREAM_TYPE}
`,
params: [entityType, entityId],
},
{ transform: 'plain' }
);
const { entities } = await entityManagerClient.v2.searchEntities({
type: entityType,
limit: 1,
metadata_fields: ['data_stream.type'],
filters: [`${hostOrContainerIdentityField}: "${entityId}"`],
start: from,
end: to,
});
return { sourceDataStreamType: response.hits[0]['source_data_stream.type'] };
const entityDataStreamType = entities[0]['data_stream.type'];
return {
sourceDataStreamType: isArray(entityDataStreamType)
? entityDataStreamType.filter(Boolean)
: entityDataStreamType,
};
} catch (e) {
logger.error(e);
}

View file

@ -29,13 +29,20 @@ export const initEntitiesConfigurationRoutes = (libs: InfraBackendLibs) => {
]),
entityId: schema.string(),
}),
query: schema.object({ from: schema.string(), to: schema.string() }),
},
options: {
access: 'internal',
},
},
async (requestContext, request, response) => {
const { entityId, entityType } = request.params;
const { entityId, entityType: entityFilterType } = request.params;
const mapTypeToV2 = {
[BUILT_IN_ENTITY_TYPES.HOST]: BUILT_IN_ENTITY_TYPES.HOST_V2,
[BUILT_IN_ENTITY_TYPES.CONTAINER]: BUILT_IN_ENTITY_TYPES.CONTAINER_V2,
};
const entityType = mapTypeToV2[entityFilterType];
const { from, to } = request.query;
const [coreContext, infraContext] = await Promise.all([
requestContext.core,
requestContext.infra,
@ -64,9 +71,12 @@ export const initEntitiesConfigurationRoutes = (libs: InfraBackendLibs) => {
entityId,
entityManagerClient,
entityType,
entityFilterType,
infraMetricsClient,
obsEsClient,
logger,
from,
to,
});
return response.ok({
@ -74,6 +84,7 @@ export const initEntitiesConfigurationRoutes = (libs: InfraBackendLibs) => {
sourceDataStreams: sourceDataStreamTypes,
entityId,
entityType,
entityFilterType,
},
});
} catch (error) {

View file

@ -110,7 +110,6 @@
"@kbn/shared-ux-page-no-data-types",
"@kbn/xstate-utils",
"@kbn/entityManager-plugin",
"@kbn/entities-schema",
"@kbn/zod",
"@kbn/observability-utils-server",
"@kbn/core-plugins-server",