Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Service Map Data API at Runtime #54027

Merged
merged 25 commits into from
Jan 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7c96d36
[APM] Runtime service maps
dgieselaar Dec 27, 2019
19f8e47
Make nodes interactive
dgieselaar Dec 29, 2019
61e5e99
Don't use smaller range query on initial request
dgieselaar Jan 2, 2020
4acf9b1
Address feedback from Ron
dgieselaar Jan 2, 2020
f7b3cbe
Get all services separately
dgieselaar Jan 2, 2020
ea9d314
Get single service as well
dgieselaar Jan 2, 2020
f99addc
Query both transactions/spans for initial request
dgieselaar Jan 3, 2020
d27c531
Optimize 'top' query for service maps
dgieselaar Jan 4, 2020
14970d7
Use agent.name from scripted metric
dgieselaar Jan 4, 2020
5f1df25
adds basic loading overlay
ogupte Jan 8, 2020
8ca834a
filter out service map node self reference edges from being rendered
ogupte Jan 8, 2020
08398d1
Make service map initial load time range configurable with
ogupte Jan 8, 2020
56d60c4
ensure destination.address is not missing in the composite agg when
ogupte Jan 8, 2020
141c4d2
wip: added incremental data fetch & progress bar
ogupte Jan 8, 2020
7a0026a
implement progressive loading design while blocking service map inter…
ogupte Jan 9, 2020
66fd5b9
adds filter that destination.address exists before fetching sample tr…
ogupte Jan 9, 2020
1ccdb58
reduce pairs of connections to 1 bi-directional connection with arrow…
ogupte Jan 9, 2020
fb8bfbd
Optimize query; add update button
dgieselaar Jan 9, 2020
35184fe
Allow user interaction after 5s, auto update in that time, otherwise
ogupte Jan 10, 2020
99f32e2
Correctly reduce nodes/connections
dgieselaar Jan 10, 2020
55cfdf2
- remove non-interactive state while loading
ogupte Jan 10, 2020
5301125
- readability improvements to the ServiceMap component
ogupte Jan 11, 2020
b02b28c
addresses feedback for changes to the Cytoscape component
ogupte Jan 11, 2020
3ccffad
Add span.type/span.subtype do external nodes
dgieselaar Jan 12, 2020
02cae50
PR feedback
ogupte Jan 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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/legacy/plugins/apm/common/elasticsearch_fieldnames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const HTTP_REQUEST_METHOD = 'http.request.method';
export const USER_ID = 'user.id';
export const USER_AGENT_NAME = 'user_agent.name';

export const DESTINATION_ADDRESS = 'destination.address';

export const OBSERVER_VERSION_MAJOR = 'observer.version_major';
export const OBSERVER_LISTENING = 'observer.listening';
export const PROCESSOR_EVENT = 'processor.event';
Expand Down
23 changes: 23 additions & 0 deletions x-pack/legacy/plugins/apm/common/service_map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.
*/

export interface ServiceConnectionNode {
'service.name': string;
'service.environment': string | null;
'agent.name': string;
}
export interface ExternalConnectionNode {
'destination.address': string;
'span.type': string;
'span.subtype': string;
}

export type ConnectionNode = ServiceConnectionNode | ExternalConnectionNode;

