Skip to content

Commit 1b7339d

Browse files
[8.19] [APM] Fix service map showing duplicate nodes (#214184) (#219992)
# Backport This will backport the following commits from `main` to `8.19`: - [[APM] Fix service map showing duplicate nodes (#214184)](#214184) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Carlos Crespo","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-03-13T12:20:04Z","message":"[APM] Fix service map showing duplicate nodes (#214184)\n\nfixes [214167](https://github.com/elastic/kibana/issues/214167)\n## Summary\n\nFixes a problem affecting the new service map api, causing the service\nmap to show duplicate exit span nodes\n\n\n\n<img width=\"1476\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/f7bf6035-17f5-4265-9950-5c764f1e7f0b\"\n/>\n\n### how to test\n\n- Enable the new service map api by adding\n`xpack.apm.ui.serviceMapApiV2Enabled: true` to `kibana.dev.yml`\n- Run the `service_map` synthtrace scenario\n- Navigate to APM and view the service map for the `frontend-rum`\nservice\n\n---------\n\nCo-authored-by: Elastic Machine <[email protected]>","sha":"9ea6de27cc1f91999e7405629aaec1659910d30c","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:skip","Team:obs-ux-infra_services","v9.1.0","v8.19.0"],"title":"[APM] Fix service map showing duplicate nodes","number":214184,"url":"https://github.com/elastic/kibana/pull/214184","mergeCommit":{"message":"[APM] Fix service map showing duplicate nodes (#214184)\n\nfixes [214167](https://github.com/elastic/kibana/issues/214167)\n## Summary\n\nFixes a problem affecting the new service map api, causing the service\nmap to show duplicate exit span nodes\n\n\n\n<img width=\"1476\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/f7bf6035-17f5-4265-9950-5c764f1e7f0b\"\n/>\n\n### how to test\n\n- Enable the new service map api by adding\n`xpack.apm.ui.serviceMapApiV2Enabled: true` to `kibana.dev.yml`\n- Run the `service_map` synthtrace scenario\n- Navigate to APM and view the service map for the `frontend-rum`\nservice\n\n---------\n\nCo-authored-by: Elastic Machine <[email protected]>","sha":"9ea6de27cc1f91999e7405629aaec1659910d30c"}},"sourceBranch":"main","suggestedTargetBranches":["8.19"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/214184","number":214184,"mergeCommit":{"message":"[APM] Fix service map showing duplicate nodes (#214184)\n\nfixes [214167](https://github.com/elastic/kibana/issues/214167)\n## Summary\n\nFixes a problem affecting the new service map api, causing the service\nmap to show duplicate exit span nodes\n\n\n\n<img width=\"1476\" alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/f7bf6035-17f5-4265-9950-5c764f1e7f0b\"\n/>\n\n### how to test\n\n- Enable the new service map api by adding\n`xpack.apm.ui.serviceMapApiV2Enabled: true` to `kibana.dev.yml`\n- Run the `service_map` synthtrace scenario\n- Navigate to APM and view the service map for the `frontend-rum`\nservice\n\n---------\n\nCo-authored-by: Elastic Machine <[email protected]>","sha":"9ea6de27cc1f91999e7405629aaec1659910d30c"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Elastic Machine <[email protected]>
1 parent 70242a2 commit 1b7339d

File tree

3 files changed

+63
-22
lines changed

3 files changed

+63
-22
lines changed

x-pack/solutions/observability/plugins/apm/common/service_map/get_service_map_nodes.test.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ const httpLoadBalancer = createExitSpan({
6363
spanSubtype: 'http',
6464
});
6565

66+
const elasticSearchExternal = createExitSpan({
67+
spanDestinationServiceResource: 'elasticsearch',
68+
spanType: 'db',
69+
spanSubtype: 'elasticsearch',
70+
});
71+
6672
// Define anomalies
6773
const anomalies = {
6874
mlJobIds: ['apm-test-1234-ml-module-name'],
@@ -133,14 +139,10 @@ describe('getServiceMapNodes', () => {
133139

134140
const { edges, nodes } = partitionElements(elements);
135141

136-
expect(getIds(nodes)).toEqual([
137-
'>opbeans-java|kafka/some-queue',
138-
'opbeans-java',
139-
'opbeans-node',
140-
]);
142+
expect(getIds(nodes)).toEqual(['>kafka/some-queue', 'opbeans-java', 'opbeans-node']);
141143
expect(getIds(edges)).toEqual([
142-
'>opbeans-java|kafka/some-queue~opbeans-node',
143-
'opbeans-java~>opbeans-java|kafka/some-queue',
144+
'>kafka/some-queue~opbeans-node',
145+
'opbeans-java~>kafka/some-queue',
144146
]);
145147
});
146148

@@ -155,17 +157,21 @@ describe('getServiceMapNodes', () => {
155157
from: getExternalConnectionNode({ ...nodejsExternal, ...javaService }),
156158
to: getServiceConnectionNode(nodejsService),
157159
},
160+
{
161+
from: getExternalConnectionNode({ ...nodejsExternal, ...goService }),
162+
to: getServiceConnectionNode(nodejsService),
163+
},
158164
],
159165
connections: [
160166
{
161167
source: getServiceConnectionNode(javaService),
162168
destination: getExternalConnectionNode({ ...nodejsExternal, ...javaService }),
163169
},
164170
{
165-
source: getServiceConnectionNode(javaService),
171+
source: getServiceConnectionNode(goService),
166172
destination: getExternalConnectionNode({
167173
...nodejsExternal,
168-
...javaService,
174+
...goService,
169175
spanType: 'foo',
170176
}),
171177
},
@@ -177,8 +183,37 @@ describe('getServiceMapNodes', () => {
177183

178184
const { edges, nodes } = partitionElements(elements);
179185

180-
expect(getIds(nodes)).toEqual(['opbeans-java', 'opbeans-node']);
181-
expect(getIds(edges)).toEqual(['opbeans-java~opbeans-node']);
186+
expect(getIds(nodes)).toEqual(['opbeans-go', 'opbeans-java', 'opbeans-node']);
187+
expect(getIds(edges)).toEqual(['opbeans-go~opbeans-node', 'opbeans-java~opbeans-node']);
188+
});
189+
190+
it('collapses external destinations based on span.destination.resource.name for exit spans without downstream transactions', () => {
191+
const response: ServiceMapConnections = {
192+
servicesData: [],
193+
exitSpanDestinations: [],
194+
connections: [
195+
{
196+
source: getServiceConnectionNode(javaService),
197+
destination: getExternalConnectionNode({ ...elasticSearchExternal, ...javaService }),
198+
},
199+
{
200+
source: getServiceConnectionNode(goService),
201+
destination: getExternalConnectionNode({
202+
...elasticSearchExternal,
203+
...goService,
204+
spanType: 'foo',
205+
}),
206+
},
207+
],
208+
anomalies,
209+
};
210+
211+
const { elements } = getServiceMapNodes(response);
212+
213+
const { edges, nodes } = partitionElements(elements);
214+
215+
expect(getIds(nodes)).toEqual(['>elasticsearch', 'opbeans-go', 'opbeans-java']);
216+
expect(getIds(edges)).toEqual(['opbeans-go~>elasticsearch', 'opbeans-java~>elasticsearch']);
182217
});
183218

184219
it('picks the first span.type/subtype in an alphabetically sorted list', () => {
@@ -213,10 +248,10 @@ describe('getServiceMapNodes', () => {
213248

214249
const { edges, nodes } = partitionElements(elements);
215250

216-
expect(getIds(nodes)).toEqual(['>opbeans-java|opbeans-node', 'opbeans-java']);
217-
expect(getIds(edges)).toEqual(['opbeans-java~>opbeans-java|opbeans-node']);
251+
expect(getIds(nodes)).toEqual(['>opbeans-node', 'opbeans-java']);
252+
expect(getIds(edges)).toEqual(['opbeans-java~>opbeans-node']);
218253

219-
const nodejsNode = elements.find((node) => node.data.id === '>opbeans-java|opbeans-node');
254+
const nodejsNode = elements.find((node) => node.data.id === '>opbeans-node');
220255
// @ts-expect-error
221256
expect(nodejsNode?.data[SPAN_TYPE]).toBe('external');
222257
// @ts-expect-error

x-pack/solutions/observability/plugins/apm/common/service_map/get_service_map_nodes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import type {
2626
} from './types';
2727

2828
import { groupResourceNodes } from './group_resource_nodes';
29-
import { getEdgeId, isExitSpan } from './utils';
29+
import { getEdgeId, getExitSpanNodeId, isExitSpan } from './utils';
3030

3131
const FORBIDDEN_SERVICE_NAMES = ['constructor'];
3232

@@ -178,8 +178,10 @@ function mapNodes({
178178
const exitSpanNodes = exitSpans.get(id) ?? [];
179179
if (exitSpanNodes.length > 0) {
180180
const exitSpanSample = exitSpanNodes[0];
181+
181182
mappedNodes.set(id, {
182183
...exitSpanSample,
184+
id: getExitSpanNodeId(exitSpanSample),
183185
label: exitSpanSample[SPAN_DESTINATION_SERVICE_RESOURCE],
184186
[SPAN_TYPE]: exitSpanNodes.map((n) => n[SPAN_TYPE]).sort()[0],
185187
[SPAN_SUBTYPE]: exitSpanNodes.map((n) => n[SPAN_SUBTYPE]).sort()[0],

x-pack/solutions/observability/plugins/apm/common/service_map/utils.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,33 +83,37 @@ export const isExitSpan = (
8383
};
8484

8585
// backward compatibility with scrited_metric versions
86-
export const getLegacyNodeId = (node: ConnectionNodeLegacy) => {
86+
export function getLegacyNodeId(node: ConnectionNodeLegacy) {
8787
if (isExitSpan(node)) {
88-
return `>${node[SPAN_DESTINATION_SERVICE_RESOURCE]}`;
88+
return getExitSpanNodeId(node);
8989
}
9090
return `${node[SERVICE_NAME]}`;
91-
};
91+
}
9292

93-
export const getServiceConnectionNode = (event: ServiceMapService): ServiceConnectionNode => {
93+
export function getServiceConnectionNode(event: ServiceMapService): ServiceConnectionNode {
9494
return {
9595
id: event.serviceName,
9696
[SERVICE_NAME]: event.serviceName,
9797
[SERVICE_ENVIRONMENT]: event.serviceEnvironment || null,
9898
[AGENT_NAME]: event.agentName,
9999
};
100-
};
100+
}
101101

102-
export const getExternalConnectionNode = (event: ServiceMapExitSpan): ExternalConnectionNode => {
102+
export function getExternalConnectionNode(event: ServiceMapExitSpan): ExternalConnectionNode {
103103
return {
104104
id: `>${event.serviceName}|${event.spanDestinationServiceResource}`,
105105
[SPAN_DESTINATION_SERVICE_RESOURCE]: event.spanDestinationServiceResource,
106106
[SPAN_TYPE]: event.spanType,
107107
[SPAN_SUBTYPE]: event.spanSubtype,
108108
};
109-
};
109+
}
110110

111111
export function getEdgeId(sourceId: string, destinationId: string) {
112112
return `${sourceId}~${destinationId}`;
113113
}
114114

115+
export function getExitSpanNodeId(span: ExternalConnectionNode) {
116+
return `>${span[SPAN_DESTINATION_SERVICE_RESOURCE]}`;
117+
}
118+
115119
export const SERVICE_MAP_TIMEOUT_ERROR = 'ServiceMapTimeoutError';

0 commit comments

Comments
 (0)