Skip to content

Commit

Permalink
[APM] use span.destination.service.resource (elastic#60908)
Browse files Browse the repository at this point in the history
* [APM] use span.destination.service.resource

Closes elastic#60405.

* update snapshots

Co-authored-by: Nathan L Smith <[email protected]>
  • Loading branch information
dgieselaar and smith authored Mar 23, 2020
1 parent f7a3049 commit d5c13c0
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import theme from '@elastic/eui/dist/eui_theme_light.json';
import cytoscape from 'cytoscape';
import { CSSProperties } from 'react';
import {
DESTINATION_ADDRESS,
SERVICE_NAME
SERVICE_NAME,
SPAN_DESTINATION_SERVICE_RESOURCE
} from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames';
import { defaultIcon, iconForNode } from './icons';

Expand Down Expand Up @@ -59,7 +59,9 @@ const style: cytoscape.Stylesheet[] = [
'ghost-opacity': 0.15,
height: nodeHeight,
label: (el: cytoscape.NodeSingular) =>
isService(el) ? el.data(SERVICE_NAME) : el.data(DESTINATION_ADDRESS),
isService(el)
? el.data(SERVICE_NAME)
: el.data(SPAN_DESTINATION_SERVICE_RESOURCE),
'min-zoomed-font-size': theme.euiSizeL,
'overlay-opacity': 0,
shape: (el: cytoscape.NodeSingular) =>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export const SPAN_SELF_TIME_SUM = 'span.self_time.sum.us';
export const SPAN_ACTION = 'span.action';
export const SPAN_NAME = 'span.name';
export const SPAN_ID = 'span.id';
export const SPAN_DESTINATION_SERVICE_RESOURCE =
'span.destination.service.resource';

// Parent ID for a transaction or span
export const PARENT_ID = 'parent.id';
Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/apm/common/service_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import { i18n } from '@kbn/i18n';
import { ILicense } from '../../licensing/public';
import {
AGENT_NAME,
DESTINATION_ADDRESS,
SERVICE_ENVIRONMENT,
SERVICE_FRAMEWORK_NAME,
SERVICE_NAME,
SPAN_SUBTYPE,
SPAN_TYPE
SPAN_TYPE,
SPAN_DESTINATION_SERVICE_RESOURCE
} from './elasticsearch_fieldnames';

export interface ServiceConnectionNode {
Expand All @@ -23,7 +23,7 @@ export interface ServiceConnectionNode {
[AGENT_NAME]: string;
}
export interface ExternalConnectionNode {
[DESTINATION_ADDRESS]: string;
[SPAN_DESTINATION_SERVICE_RESOURCE]: string;
[SPAN_TYPE]: string;
[SPAN_SUBTYPE]: string;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { ServiceMapResponse } from './';
import {
SPAN_DESTINATION_SERVICE_RESOURCE,
SERVICE_NAME,
SERVICE_ENVIRONMENT,
SERVICE_FRAMEWORK_NAME,
AGENT_NAME,
SPAN_TYPE,
SPAN_SUBTYPE
} from '../../../../common/elasticsearch_fieldnames';
import { dedupeConnections } from './';

const nodejsService = {
[SERVICE_NAME]: 'opbeans-node',
[SERVICE_ENVIRONMENT]: 'production',
[SERVICE_FRAMEWORK_NAME]: null,
[AGENT_NAME]: 'nodejs'
};

const nodejsExternal = {
[SPAN_DESTINATION_SERVICE_RESOURCE]: 'opbeans-node',
[SPAN_TYPE]: 'external',
[SPAN_SUBTYPE]: 'aa'
};

const javaService = {
[SERVICE_NAME]: 'opbeans-java',
[SERVICE_ENVIRONMENT]: 'production',
[SERVICE_FRAMEWORK_NAME]: null,
[AGENT_NAME]: 'java'
};

describe('dedupeConnections', () => {
it('maps external destinations to internal services', () => {
const response: ServiceMapResponse = {
services: [nodejsService, javaService],
discoveredServices: [
{
from: nodejsExternal,
to: nodejsService
}
],
connections: [
{
source: javaService,
destination: nodejsExternal
}
]
};

const { elements } = dedupeConnections(response);

const connection = elements.find(
element => 'source' in element.data && 'target' in element.data
);

// @ts-ignore
expect(connection?.data.target).toBe('opbeans-node');

expect(
elements.find(element => element.data.id === '>opbeans-node')
).toBeUndefined();
});

it('collapses external destinations based on span.destination.resource.name', () => {
const response: ServiceMapResponse = {
services: [nodejsService, javaService],
discoveredServices: [
{
from: nodejsExternal,
to: nodejsService
}
],
connections: [
{
source: javaService,
destination: nodejsExternal
},
{
source: javaService,
destination: {
...nodejsExternal,
[SPAN_TYPE]: 'foo'
}
}
]
};

const { elements } = dedupeConnections(response);

const connections = elements.filter(element => 'source' in element.data);

expect(connections.length).toBe(1);

const nodes = elements.filter(element => !('source' in element.data));

expect(nodes.length).toBe(2);
});

it('picks the first span.type/subtype in an alphabetically sorted list', () => {
const response: ServiceMapResponse = {
services: [javaService],
discoveredServices: [],
connections: [
{
source: javaService,
destination: nodejsExternal
},
{
source: javaService,
destination: {
...nodejsExternal,
[SPAN_TYPE]: 'foo'
}
},
{
source: javaService,
destination: {
...nodejsExternal,
[SPAN_SUBTYPE]: 'bb'
}
}
]
};

const { elements } = dedupeConnections(response);

const nodes = elements.filter(element => !('source' in element.data));

const nodejsNode = nodes.find(node => node.data.id === '>opbeans-node');

// @ts-ignore
expect(nodejsNode?.data[SPAN_TYPE]).toBe('external');
// @ts-ignore
expect(nodejsNode?.data[SPAN_SUBTYPE]).toBe('aa');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@
import { isEqual, sortBy } from 'lodash';
import { ValuesType } from 'utility-types';
import {
DESTINATION_ADDRESS,
SERVICE_NAME
} from '../../../common/elasticsearch_fieldnames';
SERVICE_NAME,
SPAN_DESTINATION_SERVICE_RESOURCE,
SPAN_TYPE,
SPAN_SUBTYPE
} from '../../../../common/elasticsearch_fieldnames';
import {
Connection,
ConnectionNode,
ExternalConnectionNode,
ServiceConnectionNode
} from '../../../common/service_map';
import { ConnectionsResponse, ServicesResponse } from './get_service_map';
ServiceConnectionNode,
ExternalConnectionNode
} from '../../../../common/service_map';
import { ConnectionsResponse, ServicesResponse } from '../get_service_map';

function getConnectionNodeId(node: ConnectionNode): string {
if (DESTINATION_ADDRESS in node) {
if ('span.destination.service.resource' in node) {
// use a prefix to distinguish exernal destination ids from services
return `>${(node as ExternalConnectionNode)[DESTINATION_ADDRESS]}`;
return `>${node[SPAN_DESTINATION_SERVICE_RESOURCE]}`;
}
return (node as ServiceConnectionNode)[SERVICE_NAME];
return node[SERVICE_NAME];
}

function getConnectionId(connection: Connection) {
Expand All @@ -31,32 +33,86 @@ function getConnectionId(connection: Connection) {
)}`;
}

type ServiceMapResponse = ConnectionsResponse & { services: ServicesResponse };
export type ServiceMapResponse = ConnectionsResponse & {
services: ServicesResponse;
};

export function dedupeConnections(response: ServiceMapResponse) {
const { discoveredServices, services, connections } = response;

const serviceNodes = services.map(service => ({
...service,
id: service[SERVICE_NAME]
}));
const allNodes = connections
.flatMap(connection => [connection.source, connection.destination])
.map(node => ({ ...node, id: getConnectionNodeId(node) }))
.concat(
services.map(service => ({
...service,
id: service[SERVICE_NAME]
}))
);

// maps destination.address to service.name if possible
function getConnectionNode(node: ConnectionNode) {
let mappedNode: ConnectionNode | undefined;
const serviceNodes = allNodes.filter(node => SERVICE_NAME in node) as Array<
ServiceConnectionNode & {
id: string;
}
>;

if (DESTINATION_ADDRESS in node) {
mappedNode = discoveredServices.find(map => isEqual(map.from, node))?.to;
const externalNodes = allNodes.filter(
node => SPAN_DESTINATION_SERVICE_RESOURCE in node
) as Array<
ExternalConnectionNode & {
id: string;
}
>;

// 1. maps external nodes to internal services
// 2. collapses external nodes into one node based on span.destination.service.resource
// 3. picks the first available span.type/span.subtype in an alphabetically sorted list
const nodeMap = allNodes.reduce((map, node) => {
if (map[node.id]) {
return map;
}

if (!mappedNode) {
mappedNode = node;
const service =
discoveredServices.find(({ from }) => {
if ('span.destination.service.resource' in node) {
return (
node[SPAN_DESTINATION_SERVICE_RESOURCE] ===
from[SPAN_DESTINATION_SERVICE_RESOURCE]
);
}
return false;
})?.to ?? serviceNodes.find(serviceNode => serviceNode.id === node.id);

if (service) {
return {
...map,
[node.id]: {
id: service[SERVICE_NAME],
...service
}
};
}

const allMatchedExternalNodes = externalNodes.filter(n => n.id === node.id);

const firstMatchedNode = allMatchedExternalNodes[0];

return {
...mappedNode,
id: getConnectionNodeId(mappedNode)
...map,
[node.id]: {
...firstMatchedNode,
label: firstMatchedNode[SPAN_DESTINATION_SERVICE_RESOURCE],
[SPAN_TYPE]: allMatchedExternalNodes.map(n => n[SPAN_TYPE]).sort()[0],
[SPAN_SUBTYPE]: allMatchedExternalNodes
.map(n => n[SPAN_SUBTYPE])
.sort()[0]
}
};
}, {} as Record<string, ConnectionNode & { id: string }>);

// maps destination.address to service.name if possible
function getConnectionNode(node: ConnectionNode) {
return nodeMap[getConnectionNodeId(node)];
}

// build connections with mapped nodes
Expand Down
Loading

0 comments on commit d5c13c0

Please sign in to comment.