[8.x] [ECO][Inventory v2] APM changes (#202497) (#204524)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ECO][Inventory v2] APM changes
(#202497)](https://github.com/elastic/kibana/pull/202497)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Cauê
Marcondes","email":"55978943+cauemarcondes@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-12-17T09:24:33Z","message":"[ECO][Inventory
v2] APM changes (#202497)\n\nCloses
[202299](https://github.com/elastic/kibana/issues/202299)\r\n\r\n##
Summary\r\n\r\nThis PR replaces the query in `getServiceEntitySummary`
with the v2\r\nfunction
(`entityManagerClient.v2.searchEntities`)\r\n\r\n## Testing\r\n- Verify
the response of the summary endpoint in the
UI:\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/ba895f7d-57c8-492b-81dd-cf7869ffbc86\r\n\r\n\r\n\r\n-
Dev tools query\r\n - APM service\r\n #### Request:\r\n
```\r\nGET\r\nkbn:/internal/apm/entities/services/synth-node-0/summary?environment=ENVIRONMENT_ALL\r\n
```\r\n #### Response\r\n ```\r\n {\r\n \"serviceName\":
\"synth-node-0\",\r\n \"agentName\": \"nodejs\",\r\n
\"lastSeenTimestamp\": \"2024-12-13T16:29:19.868Z\",\r\n
\"dataStreamTypes\": [\r\n \"logs\",\r\n \"traces\"\r\n ],\r\n
\"environments\": [\r\n \"Synthtrace: simple_trace\"\r\n ]\r\n }\r\n
```\r\n\r\n - Service from logs\r\n #### Request:\r\n
```\r\nGET\r\nkbn:/internal/apm/entities/services/synth-node/summary?environment=ENVIRONMENT_ALL\r\n
```\r\n #### Response: \r\n ```\r\n {\r\n \"serviceName\":
\"synth-node\",\r\n \"agentName\": \"go\",\r\n \"lastSeenTimestamp\":
\"2024-12-13T16:27:43.461Z\",\r\n \"dataStreamTypes\": [\r\n
\"logs\",\r\n \"traces\"\r\n ],\r\n \"environments\": [\r\n
\"Synthtrace: logs_traces_hosts\"\r\n ]\r\n }\r\n
```\r\n\r\n---------\r\n\r\nCo-authored-by: Jenny
<dzheni.pavlova@elastic.co>\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"a66c1399e60b184c338fc99c9550539bd2ecf68b","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-infra_services"],"title":"[ECO][Inventory
v2] APM
changes","number":202497,"url":"https://github.com/elastic/kibana/pull/202497","mergeCommit":{"message":"[ECO][Inventory
v2] APM changes (#202497)\n\nCloses
[202299](https://github.com/elastic/kibana/issues/202299)\r\n\r\n##
Summary\r\n\r\nThis PR replaces the query in `getServiceEntitySummary`
with the v2\r\nfunction
(`entityManagerClient.v2.searchEntities`)\r\n\r\n## Testing\r\n- Verify
the response of the summary endpoint in the
UI:\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/ba895f7d-57c8-492b-81dd-cf7869ffbc86\r\n\r\n\r\n\r\n-
Dev tools query\r\n - APM service\r\n #### Request:\r\n
```\r\nGET\r\nkbn:/internal/apm/entities/services/synth-node-0/summary?environment=ENVIRONMENT_ALL\r\n
```\r\n #### Response\r\n ```\r\n {\r\n \"serviceName\":
\"synth-node-0\",\r\n \"agentName\": \"nodejs\",\r\n
\"lastSeenTimestamp\": \"2024-12-13T16:29:19.868Z\",\r\n
\"dataStreamTypes\": [\r\n \"logs\",\r\n \"traces\"\r\n ],\r\n
\"environments\": [\r\n \"Synthtrace: simple_trace\"\r\n ]\r\n }\r\n
```\r\n\r\n - Service from logs\r\n #### Request:\r\n
```\r\nGET\r\nkbn:/internal/apm/entities/services/synth-node/summary?environment=ENVIRONMENT_ALL\r\n
```\r\n #### Response: \r\n ```\r\n {\r\n \"serviceName\":
\"synth-node\",\r\n \"agentName\": \"go\",\r\n \"lastSeenTimestamp\":
\"2024-12-13T16:27:43.461Z\",\r\n \"dataStreamTypes\": [\r\n
\"logs\",\r\n \"traces\"\r\n ],\r\n \"environments\": [\r\n
\"Synthtrace: logs_traces_hosts\"\r\n ]\r\n }\r\n
```\r\n\r\n---------\r\n\r\nCo-authored-by: Jenny
<dzheni.pavlova@elastic.co>\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"a66c1399e60b184c338fc99c9550539bd2ecf68b"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202497","number":202497,"mergeCommit":{"message":"[ECO][Inventory
v2] APM changes (#202497)\n\nCloses
[202299](https://github.com/elastic/kibana/issues/202299)\r\n\r\n##
Summary\r\n\r\nThis PR replaces the query in `getServiceEntitySummary`
with the v2\r\nfunction
(`entityManagerClient.v2.searchEntities`)\r\n\r\n## Testing\r\n- Verify
the response of the summary endpoint in the
UI:\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/ba895f7d-57c8-492b-81dd-cf7869ffbc86\r\n\r\n\r\n\r\n-
Dev tools query\r\n - APM service\r\n #### Request:\r\n
```\r\nGET\r\nkbn:/internal/apm/entities/services/synth-node-0/summary?environment=ENVIRONMENT_ALL\r\n
```\r\n #### Response\r\n ```\r\n {\r\n \"serviceName\":
\"synth-node-0\",\r\n \"agentName\": \"nodejs\",\r\n
\"lastSeenTimestamp\": \"2024-12-13T16:29:19.868Z\",\r\n
\"dataStreamTypes\": [\r\n \"logs\",\r\n \"traces\"\r\n ],\r\n
\"environments\": [\r\n \"Synthtrace: simple_trace\"\r\n ]\r\n }\r\n
```\r\n\r\n - Service from logs\r\n #### Request:\r\n
```\r\nGET\r\nkbn:/internal/apm/entities/services/synth-node/summary?environment=ENVIRONMENT_ALL\r\n
```\r\n #### Response: \r\n ```\r\n {\r\n \"serviceName\":
\"synth-node\",\r\n \"agentName\": \"go\",\r\n \"lastSeenTimestamp\":
\"2024-12-13T16:27:43.461Z\",\r\n \"dataStreamTypes\": [\r\n
\"logs\",\r\n \"traces\"\r\n ],\r\n \"environments\": [\r\n
\"Synthtrace: logs_traces_hosts\"\r\n ]\r\n }\r\n
```\r\n\r\n---------\r\n\r\nCo-authored-by: Jenny
<dzheni.pavlova@elastic.co>\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"a66c1399e60b184c338fc99c9550539bd2ecf68b"}}]}]
BACKPORT-->

Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-12-17 22:14:39 +11:00 committed by GitHub
parent 4e7bca8637
commit ab308aa2d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 249 additions and 488 deletions

View file

@ -39,6 +39,7 @@
"uiActions",
"logsDataAccess",
"savedSearch",
"entityManager"
],
"optionalPlugins": [
"actions",

View file

@ -173,7 +173,9 @@ describe('Entity link', () => {
renderEntityLink({
isEntityCentricExperienceEnabled: true,
serviceEntitySummaryMockReturnValue: {
serviceEntitySummary: { dataStreamTypes: ['metrics'] } as unknown as ServiceEntitySummary,
serviceEntitySummary: {
['data_stream.type']: ['metrics'],
} as unknown as ServiceEntitySummary,
serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS,
},
hasApmDataFetcherMockReturnValue: {
@ -200,7 +202,9 @@ describe('Entity link', () => {
renderEntityLink({
isEntityCentricExperienceEnabled: true,
serviceEntitySummaryMockReturnValue: {
serviceEntitySummary: { dataStreamTypes: ['metrics'] } as unknown as ServiceEntitySummary,
serviceEntitySummary: {
['data_stream.type']: ['metrics'],
} as unknown as ServiceEntitySummary,
serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS,
},
hasApmDataFetcherMockReturnValue: {

View file

@ -1,67 +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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
ENTITY_FIRST_SEEN,
ENTITY_LAST_SEEN,
} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch';
import type { EntitiesESClient } from '../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client';
import { getEntityLatestServices } from './get_entity_latest_services';
import type { EntityLatestServiceRaw } from './types';
export function entitiesRangeQuery(start?: number, end?: number): QueryDslQueryContainer[] {
if (!start || !end) {
return [];
}
return [
{
range: {
[ENTITY_LAST_SEEN]: {
gte: start,
},
},
},
{
range: {
[ENTITY_FIRST_SEEN]: {
lte: end,
},
},
},
];
}
export async function getEntities({
entitiesESClient,
start,
end,
environment,
kuery,
size,
serviceName,
}: {
entitiesESClient: EntitiesESClient;
start: number;
end: number;
environment: string;
kuery?: string;
size: number;
serviceName?: string;
}): Promise<EntityLatestServiceRaw[]> {
const entityLatestServices = await getEntityLatestServices({
entitiesESClient,
start,
end,
environment,
kuery,
size,
serviceName,
});
return entityLatestServices;
}

View file

@ -1,59 +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 { kqlQuery, termQuery } from '@kbn/observability-plugin/server';
import {
ENTITY,
ENTITY_TYPE,
SOURCE_DATA_STREAM_TYPE,
} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch';
import { AGENT_NAME, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../common/es_fields/apm';
import { environmentQuery } from '../../../common/utils/environment_query';
import { EntitiesESClient } from '../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client';
import { entitiesRangeQuery } from './get_entities';
import { EntityLatestServiceRaw, EntityType } from './types';
export async function getEntityLatestServices({
entitiesESClient,
start,
end,
environment,
kuery,
size,
serviceName,
}: {
entitiesESClient: EntitiesESClient;
start?: number;
end?: number;
environment: string;
kuery?: string;
size: number;
serviceName?: string;
}): Promise<EntityLatestServiceRaw[]> {
const latestEntityServices = (
await entitiesESClient.searchLatest<EntityLatestServiceRaw>(`get_entity_latest_services`, {
body: {
size,
track_total_hits: false,
_source: [AGENT_NAME, ENTITY, SOURCE_DATA_STREAM_TYPE, SERVICE_NAME, SERVICE_ENVIRONMENT],
query: {
bool: {
filter: [
...kqlQuery(kuery),
...environmentQuery(environment, SERVICE_ENVIRONMENT),
...entitiesRangeQuery(start, end),
...termQuery(ENTITY_TYPE, EntityType.SERVICE),
...termQuery(SERVICE_NAME, serviceName),
],
},
},
},
})
).hits.hits.map((hit) => hit._source);
return latestEntityServices;
}

View file

@ -1,61 +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 { errors } from '@elastic/elasticsearch';
import { Logger } from '@kbn/core/server';
import { WrappedElasticsearchClientError } from '@kbn/observability-plugin/server';
import { EntitiesESClient } from '../../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client';
import { withApmSpan } from '../../../utils/with_apm_span';
import { getEntities } from '../get_entities';
import { mergeEntities } from '../utils/merge_entities';
export const MAX_NUMBER_OF_SERVICES = 1_000;
export async function getServiceEntities({
entitiesESClient,
start,
end,
kuery,
environment,
logger,
}: {
entitiesESClient: EntitiesESClient;
start: number;
end: number;
kuery: string;
environment: string;
logger: Logger;
}) {
return withApmSpan('get_service_entities', async () => {
try {
const entities = await getEntities({
entitiesESClient,
start,
end,
kuery,
environment,
size: MAX_NUMBER_OF_SERVICES,
});
return mergeEntities({ entities });
} catch (error) {
// If the index does not exist, handle it gracefully
if (
error instanceof WrappedElasticsearchClientError &&
error.originalError instanceof errors.ResponseError
) {
const type = error.originalError.body.error.type;
if (type === 'index_not_found_exception') {
logger.error(`Entities index does not exist. Unable to fetch services.`);
return [];
}
}
throw error;
}
});
}

View file

@ -5,28 +5,31 @@
* 2.0.
*/
import type { EntitiesESClient } from '../../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client';
import { SERVICE_NAME, AGENT_NAME, SERVICE_ENVIRONMENT } from '@kbn/apm-types';
import { BUILT_IN_ENTITY_TYPES, DATA_STREAM_TYPE } from '@kbn/observability-shared-plugin/common';
import moment from 'moment';
import type { EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
import { withApmSpan } from '../../../utils/with_apm_span';
import { getEntityLatestServices } from '../get_entity_latest_services';
import { mergeEntities } from '../utils/merge_entities';
import { MAX_NUMBER_OF_SERVICES } from './get_service_entities';
interface Params {
entitiesESClient: EntitiesESClient;
entityManagerClient: EntityClient;
serviceName: string;
environment: string;
}
export function getServiceEntitySummary({ entitiesESClient, environment, serviceName }: Params) {
export function getServiceEntitySummary({ entityManagerClient, environment, serviceName }: Params) {
return withApmSpan('get_service_entity_summary', async () => {
const entityLatestServices = await getEntityLatestServices({
entitiesESClient,
environment,
size: MAX_NUMBER_OF_SERVICES,
serviceName,
const serviceEntitySummary = await entityManagerClient.v2.searchEntities({
start: moment().subtract(15, 'm').toISOString(),
end: moment().toISOString(),
type: BUILT_IN_ENTITY_TYPES.SERVICE_V2,
filters: [`${SERVICE_NAME}: "${serviceName}"`],
limit: 1,
metadata_fields: [DATA_STREAM_TYPE, AGENT_NAME, SERVICE_ENVIRONMENT],
});
const serviceEntity = mergeEntities({ entities: entityLatestServices });
const serviceEntity = mergeEntities({ entities: serviceEntitySummary?.entities });
return serviceEntity[0];
});
}

View file

@ -6,10 +6,8 @@
*/
import * as t from 'io-ts';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { createEntitiesESClient } from '../../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client';
import { createApmServerRoute } from '../../apm_routes/create_apm_server_route';
import { environmentRt, kueryRt, rangeRt } from '../../default_api_types';
import { getServiceEntities } from './get_service_entities';
import { getServiceEntitySummary } from './get_service_entity_summary';
const serviceEntitiesSummaryRoute = createApmServerRoute({
@ -20,52 +18,20 @@ const serviceEntitiesSummaryRoute = createApmServerRoute({
}),
security: { authz: { requiredPrivileges: ['apm'] } },
async handler(resources) {
const { context, params, request } = resources;
const coreContext = await context.core;
const entitiesESClient = await createEntitiesESClient({
request,
esClient: coreContext.elasticsearch.client.asCurrentUser,
});
const { params, request, plugins } = resources;
const entityManagerStart = await plugins.entityManager.start();
const entityManagerClient = await entityManagerStart.getScopedClient({ request });
const { serviceName } = params.path;
const { environment } = params.query;
return getServiceEntitySummary({
entitiesESClient,
const serviceEntitySummary = await getServiceEntitySummary({
entityManagerClient,
serviceName,
environment,
});
},
});
const servicesEntitiesRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/entities/services',
params: t.type({
query: t.intersection([environmentRt, kueryRt, rangeRt]),
}),
security: { authz: { requiredPrivileges: ['apm'] } },
async handler(resources) {
const { context, params, request } = resources;
const coreContext = await context.core;
const entitiesESClient = await createEntitiesESClient({
request,
esClient: coreContext.elasticsearch.client.asCurrentUser,
});
const { start, end, kuery, environment } = params.query;
const services = await getServiceEntities({
entitiesESClient,
start,
end,
kuery,
environment,
logger: resources.logger,
});
return { services };
return serviceEntitySummary;
},
});
@ -137,7 +103,6 @@ const serviceLogErrorRateTimeseriesRoute = createApmServerRoute({
});
export const servicesEntitiesRoutesRepository = {
...servicesEntitiesRoute,
...serviceLogRateTimeseriesRoute,
...serviceLogErrorRateTimeseriesRoute,
...serviceEntitiesSummaryRoute,

View file

@ -4,28 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
import type { EntityMetadata, EntityV2 } from '@kbn/entities-schema';
export enum EntityType {
SERVICE = 'service',
}
export interface EntityLatestServiceRaw {
agent: {
name: AgentName[];
};
source_data_stream: {
type: string[];
};
service: {
name: string;
environment?: string;
};
entity: Entity;
}
interface Entity {
id: string;
last_seen_timestamp: string;
identity_fields: string[];
}
export type EntityLatestServiceRaw = EntityV2 & EntityMetadata;

View file

@ -13,17 +13,14 @@ describe('mergeEntities', () => {
it('modifies one service', () => {
const entities: EntityLatestServiceRaw[] = [
{
service: {
name: 'service-1',
environment: 'test',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: ['metrics', 'logs'] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name', 'service.environment'],
id: 'service-1:test',
},
'data_stream.type': ['metrics', 'logs'],
'agent.name': 'nodejs',
'service.environment': 'test',
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1',
'entity.display_name': 'service-1',
},
];
const result = mergeEntities({ entities });
@ -32,7 +29,7 @@ describe('mergeEntities', () => {
agentName: 'nodejs' as AgentName,
dataStreamTypes: ['metrics', 'logs'],
environments: ['test'],
lastSeenTimestamp: '2024-06-05T10:34:40.810Z',
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-1',
},
]);
@ -41,56 +38,44 @@ describe('mergeEntities', () => {
it('joins two service with the same name ', () => {
const entities: EntityLatestServiceRaw[] = [
{
service: {
name: 'service-1',
environment: 'env-service-1',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: ['foo'] },
entity: {
last_seen_timestamp: '2024-03-05T10:34:40.810Z',
identity_fields: ['service.name', 'service.environment'],
id: 'service-1:env-service-1',
},
'data_stream.type': ['foo'],
'agent.name': 'nodejs',
'service.environment': 'env-service-1',
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:env-service-1',
'entity.display_name': 'service-1',
},
{
service: {
name: 'service-1',
environment: 'env-service-2',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: ['bar'] },
entity: {
last_seen_timestamp: '2024-03-05T10:34:40.810Z',
identity_fields: ['service.name', 'service.environment'],
id: 'apm-only-1:synthtrace-env-2',
},
'data_stream.type': ['bar'],
'agent.name': 'nodejs',
'service.environment': 'env-service-2',
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:env-service-2',
'entity.display_name': 'service-1',
},
{
service: {
name: 'service-2',
environment: 'env-service-3',
},
agent: { name: ['java'] },
source_data_stream: { type: ['baz'] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name', 'service.environment'],
id: 'service-2:env-service-3',
},
'data_stream.type': ['baz'],
'agent.name': 'java',
'service.environment': 'env-service-3',
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-2',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-2:env-service-3',
'entity.display_name': 'service-2',
},
{
service: {
name: 'service-2',
environment: 'env-service-4',
},
agent: { name: ['java'] },
source_data_stream: { type: ['baz'] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name', 'service.environment'],
id: 'service-2:env-service-3',
},
'data_stream.type': ['baz'],
'agent.name': ['java'],
'service.environment': 'env-service-4',
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-2',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-2:env-service-4',
'entity.display_name': 'service-2',
},
];
@ -100,14 +85,14 @@ describe('mergeEntities', () => {
agentName: 'nodejs' as AgentName,
dataStreamTypes: ['foo', 'bar'],
environments: ['env-service-1', 'env-service-2'],
lastSeenTimestamp: '2024-03-05T10:34:40.810Z',
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-1',
},
{
agentName: 'java' as AgentName,
dataStreamTypes: ['baz'],
environments: ['env-service-3', 'env-service-4'],
lastSeenTimestamp: '2024-06-05T10:34:40.810Z',
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-2',
},
]);
@ -115,43 +100,34 @@ describe('mergeEntities', () => {
it('handles duplicate environments and data streams', () => {
const entities: EntityLatestServiceRaw[] = [
{
service: {
name: 'service-1',
environment: 'test',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: ['metrics', 'logs'] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name', 'service.environment'],
id: 'service-1:test',
},
'data_stream.type': ['metrics', 'logs'],
'agent.name': ['nodejs'],
'service.environment': 'test',
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:test',
'entity.display_name': 'service-1',
},
{
service: {
name: 'service-1',
environment: 'test',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: ['metrics', 'logs'] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name', 'service.environment'],
id: 'service-1:test',
},
'data_stream.type': ['metrics', 'logs'],
'agent.name': ['nodejs'],
'service.environment': 'test',
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:test',
'entity.display_name': 'service-1',
},
{
service: {
name: 'service-1',
environment: 'prod',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: ['foo'] },
entity: {
last_seen_timestamp: '2024-23-05T10:34:40.810Z',
identity_fields: ['service.name', 'service.environment'],
id: 'service-1:prod',
},
'data_stream.type': ['foo'],
'agent.name': ['nodejs'],
'service.environment': 'prod',
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:prod',
'entity.display_name': 'service-1',
},
];
const result = mergeEntities({ entities });
@ -160,7 +136,7 @@ describe('mergeEntities', () => {
agentName: 'nodejs' as AgentName,
dataStreamTypes: ['metrics', 'logs', 'foo'],
environments: ['test', 'prod'],
lastSeenTimestamp: '2024-23-05T10:34:40.810Z',
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-1',
},
]);
@ -168,17 +144,14 @@ describe('mergeEntities', () => {
it('handles null environment', () => {
const entity: EntityLatestServiceRaw[] = [
{
service: {
name: 'service-1',
environment: undefined,
},
agent: { name: ['nodejs'] },
source_data_stream: { type: [] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name'],
id: 'service-1:test',
},
'data_stream.type': [],
'agent.name': ['nodejs'],
'service.environment': null,
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:test',
'entity.display_name': 'service-1',
},
];
const entityResult = mergeEntities({ entities: entity });
@ -187,35 +160,31 @@ describe('mergeEntities', () => {
agentName: 'nodejs' as AgentName,
dataStreamTypes: [],
environments: [],
lastSeenTimestamp: '2024-06-05T10:34:40.810Z',
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-1',
},
]);
const entities: EntityLatestServiceRaw[] = [
{
service: {
name: 'service-1',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: [] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name'],
id: 'service-1:test',
},
'data_stream.type': [],
'agent.name': ['nodejs'],
'service.environment': null,
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:test',
'entity.display_name': 'service-1',
},
{
service: {
name: 'service-1',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: [] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name'],
id: 'service-1:test',
},
'data_stream.type': [],
'agent.name': ['nodejs'],
'service.environment': null,
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:test',
'entity.display_name': 'service-1',
},
];
const result = mergeEntities({ entities });
@ -224,7 +193,7 @@ describe('mergeEntities', () => {
agentName: 'nodejs' as AgentName,
dataStreamTypes: [],
environments: [],
lastSeenTimestamp: '2024-06-05T10:34:40.810Z',
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-1',
},
]);
@ -233,16 +202,13 @@ describe('mergeEntities', () => {
it('handles undefined environment', () => {
const entity: EntityLatestServiceRaw[] = [
{
service: {
name: 'service-1',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: [] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name'],
id: 'service-1:test',
},
'data_stream.type': [],
'agent.name': ['nodejs'],
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:test',
'entity.display_name': 'service-1',
},
];
const entityResult = mergeEntities({ entities: entity });
@ -251,35 +217,29 @@ describe('mergeEntities', () => {
agentName: 'nodejs',
dataStreamTypes: [],
environments: [],
lastSeenTimestamp: '2024-06-05T10:34:40.810Z',
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-1',
},
]);
const entities: EntityLatestServiceRaw[] = [
{
service: {
name: 'service-1',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: [] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name'],
id: 'service-1:test',
},
'data_stream.type': [],
'agent.name': ['nodejs'],
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:test',
'entity.display_name': 'service-1',
},
{
service: {
name: 'service-1',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: [] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name'],
id: 'service-1:test',
},
'data_stream.type': [],
'agent.name': ['nodejs'],
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:test',
'entity.display_name': 'service-1',
},
];
const result = mergeEntities({ entities });
@ -288,7 +248,7 @@ describe('mergeEntities', () => {
agentName: 'nodejs',
dataStreamTypes: [],
environments: [],
lastSeenTimestamp: '2024-06-05T10:34:40.810Z',
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-1',
},
]);
@ -297,17 +257,14 @@ describe('mergeEntities', () => {
it('has no logs when log rate is not returned', () => {
const entities: EntityLatestServiceRaw[] = [
{
service: {
name: 'service-1',
environment: 'test',
},
agent: { name: ['nodejs'] },
source_data_stream: { type: ['metrics'] },
entity: {
last_seen_timestamp: '2024-06-05T10:34:40.810Z',
identity_fields: ['service.name', 'service.environment'],
id: 'service-1:test',
},
'data_stream.type': ['metrics'],
'agent.name': ['nodejs'],
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'service.environment': 'test',
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:test',
'entity.display_name': 'service-1',
},
];
const result = mergeEntities({ entities });
@ -316,7 +273,31 @@ describe('mergeEntities', () => {
agentName: 'nodejs' as AgentName,
dataStreamTypes: ['metrics'],
environments: ['test'],
lastSeenTimestamp: '2024-06-05T10:34:40.810Z',
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-1',
},
]);
});
it('has multiple duplicate environments and data stream types', () => {
const entities: EntityLatestServiceRaw[] = [
{
'data_stream.type': ['metrics', 'metrics', 'logs', 'logs'],
'agent.name': ['nodejs', 'nodejs'],
'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z',
'service.name': 'service-1',
'service.environment': ['test', 'test', 'test'],
'entity.type': 'built_in_services_from_ecs_data',
'entity.id': 'service-1:test',
'entity.display_name': 'service-1',
},
];
const result = mergeEntities({ entities });
expect(result).toEqual([
{
agentName: 'nodejs' as AgentName,
dataStreamTypes: ['metrics', 'logs'],
environments: ['test'],
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-1',
},
]);

View file

@ -23,7 +23,7 @@ export function mergeEntities({
entities: EntityLatestServiceRaw[];
}): MergedServiceEntity[] {
const mergedEntities = entities.reduce((map, current) => {
const key = current.service.name;
const key = current['service.name'];
if (map.has(key)) {
const existingEntity = map.get(key);
map.set(key, mergeFunc(current, existingEntity));
@ -33,28 +33,37 @@ export function mergeEntities({
return map;
}, new Map());
return [...mergedEntities.values()];
return [...new Set(mergedEntities.values())];
}
function mergeFunc(entity: EntityLatestServiceRaw, existingEntity?: MergedServiceEntity) {
const commonEntityFields = {
serviceName: entity.service.name,
agentName: entity.agent.name[0],
lastSeenTimestamp: entity.entity.last_seen_timestamp,
serviceName: entity['service.name'],
agentName:
Array.isArray(entity['agent.name']) && entity['agent.name'].length > 0
? entity['agent.name'][0]
: entity['agent.name'],
lastSeenTimestamp: entity['entity.last_seen_timestamp'],
};
if (!existingEntity) {
return {
...commonEntityFields,
dataStreamTypes: entity.source_data_stream.type,
environments: compact([entity?.service.environment]),
dataStreamTypes: uniq(entity['data_stream.type']),
environments: uniq(
compact(
Array.isArray(entity['service.environment'])
? entity['service.environment']
: [entity['service.environment']]
)
),
};
}
return {
...commonEntityFields,
dataStreamTypes: uniq(
compact([...(existingEntity?.dataStreamTypes ?? []), ...entity.source_data_stream.type])
compact([...(existingEntity?.dataStreamTypes ?? []), ...entity['data_stream.type']])
),
environments: uniq(compact([...existingEntity?.environments, entity?.service.environment])),
environments: uniq(compact([...existingEntity?.environments, entity['service.environment']])),
};
}

View file

@ -80,7 +80,6 @@ import {
import { getThroughput, ServiceThroughputResponse } from './get_throughput';
import { getServiceEntitySummary } from '../entities/services/get_service_entity_summary';
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
import { createEntitiesESClient } from '../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client';
const servicesRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services',
@ -297,16 +296,11 @@ const serviceAgentRoute = createApmServerRoute({
}),
security: { authz: { requiredPrivileges: ['apm'] } },
handler: async (resources): Promise<ServiceAgentResponse> => {
const { context, request } = resources;
const coreContext = await context.core;
const { request, plugins } = resources;
const entityManagerStart = await plugins.entityManager.start();
const [apmEventClient, entitiesESClient] = await Promise.all([
getApmEventClient(resources),
createEntitiesESClient({
request,
esClient: coreContext.elasticsearch.client.asCurrentUser,
}),
]);
const apmEventClient = await getApmEventClient(resources);
const entityManagerClient = await entityManagerStart.getScopedClient({ request });
const { params } = resources;
const { serviceName } = params.path;
const { start, end } = params.query;
@ -320,7 +314,7 @@ const serviceAgentRoute = createApmServerRoute({
}),
getServiceEntitySummary({
serviceName,
entitiesESClient,
entityManagerClient,
environment: ENVIRONMENT_ALL.value,
}),
]);

View file

@ -5,52 +5,50 @@
* 2.0.
*/
import { SharePluginSetup } from '@kbn/share-plugin/server';
import { Observable } from 'rxjs';
import {
RuleRegistryPluginSetupContract,
RuleRegistryPluginStartContract,
} from '@kbn/rule-registry-plugin/server';
import {
PluginSetup as DataPluginSetup,
PluginStart as DataPluginStart,
} from '@kbn/data-plugin/server';
import {
import type {
ApmDataAccessPluginSetup,
ApmDataAccessPluginStart,
} from '@kbn/apm-data-access-plugin/server';
import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server';
import type {
PluginSetup as DataPluginSetup,
PluginStart as DataPluginStart,
} from '@kbn/data-plugin/server';
import type {
RuleRegistryPluginSetupContract,
RuleRegistryPluginStartContract,
} from '@kbn/rule-registry-plugin/server';
import type { SharePluginSetup } from '@kbn/share-plugin/server';
import type { Observable } from 'rxjs';
import type { ActionsPlugin } from '@kbn/actions-plugin/server';
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import type { FeaturesPluginSetup, FeaturesPluginStart } from '@kbn/features-plugin/server';
import { HomeServerPluginSetup, HomeServerPluginStart } from '@kbn/home-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { ActionsPlugin } from '@kbn/actions-plugin/server';
import type { AlertingServerSetup, AlertingServerStart } from '@kbn/alerting-plugin/server';
import { CloudSetup } from '@kbn/cloud-plugin/server';
import { FeaturesPluginSetup, FeaturesPluginStart } from '@kbn/features-plugin/server';
import { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server';
import { MlPluginSetup, MlPluginStart } from '@kbn/ml-plugin/server';
import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server';
import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server';
import {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import {
FleetSetupContract as FleetPluginSetup,
FleetStartContract as FleetPluginStart,
} from '@kbn/fleet-plugin/server';
import { MetricsDataPluginSetup } from '@kbn/metrics-data-access-plugin/server';
import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import {
import type { MetricsDataPluginSetup } from '@kbn/metrics-data-access-plugin/server';
import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server';
import type {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import type {
CustomIntegrationsPluginSetup,
CustomIntegrationsPluginStart,
} from '@kbn/custom-integrations-plugin/server';
import {
ProfilingDataAccessPluginSetup,
ProfilingDataAccessPluginStart,
} from '@kbn/profiling-data-access-plugin/server';
import {
import type {
EntityManagerServerPluginSetup,
EntityManagerServerPluginStart,
} from '@kbn/entityManager-plugin/server';
import type {
LogsDataAccessPluginSetup,
LogsDataAccessPluginStart,
} from '@kbn/logs-data-access-plugin/server';
@ -58,6 +56,10 @@ import type {
ObservabilityAIAssistantServerSetup,
ObservabilityAIAssistantServerStart,
} from '@kbn/observability-ai-assistant-plugin/server';
import type {
ProfilingDataAccessPluginSetup,
ProfilingDataAccessPluginStart,
} from '@kbn/profiling-data-access-plugin/server';
import { APMConfig } from '.';
export interface APMPluginSetup {
@ -75,8 +77,10 @@ export interface APMPluginSetupDependencies {
metricsDataAccess: MetricsDataPluginSetup;
dataViews: {};
share: SharePluginSetup;
observabilityAIAssistant?: ObservabilityAIAssistantServerSetup;
logsDataAccess: LogsDataAccessPluginSetup;
entityManager: EntityManagerServerPluginSetup;
// optional dependencies
observabilityAIAssistant?: ObservabilityAIAssistantServerSetup;
actions?: ActionsPlugin['setup'];
alerting?: AlertingServerSetup;
cloud?: CloudSetup;
@ -89,7 +93,6 @@ export interface APMPluginSetupDependencies {
usageCollection?: UsageCollectionSetup;
customIntegrations?: CustomIntegrationsPluginSetup;
profilingDataAccess?: ProfilingDataAccessPluginSetup;
logsDataAccess: LogsDataAccessPluginSetup;
}
export interface APMPluginStartDependencies {
// required dependencies
@ -102,8 +105,10 @@ export interface APMPluginStartDependencies {
metricsDataAccess: MetricsDataPluginSetup;
dataViews: DataViewsServerPluginStart;
share: undefined;
observabilityAIAssistant?: ObservabilityAIAssistantServerStart;
logsDataAccess: LogsDataAccessPluginStart;
entityManager: EntityManagerServerPluginStart;
// optional dependencies
observabilityAIAssistant?: ObservabilityAIAssistantServerStart;
actions?: ActionsPlugin['start'];
alerting?: AlertingServerStart;
cloud?: undefined;
@ -116,5 +121,4 @@ export interface APMPluginStartDependencies {
usageCollection?: undefined;
customIntegrations?: CustomIntegrationsPluginStart;
profilingDataAccess?: ProfilingDataAccessPluginStart;
logsDataAccess: LogsDataAccessPluginStart;
}

View file

@ -129,6 +129,7 @@
"@kbn/alerting-comparators",
"@kbn/saved-search-component",
"@kbn/saved-search-plugin",
"@kbn/entityManager-plugin",
],
"exclude": ["target/**/*"]
}

View file

@ -14,6 +14,7 @@ export const BUILT_IN_ENTITY_TYPES = {
HOST: 'host',
CONTAINER: 'container',
SERVICE: 'service',
SERVICE_V2: 'built_in_services_from_ecs_data',
KUBERNETES: {
CLUSTER: createKubernetesEntity('cluster'),
CONTAINER: createKubernetesEntity('container'),

View file

@ -146,6 +146,8 @@ export const PROFILE_ALLOC_SPACE = 'profile.alloc_space.bytes';
export const PROFILE_INUSE_OBJECTS = 'profile.inuse_objects.count';
export const PROFILE_INUSE_SPACE = 'profile.inuse_space.bytes';
export const DATA_STREAM_TYPE = 'data_stream.type';
export const ENTITY = 'entity';
export const ENTITY_ID = 'entity.id';
export const ENTITY_TYPE = 'entity.type';

View file

@ -128,15 +128,16 @@ export {
PROFILE_ALLOC_SPACE,
PROFILE_INUSE_OBJECTS,
PROFILE_INUSE_SPACE,
DATA_STREAM_TYPE,
ENTITY,
ENTITY_DEFINITION_ID,
ENTITY_DISPLAY_NAME,
ENTITY_FIRST_SEEN,
ENTITY_ID,
ENTITY_LAST_SEEN,
ENTITY_TYPE,
SOURCE_DATA_STREAM_TYPE,
ENTITY_LAST_SEEN,
ENTITY_FIRST_SEEN,
ENTITY_DISPLAY_NAME,
ENTITY_DEFINITION_ID,
ENTITY_IDENTITY_FIELDS,
SOURCE_DATA_STREAM_TYPE,
} from './field_names/elasticsearch';
export {