export interface Connection {
source: ConnectionNode;
destination: ConnectionNode;
}
3 changes: 2 additions & 1 deletion x-pack/legacy/plugins/apm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export const apm: LegacyPluginInitializer = kibana => {
autocreateApmIndexPattern: Joi.boolean().default(true),

// service map
serviceMapEnabled: Joi.boolean().default(false)
serviceMapEnabled: Joi.boolean().default(false),
serviceMapInitialTimeRange: Joi.number().default(60 * 1000 * 60) // last 1 hour
}).default();
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function Cytoscape({
cy.on('data', event => {
// Add the "primary" class to the node if its id matches the serviceName.
if (cy.nodes().length > 0 && serviceName) {
cy.nodes().removeClass('primary');
cy.getElementById(serviceName).addClass('primary');
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 theme from '@elastic/eui/dist/eui_theme_light.json';
import React from 'react';
import { EuiProgress, EuiText, EuiSpacer } from '@elastic/eui';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';

const Container = styled.div`
position: relative;
`;

const Overlay = styled.div`
position: absolute;
top: 0;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding: ${theme.gutterTypes.gutterMedium};
`;

const ProgressBarContainer = styled.div`
width: 50%;
max-width: 600px;
`;

interface Props {
children: React.ReactNode;
isLoading: boolean;
percentageLoaded: number;
}

export const LoadingOverlay = ({
children,
isLoading,
percentageLoaded
}: Props) => (
<Container>
{isLoading && (
<Overlay>
<ProgressBarContainer>
<EuiProgress
value={percentageLoaded}
max={100}
color="primary"
size="m"
/>
</ProgressBarContainer>
<EuiSpacer size="s" />
<EuiText size="s" textAlign="center">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, have we tried removing the text and having just the loader? just a thought.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been going off the design issue #54246 which includes the text.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ogupte @dgieselaar Not sure where we stand on the load optimizations yet, but if we go more than 5 seconds on load, I think it's still worth showing a message to the user was is happening behind the scenes, why the map is locked. The progress bar probably isn't enough?

{i18n.translate('xpack.apm.loadingServiceMap', {
defaultMessage:
'Loading service map... This might take a short while.'
})}
</EuiText>
</Overlay>
)}
{children}
</Container>
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,13 @@ import theme from '@elastic/eui/dist/eui_theme_light.json';
import { icons, defaultIcon } from './icons';

const layout = {
animate: true,
animationEasing: theme.euiAnimSlightBounce as cytoscape.Css.TransitionTimingFunction,
animationDuration: parseInt(theme.euiAnimSpeedFast, 10),
name: 'dagre',
nodeDimensionsIncludeLabels: true,
rankDir: 'LR',
spacingFactor: 2
rankDir: 'LR'
};

function isDatabaseOrExternal(agentName: string) {
return agentName === 'database' || agentName === 'external';
return !agentName;
}

const style: cytoscape.Stylesheet[] = [
Expand Down Expand Up @@ -47,7 +43,7 @@ const style: cytoscape.Stylesheet[] = [
'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif',
'font-size': theme.euiFontSizeXS,
height: theme.avatarSizing.l.size,
label: 'data(id)',
label: 'data(label)',
'min-zoomed-font-size': theme.euiSizeL,
'overlay-opacity': 0,
shape: (el: cytoscape.NodeSingular) =>
Expand Down Expand Up @@ -76,7 +72,18 @@ const style: cytoscape.Stylesheet[] = [
//
// @ts-ignore
'target-distance-from-node': theme.paddingSizes.xs,
width: 2
width: 1,
'source-arrow-shape': 'none'
}
},
{
selector: 'edge[bidirectional]',
style: {
'source-arrow-shape': 'triangle',
'target-arrow-shape': 'triangle',
// @ts-ignore
'source-distance-from-node': theme.paddingSizes.xs,
'target-distance-from-node': theme.paddingSizes.xs
}
}
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* 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 { ValuesType } from 'utility-types';
import { sortBy, isEqual } from 'lodash';
import { Connection, ConnectionNode } from '../../../../common/service_map';
import { ServiceMapAPIResponse } from '../../../../server/lib/service_map/get_service_map';
import { getAPMHref } from '../../shared/Links/apm/APMLink';

function getConnectionNodeId(node: ConnectionNode): string {
if ('destination.address' in node) {
// use a prefix to distinguish exernal destination ids from services
return `>${node['destination.address']}`;
}
return node['service.name'];
}

function getConnectionId(connection: Connection) {
return `${getConnectionNodeId(connection.source)}~${getConnectionNodeId(
connection.destination
)}`;
}
export function getCytoscapeElements(
responses: ServiceMapAPIResponse[],
search: string
) {
const discoveredServices = responses.flatMap(
response => response.discoveredServices
);

const serviceNodes = responses
.flatMap(response => response.services)
.map(service => ({
...service,
id: service['service.name']
}));

// maps destination.address to service.name if possible
function getConnectionNode(node: ConnectionNode) {
let mappedNode: ConnectionNode | undefined;

if ('destination.address' in node) {
mappedNode = discoveredServices.find(map => isEqual(map.from, node))?.to;
}

if (!mappedNode) {
mappedNode = node;
}

return {
...mappedNode,
id: getConnectionNodeId(mappedNode)
};
}

// build connections with mapped nodes
const connections = responses
.flatMap(response => response.connections)
.map(connection => {
const source = getConnectionNode(connection.source);
const destination = getConnectionNode(connection.destination);

return {
source,
destination,
id: getConnectionId({ source, destination })
};
})
.filter(connection => connection.source.id !== connection.destination.id);

const nodes = connections
.flatMap(connection => [connection.source, connection.destination])
.concat(serviceNodes);

type ConnectionWithId = ValuesType<typeof connections>;
type ConnectionNodeWithId = ValuesType<typeof nodes>;

const connectionsById = connections.reduce((connectionMap, connection) => {
return {
...connectionMap,
[connection.id]: connection
};
}, {} as Record<string, ConnectionWithId>);

const nodesById = nodes.reduce((nodeMap, node) => {
return {
...nodeMap,
[node.id]: node
};
}, {} as Record<string, ConnectionNodeWithId>);

const cyNodes = (Object.values(nodesById) as ConnectionNodeWithId[]).map(
node => {
let data = {};

if ('service.name' in node) {
data = {
href: getAPMHref(
`/services/${node['service.name']}/service-map`,
search
),
agentName: node['agent.name'] || node['agent.name']
};
}

return {
group: 'nodes' as const,
data: {
id: node.id,
label:
'service.name' in node
? node['service.name']
: node['destination.address'],
...data
}
};
}
);

// instead of adding connections in two directions,
// we add a `bidirectional` flag to use in styling
const dedupedConnections = (sortBy(
Object.values(connectionsById),
// make sure that order is stable
'id'
) as ConnectionWithId[]).reduce<
Array<ConnectionWithId & { bidirectional?: boolean }>
>((prev, connection) => {
const reversedConnection = prev.find(
c =>
c.destination.id === connection.source.id &&
c.source.id === connection.destination.id
);

if (reversedConnection) {
reversedConnection.bidirectional = true;
return prev;
}

return prev.concat(connection);
}, []);

const cyEdges = dedupedConnections.map(connection => {
return {
group: 'edges' as const,
data: {
id: connection.id,
source: connection.source.id,
target: connection.destination.id,
bidirectional: connection.bidirectional ? true : undefined
}
};
}, []);

return [...cyNodes, ...cyEdges];
}
Loading