+ {showControls &&
}
- {details.tables.map(function(table) {
- const key = _.snakeCase(table.title);
- return
;
+ {showSummary &&
+
Status
+ {details.metrics &&
}
+ {details.metadata &&
}
+
}
+
+ {details.children && details.children.map(children => {
+ return (
+
+
+
+ );
})}
diff --git a/client/app/scripts/components/node-details/node-details-health-item.js b/client/app/scripts/components/node-details/node-details-health-item.js
new file mode 100644
index 0000000000..7ddeba23fe
--- /dev/null
+++ b/client/app/scripts/components/node-details/node-details-health-item.js
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import Sparkline from '../sparkline';
+import { formatMetric } from '../../utils/string-utils';
+
+export default (props) => {
+ return (
+
+
{formatMetric(props.item.value, props.item)}
+
+
+
+
{props.item.label}
+
+ );
+};
diff --git a/client/app/scripts/components/node-details/node-details-health-overflow-item.js b/client/app/scripts/components/node-details/node-details-health-overflow-item.js
new file mode 100644
index 0000000000..1e59d6cfe0
--- /dev/null
+++ b/client/app/scripts/components/node-details/node-details-health-overflow-item.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import { formatMetric } from '../../utils/string-utils';
+
+export default class NodeDetailsHealthOverflowItem extends React.Component {
+ render() {
+ return (
+
+
{formatMetric(this.props.item.value, this.props.item)}
+
{this.props.item.label}
+
+ );
+ }
+}
diff --git a/client/app/scripts/components/node-details/node-details-health-overflow.js b/client/app/scripts/components/node-details/node-details-health-overflow.js
new file mode 100644
index 0000000000..260786fdc1
--- /dev/null
+++ b/client/app/scripts/components/node-details/node-details-health-overflow.js
@@ -0,0 +1,18 @@
+import React from 'react';
+
+import NodeDetailsHealthOverflowItem from './node-details-health-overflow-item';
+
+export default class NodeDetailsHealthOverflow extends React.Component {
+ render() {
+ const items = this.props.items.slice(0, 4);
+
+ return (
+
+ {items.map(item =>
)}
+
+ Show more
+
+
+ );
+ }
+}
diff --git a/client/app/scripts/components/node-details/node-details-health.js b/client/app/scripts/components/node-details/node-details-health.js
new file mode 100644
index 0000000000..3f4ecc444c
--- /dev/null
+++ b/client/app/scripts/components/node-details/node-details-health.js
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import NodeDetailsHealthOverflow from './node-details-health-overflow';
+import NodeDetailsHealthItem from './node-details-health-item';
+
+export default class NodeDetailsHealth extends React.Component {
+
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ expanded: false
+ };
+ this.handleClickMore = this.handleClickMore.bind(this);
+ }
+
+ handleClickMore(ev) {
+ ev.preventDefault();
+ const expanded = !this.state.expanded;
+ this.setState({expanded});
+ }
+
+ render() {
+ const metrics = this.props.metrics || [];
+ const primeCutoff = metrics.length > 3 && !this.state.expanded ? 2 : metrics.length;
+ const primeMetrics = metrics.slice(0, primeCutoff);
+ const overflowMetrics = metrics.slice(primeCutoff);
+ const showOverflow = overflowMetrics.length > 0 && !this.state.expanded;
+ const showLess = this.state.expanded;
+ const flexWrap = showOverflow || !this.state.expanded ? 'nowrap' : 'wrap';
+ const justifyContent = showOverflow || !this.state.expanded ? 'space-around' : 'flex-start';
+
+ return (
+
+ {primeMetrics.map(item => {
+ return
;
+ })}
+ {showOverflow &&
}
+ {showLess &&
show less
}
+
+ );
+ }
+}
diff --git a/client/app/scripts/components/node-details/node-details-info.js b/client/app/scripts/components/node-details/node-details-info.js
new file mode 100644
index 0000000000..1f9b2e27d1
--- /dev/null
+++ b/client/app/scripts/components/node-details/node-details-info.js
@@ -0,0 +1,24 @@
+import React from 'react';
+
+export default class NodeDetailsInfo extends React.Component {
+ render() {
+ return (
+
+ {this.props.metadata && this.props.metadata.map(field => {
+ return (
+
+ );
+ })}
+
+ );
+ }
+}
diff --git a/client/app/scripts/components/node-details/node-details-relatives-link.js b/client/app/scripts/components/node-details/node-details-relatives-link.js
new file mode 100644
index 0000000000..315f7d0268
--- /dev/null
+++ b/client/app/scripts/components/node-details/node-details-relatives-link.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import { clickRelative } from '../../actions/app-actions';
+
+export default class NodeDetailsRelativesLink extends React.Component {
+
+ constructor(props, context) {
+ super(props, context);
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ handleClick(ev) {
+ ev.preventDefault();
+ clickRelative(this.props.id, this.props.topologyId, this.props.label,
+ ReactDOM.findDOMNode(this).getBoundingClientRect());
+ }
+
+ render() {
+ const title = `View in ${this.props.topologyId}: ${this.props.label}`;
+ return (
+
+ {this.props.label}
+
+ );
+ }
+}
diff --git a/client/app/scripts/components/node-details/node-details-relatives.js b/client/app/scripts/components/node-details/node-details-relatives.js
new file mode 100644
index 0000000000..b0813bc27e
--- /dev/null
+++ b/client/app/scripts/components/node-details/node-details-relatives.js
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import NodeDetailsRelativesLink from './node-details-relatives-link';
+
+export default class NodeDetailsRelatives extends React.Component {
+
+ constructor(props, context) {
+ super(props, context);
+ this.DEFAULT_LIMIT = 5;
+ this.state = {
+ limit: this.DEFAULT_LIMIT
+ };
+ this.handleLimitClick = this.handleLimitClick.bind(this);
+ }
+
+ handleLimitClick(ev) {
+ ev.preventDefault();
+ const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT;
+ this.setState({limit: limit});
+ }
+
+ render() {
+ let relatives = this.props.relatives;
+ const limited = this.state.limit > 0 && relatives.length > this.state.limit;
+ const showLimitAction = limited || (this.state.limit === 0 && relatives.length > this.DEFAULT_LIMIT);
+ const limitActionText = limited ? 'Show more' : 'Show less';
+ if (limited) {
+ relatives = relatives.slice(0, this.state.limit);
+ }
+
+ return (
+
+ {relatives.map(relative => {
+ return ;
+ })}
+ {showLimitAction && {limitActionText}}
+
+ );
+ }
+}
diff --git a/client/app/scripts/components/node-details/node-details-table-node-link.js b/client/app/scripts/components/node-details/node-details-table-node-link.js
new file mode 100644
index 0000000000..3e2c1f5549
--- /dev/null
+++ b/client/app/scripts/components/node-details/node-details-table-node-link.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import { clickRelative } from '../../actions/app-actions';
+
+export default class NodeDetailsTableNodeLink extends React.Component {
+
+ constructor(props, context) {
+ super(props, context);
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ handleClick(ev) {
+ ev.preventDefault();
+ clickRelative(this.props.id, this.props.topologyId, this.props.label,
+ ReactDOM.findDOMNode(this).getBoundingClientRect());
+ }
+
+ render() {
+ const titleLines = [`${this.props.label} (${this.props.topologyId})`];
+ this.props.metadata.forEach(data => {
+ titleLines.push(`${data.label}: ${data.value}`);
+ });
+ const title = titleLines.join('\n');
+
+ if (this.props.linkable) {
+ return (
+
+ {this.props.label}
+
+ );
+ }
+ return (
+
+ {this.props.label}
+
+ );
+ }
+}
diff --git a/client/app/scripts/components/node-details/node-details-table-row-number.js b/client/app/scripts/components/node-details/node-details-table-row-number.js
deleted file mode 100644
index d41eaa10ae..0000000000
--- a/client/app/scripts/components/node-details/node-details-table-row-number.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-
-export default class NodeDetailsTableRowNumber extends React.Component {
- render() {
- const row = this.props.row;
- return (
-
-
{row.value_major}
-
{row.value_minor}
-
- );
- }
-}
diff --git a/client/app/scripts/components/node-details/node-details-table-row-sparkline.js b/client/app/scripts/components/node-details/node-details-table-row-sparkline.js
deleted file mode 100644
index f40b77d0f9..0000000000
--- a/client/app/scripts/components/node-details/node-details-table-row-sparkline.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-
-import Sparkline from '../sparkline';
-
-export default class NodeDetailsTableRowSparkline extends React.Component {
- render() {
- const row = this.props.row;
- return (
-
-
{row.value_major}
-
{row.value_minor}
-
- );
- }
-}
diff --git a/client/app/scripts/components/node-details/node-details-table-row-value.js b/client/app/scripts/components/node-details/node-details-table-row-value.js
deleted file mode 100644
index 1dc765eb5e..0000000000
--- a/client/app/scripts/components/node-details/node-details-table-row-value.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-
-export default class NodeDetailsTableRowValue extends React.Component {
- render() {
- const row = this.props.row;
- return (
-
-
- {row.value_major}
-
- {row.value_minor &&
- {row.value_minor}
-
}
-
- );
- }
-}
diff --git a/client/app/scripts/components/node-details/node-details-table.js b/client/app/scripts/components/node-details/node-details-table.js
index b95281b264..26b290c996 100644
--- a/client/app/scripts/components/node-details/node-details-table.js
+++ b/client/app/scripts/components/node-details/node-details-table.js
@@ -1,33 +1,156 @@
+import _ from 'lodash';
import React from 'react';
-import NodeDetailsTableRowValue from './node-details-table-row-value';
-import NodeDetailsTableRowNumber from './node-details-table-row-number';
-import NodeDetailsTableRowSparkline from './node-details-table-row-sparkline';
+import NodeDetailsTableNodeLink from './node-details-table-node-link';
+import { formatMetric } from '../../utils/string-utils';
export default class NodeDetailsTable extends React.Component {
+
+ constructor(props, context) {
+ super(props, context);
+ this.DEFAULT_LIMIT = 5;
+ this.state = {
+ limit: this.DEFAULT_LIMIT,
+ sortedDesc: true,
+ sortBy: null
+ };
+ this.handleLimitClick = this.handleLimitClick.bind(this);
+ this.getValueForSortBy = this.getValueForSortBy.bind(this);
+ }
+
+ handleHeaderClick(ev, headerId) {
+ ev.preventDefault();
+ const sortedDesc = headerId === this.state.sortBy ? !this.state.sortedDesc : this.state.sortedDesc;
+ const sortBy = headerId;
+ this.setState({sortBy, sortedDesc});
+ }
+
+ handleLimitClick(ev) {
+ ev.preventDefault();
+ const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT;
+ this.setState({limit});
+ }
+
+ getDefaultSortBy() {
+ // first metric
+ return _.get(this.props.nodes, [0, 'metrics', 0, 'id']);
+ }
+
+ getMetaDataSorters() {
+ // returns an array of sorters that will take a node
+ return _.get(this.props.nodes, [0, 'metadata'], []).map((field, index) => {
+ return node => node.metadata[index] ? node.metadata[index].value : null;
+ });
+ }
+
+ getValueForSortBy(node) {
+ // return the node's value based on the sortBy field
+ const sortBy = this.state.sortBy || this.getDefaultSortBy();
+ if (sortBy !== null) {
+ const field = _.union(node.metrics, node.metadata).find(f => f.id === sortBy);
+ if (field) {
+ return field.value;
+ }
+ }
+ return 0;
+ }
+
+ getValuesForNode(node) {
+ const values = {};
+ ['metrics', 'metadata'].forEach(collection => {
+ if (node[collection]) {
+ node[collection].forEach(field => {
+ values[field.id] = field;
+ });
+ }
+ });
+ return values;
+ }
+
+ renderHeaders() {
+ if (this.props.nodes && this.props.nodes.length > 0) {
+ let headers = [{id: 'label', label: this.props.label}];
+ // gather header labels from metrics and metadata
+ const firstValues = this.getValuesForNode(this.props.nodes[0]);
+ headers = headers.concat(this.props.columns.map(column => ({id: column, label: firstValues[column].label})));
+ const defaultSortBy = this.getDefaultSortBy();
+
+ return (
+
+ {headers.map(header => {
+ const headerClasses = ['node-details-table-header', 'truncate'];
+ const onHeaderClick = ev => {
+ this.handleHeaderClick(ev, header.id);
+ };
+ // sort by first metric by default
+ const isSorted = this.state.sortBy !== null ? header.id === this.state.sortBy : header.id === defaultSortBy;
+ const isSortedDesc = isSorted && this.state.sortedDesc;
+ const isSortedAsc = isSorted && !isSortedDesc;
+ if (isSorted) {
+ headerClasses.push('node-details-table-header-sorted');
+ }
+ return (
+
+ {isSortedAsc && }
+ {isSortedDesc && }
+ {header.label}
+ |
+ );
+ })}
+
+ );
+ }
+ return '';
+ }
+
+ renderValues(node) {
+ const fields = this.getValuesForNode(node);
+ return this.props.columns.map(col => {
+ const field = fields[col];
+ if (field) {
+ return (
+
+ {formatMetric(field.value, field)}
+ |
+ );
+ }
+ });
+ }
+
render() {
+ const headers = this.renderHeaders();
+ let nodes = _.sortByAll(this.props.nodes, this.getValueForSortBy, 'label', this.getMetaDataSorters());
+ const limited = nodes && this.state.limit > 0 && nodes.length > this.state.limit;
+ const showLimitAction = nodes && (limited || (this.state.limit === 0 && nodes.length > this.DEFAULT_LIMIT));
+ const limitActionText = limited ? 'Show more' : 'Show less';
+ if (this.state.sortedDesc) {
+ nodes.reverse();
+ }
+ if (nodes && limited) {
+ nodes = nodes.slice(0, this.state.limit);
+ }
+
return (
-
-
- {this.props.title}
-
-
- {this.props.rows.map(function(row) {
- let valueComponent;
- if (row.value_type === 'numeric') {
- valueComponent =
;
- } else if (row.value_type === 'sparkline') {
- valueComponent =
;
- } else {
- valueComponent =
;
- }
- return (
-
-
{row.key}
- {valueComponent}
-
- );
- })}
+
+
+
+ {headers}
+
+
+ {nodes && nodes.map(node => {
+ const values = this.renderValues(node);
+ return (
+
+
+
+ |
+ {values}
+
+ );
+ })}
+
+
+ {showLimitAction &&
{limitActionText}
}
);
}
diff --git a/client/app/scripts/components/sparkline.js b/client/app/scripts/components/sparkline.js
index f72fbe7481..6831d5654e 100644
--- a/client/app/scripts/components/sparkline.js
+++ b/client/app/scripts/components/sparkline.js
@@ -116,7 +116,7 @@ export default class Sparkline extends React.Component {
}
Sparkline.defaultProps = {
- width: 100,
+ width: 80,
height: 16,
strokeColor: '#7d7da8',
strokeWidth: '0.5px',
diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js
index a4019dea4b..0cdf0b39a7 100644
--- a/client/app/scripts/constants/action-types.js
+++ b/client/app/scripts/constants/action-types.js
@@ -3,9 +3,12 @@ import _ from 'lodash';
const ACTION_TYPES = [
'CHANGE_TOPOLOGY_OPTION',
'CLEAR_CONTROL_ERROR',
+ 'CLICK_BACKGROUND',
'CLICK_CLOSE_DETAILS',
'CLICK_CLOSE_TERMINAL',
'CLICK_NODE',
+ 'CLICK_RELATIVE',
+ 'CLICK_SHOW_TOPOLOGY_FOR_NODE',
'CLICK_TERMINAL',
'CLICK_TOPOLOGY',
'CLOSE_WEBSOCKET',
@@ -23,6 +26,7 @@ const ACTION_TYPES = [
'RECEIVE_NODE_DETAILS',
'RECEIVE_NODES',
'RECEIVE_NODES_DELTA',
+ 'RECEIVE_NOT_FOUND',
'RECEIVE_TOPOLOGIES',
'RECEIVE_API_DETAILS',
'RECEIVE_ERROR',
diff --git a/client/app/scripts/stores/__tests__/app-store-test.js b/client/app/scripts/stores/__tests__/app-store-test.js
index 7604266640..ca31a2fdcc 100644
--- a/client/app/scripts/stores/__tests__/app-store-test.js
+++ b/client/app/scripts/stores/__tests__/app-store-test.js
@@ -51,6 +51,22 @@ describe('AppStore', function() {
nodeId: 'n1'
};
+ const ClickNode2Action = {
+ type: ActionTypes.CLICK_NODE,
+ nodeId: 'n2'
+ };
+
+ const ClickRelativeAction = {
+ type: ActionTypes.CLICK_RELATIVE,
+ nodeId: 'rel1'
+ };
+
+ const ClickShowTopologyForNodeAction = {
+ type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE,
+ topologyId: 'topo2',
+ nodeId: 'rel1'
+ };
+
const ClickSubTopologyAction = {
type: ActionTypes.CLICK_TOPOLOGY,
topologyId: 'topo1-grouped'
@@ -335,4 +351,77 @@ describe('AppStore', function() {
registeredCallback(ClickTopologyAction);
expect(AppStore.isTopologyEmpty()).toBeFalsy();
});
+
+ // selection of relatives
+
+ it('keeps relatives as a stack', function() {
+ registeredCallback(ClickNodeAction);
+ expect(AppStore.getSelectedNodeId()).toBe('n1');
+ expect(AppStore.getNodeDetails().size).toEqual(1);
+ expect(AppStore.getNodeDetails().has('n1')).toBeTruthy();
+ expect(AppStore.getNodeDetails().keySeq().last()).toEqual('n1');
+
+ registeredCallback(ClickRelativeAction);
+ // stack relative, first node stays main node
+ expect(AppStore.getSelectedNodeId()).toBe('n1');
+ expect(AppStore.getNodeDetails().keySeq().last()).toEqual('rel1');
+ expect(AppStore.getNodeDetails().size).toEqual(2);
+ expect(AppStore.getNodeDetails().has('rel1')).toBeTruthy();
+
+ // click on first node should clear the stack
+ registeredCallback(ClickNodeAction);
+ expect(AppStore.getSelectedNodeId()).toBe('n1');
+ expect(AppStore.getNodeDetails().keySeq().last()).toEqual('n1');
+ expect(AppStore.getNodeDetails().size).toEqual(1);
+ expect(AppStore.getNodeDetails().has('rel1')).toBeFalsy();
+ });
+
+ it('keeps clears stack when sibling is clicked', function() {
+ registeredCallback(ClickNodeAction);
+ expect(AppStore.getSelectedNodeId()).toBe('n1');
+ expect(AppStore.getNodeDetails().size).toEqual(1);
+ expect(AppStore.getNodeDetails().has('n1')).toBeTruthy();
+ expect(AppStore.getNodeDetails().keySeq().last()).toEqual('n1');
+
+ registeredCallback(ClickRelativeAction);
+ // stack relative, first node stays main node
+ expect(AppStore.getSelectedNodeId()).toBe('n1');
+ expect(AppStore.getNodeDetails().keySeq().last()).toEqual('rel1');
+ expect(AppStore.getNodeDetails().size).toEqual(2);
+ expect(AppStore.getNodeDetails().has('rel1')).toBeTruthy();
+
+ // click on sibling node should clear the stack
+ registeredCallback(ClickNode2Action);
+ expect(AppStore.getSelectedNodeId()).toBe('n2');
+ expect(AppStore.getNodeDetails().keySeq().last()).toEqual('n2');
+ expect(AppStore.getNodeDetails().size).toEqual(1);
+ expect(AppStore.getNodeDetails().has('n1')).toBeFalsy();
+ expect(AppStore.getNodeDetails().has('rel1')).toBeFalsy();
+ });
+
+ it('selectes relatives topology while keeping node selected', function() {
+ registeredCallback(ClickTopologyAction);
+ registeredCallback(ReceiveTopologiesAction);
+ expect(AppStore.getCurrentTopology().name).toBe('Topo1');
+
+ registeredCallback(ClickNodeAction);
+ expect(AppStore.getSelectedNodeId()).toBe('n1');
+ expect(AppStore.getNodeDetails().size).toEqual(1);
+ expect(AppStore.getNodeDetails().has('n1')).toBeTruthy();
+ expect(AppStore.getNodeDetails().keySeq().last()).toEqual('n1');
+
+ registeredCallback(ClickRelativeAction);
+ // stack relative, first node stays main node
+ expect(AppStore.getSelectedNodeId()).toBe('n1');
+ expect(AppStore.getNodeDetails().keySeq().last()).toEqual('rel1');
+ expect(AppStore.getNodeDetails().size).toEqual(2);
+ expect(AppStore.getNodeDetails().has('rel1')).toBeTruthy();
+
+ // click switches over to relative's topology and selectes relative
+ registeredCallback(ClickShowTopologyForNodeAction);
+ expect(AppStore.getSelectedNodeId()).toBe('rel1');
+ expect(AppStore.getNodeDetails().keySeq().last()).toEqual('rel1');
+ expect(AppStore.getNodeDetails().size).toEqual(1);
+ expect(AppStore.getCurrentTopology().name).toBe('Topo2');
+ });
});
diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js
index fca513c1e6..549c3da001 100644
--- a/client/app/scripts/stores/app-store.js
+++ b/client/app/scripts/stores/app-store.js
@@ -16,7 +16,7 @@ const error = debug('scope:error');
// Helpers
-function findCurrentTopology(subTree, topologyId) {
+function findTopologyById(subTree, topologyId) {
let foundTopology;
_.each(subTree, function(topology) {
@@ -24,7 +24,7 @@ function findCurrentTopology(subTree, topologyId) {
foundTopology = topology;
}
if (!foundTopology) {
- foundTopology = findCurrentTopology(topology.sub_topologies, topologyId);
+ foundTopology = findTopologyById(topology.sub_topologies, topologyId);
}
if (foundTopology) {
return false;
@@ -57,19 +57,22 @@ let hostname = '...';
let version = '...';
let mouseOverEdgeId = null;
let mouseOverNodeId = null;
+let nodeDetails = makeOrderedMap(); // nodeId -> details
let nodes = makeOrderedMap(); // nodeId -> node
-let nodeDetails = null;
let selectedNodeId = null;
let topologies = [];
let topologiesLoaded = false;
+let topologyUrlsById = makeOrderedMap(); // topologyId -> topologyUrl
let routeSet = false;
-let controlPipe = null;
+let controlPipes = makeOrderedMap(); // pipeId -> controlPipe
let websocketClosed = true;
+// adds ID field to topology (based on last part of URL path) and save urls in
+// map for easy lookup
function processTopologies(topologyList) {
- // adds ID field to topology, based on last part of URL path
_.each(topologyList, function(topology) {
topology.id = topology.url.split('/').pop();
+ topologyUrlsById = topologyUrlsById.set(topology.id, topology.url);
processTopologies(topology.sub_topologies);
});
return topologyList;
@@ -77,7 +80,7 @@ function processTopologies(topologyList) {
function setTopology(topologyId) {
currentTopologyId = topologyId;
- currentTopology = findCurrentTopology(topologies, topologyId);
+ currentTopology = findTopologyById(topologies, topologyId);
}
function setDefaultTopologyOptions(topologyList) {
@@ -102,10 +105,24 @@ function setDefaultTopologyOptions(topologyList) {
});
}
-function deSelectNode() {
- selectedNodeId = null;
- nodeDetails = null;
- controlPipe = null;
+function closeNodeDetails(nodeId) {
+ if (nodeDetails.size > 0) {
+ const popNodeId = nodeId || nodeDetails.keySeq().last();
+ // remove pipe if it belongs to the node being closed
+ controlPipes = controlPipes.filter(pipe => {
+ return pipe.nodeId !== popNodeId;
+ });
+ nodeDetails = nodeDetails.delete(popNodeId);
+ }
+ if (nodeDetails.size === 0 || selectedNodeId === nodeId) {
+ selectedNodeId = null;
+ }
+}
+
+function closeAllNodeDetails() {
+ while (nodeDetails.size) {
+ closeNodeDetails();
+ }
}
// Store API
@@ -115,9 +132,10 @@ export class AppStore extends Store {
// keep at the top
getAppState() {
return {
- topologyId: currentTopologyId,
- selectedNodeId: this.getSelectedNodeId(),
controlPipe: this.getControlPipe(),
+ nodeDetails: this.getNodeDetailsState(),
+ selectedNodeId: selectedNodeId,
+ topologyId: currentTopologyId,
topologyOptions: topologyOptions.toJS() // all options
};
}
@@ -148,7 +166,8 @@ export class AppStore extends Store {
}
getControlPipe() {
- return controlPipe;
+ const cp = controlPipes.last();
+ return cp && cp.toJS();
}
getCurrentTopology() {
@@ -214,6 +233,12 @@ export class AppStore extends Store {
return nodeDetails;
}
+ getNodeDetailsState() {
+ return nodeDetails.toIndexedSeq().map(details => {
+ return {id: details.id, label: details.label, topologyId: details.topologyId};
+ }).toJS();
+ }
+
getNodes() {
return nodes;
}
@@ -226,6 +251,10 @@ export class AppStore extends Store {
return topologies;
}
+ getTopologyUrlsById() {
+ return topologyUrlsById;
+ }
+
getVersion() {
return version;
}
@@ -269,27 +298,76 @@ export class AppStore extends Store {
this.__emitChange();
break;
+ case ActionTypes.CLICK_BACKGROUND:
+ closeAllNodeDetails();
+ this.__emitChange();
+ break;
+
case ActionTypes.CLICK_CLOSE_DETAILS:
- deSelectNode();
+ closeNodeDetails(payload.nodeId);
this.__emitChange();
break;
case ActionTypes.CLICK_CLOSE_TERMINAL:
- controlPipe = null;
+ controlPipes = controlPipes.clear();
this.__emitChange();
break;
case ActionTypes.CLICK_NODE:
- deSelectNode();
- if (payload.nodeId !== selectedNodeId) {
- // select new node if it's not the same (in that case just delesect)
+ const prevSelectedNodeId = selectedNodeId;
+ const prevDetailsStackSize = nodeDetails.size;
+ // click on sibling closes all
+ closeAllNodeDetails();
+ // select new node if it's not the same (in that case just delesect)
+ if (prevDetailsStackSize > 1 || prevSelectedNodeId !== payload.nodeId) {
+ // dont set origin if a node was already selected, suppresses animation
+ const origin = prevSelectedNodeId === null ? payload.origin : null;
+ nodeDetails = nodeDetails.set(
+ payload.nodeId,
+ {
+ id: payload.nodeId,
+ label: payload.label,
+ origin,
+ topologyId: currentTopologyId
+ }
+ );
selectedNodeId = payload.nodeId;
}
this.__emitChange();
break;
+ case ActionTypes.CLICK_RELATIVE:
+ if (nodeDetails.has(payload.nodeId)) {
+ // bring to front
+ const details = nodeDetails.get(payload.nodeId);
+ nodeDetails = nodeDetails.delete(payload.nodeId);
+ nodeDetails = nodeDetails.set(payload.nodeId, details);
+ } else {
+ nodeDetails = nodeDetails.set(
+ payload.nodeId,
+ {
+ id: payload.nodeId,
+ label: payload.label,
+ origin: payload.origin,
+ topologyId: payload.topologyId
+ }
+ );
+ }
+ this.__emitChange();
+ break;
+
+ case ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE:
+ nodeDetails = nodeDetails.filter((v, k) => k === payload.nodeId);
+ selectedNodeId = payload.nodeId;
+ if (payload.topologyId !== currentTopologyId) {
+ setTopology(payload.topologyId);
+ nodes = nodes.clear();
+ }
+ this.__emitChange();
+ break;
+
case ActionTypes.CLICK_TOPOLOGY:
- deSelectNode();
+ closeAllNodeDetails();
if (payload.topologyId !== currentTopologyId) {
setTopology(payload.topologyId);
nodes = nodes.clear();
@@ -302,6 +380,11 @@ export class AppStore extends Store {
this.__emitChange();
break;
+ case ActionTypes.DESELECT_NODE:
+ closeNodeDetails();
+ this.__emitChange();
+ break;
+
case ActionTypes.DO_CONTROL:
controlStatus = controlStatus.set(payload.nodeId, makeMap({
pending: true,
@@ -320,11 +403,6 @@ export class AppStore extends Store {
this.__emitChange();
break;
- case ActionTypes.DESELECT_NODE:
- deSelectNode();
- this.__emitChange();
- break;
-
case ActionTypes.LEAVE_EDGE:
mouseOverEdgeId = null;
this.__emitChange();
@@ -360,16 +438,17 @@ export class AppStore extends Store {
break;
case ActionTypes.RECEIVE_CONTROL_PIPE:
- controlPipe = {
+ controlPipes = controlPipes.set(payload.pipeId, makeOrderedMap({
id: payload.pipeId,
+ nodeId: payload.nodeId,
raw: payload.rawTty
- };
+ }));
this.__emitChange();
break;
case ActionTypes.RECEIVE_CONTROL_PIPE_STATUS:
- if (controlPipe) {
- controlPipe.status = payload.status;
+ if (controlPipes.has(payload.pipeId)) {
+ controlPipes = controlPipes.setIn([payload.pipeId, 'status'], payload.status);
this.__emitChange();
}
break;
@@ -382,8 +461,12 @@ export class AppStore extends Store {
case ActionTypes.RECEIVE_NODE_DETAILS:
errorUrl = null;
// disregard if node is not selected anymore
- if (payload.details.id === selectedNodeId) {
- nodeDetails = payload.details;
+ if (nodeDetails.has(payload.details.id)) {
+ nodeDetails = nodeDetails.update(payload.details.id, obj => {
+ obj.notFound = false;
+ obj.details = payload.details;
+ return obj;
+ });
}
this.__emitChange();
break;
@@ -415,7 +498,9 @@ export class AppStore extends Store {
// update existing nodes
_.each(payload.delta.update, function(node) {
- nodes = nodes.set(node.id, nodes.get(node.id).merge(makeNode(node)));
+ if (nodes.has(node.id)) {
+ nodes = nodes.set(node.id, nodes.get(node.id).merge(makeNode(node)));
+ }
});
// add new nodes
@@ -426,8 +511,19 @@ export class AppStore extends Store {
this.__emitChange();
break;
+ case ActionTypes.RECEIVE_NOT_FOUND:
+ if (nodeDetails.has(payload.nodeId)) {
+ nodeDetails = nodeDetails.update(payload.nodeId, obj => {
+ obj.notFound = true;
+ return obj;
+ });
+ this.__emitChange();
+ }
+ break;
+
case ActionTypes.RECEIVE_TOPOLOGIES:
errorUrl = null;
+ topologyUrlsById = topologyUrlsById.clear();
topologies = processTopologies(payload.topologies);
setTopology(currentTopologyId);
// only set on first load, if options are not already set via route
@@ -453,7 +549,19 @@ export class AppStore extends Store {
setTopology(payload.state.topologyId);
setDefaultTopologyOptions(topologies);
selectedNodeId = payload.state.selectedNodeId;
- controlPipe = payload.state.controlPipe;
+ if (payload.state.controlPipe) {
+ controlPipes = makeOrderedMap({
+ [payload.state.controlPipe.pipeId]:
+ makeOrderedMap(payload.state.controlPipe)
+ });
+ } else {
+ controlPipes = controlPipes.clear();
+ }
+ if (payload.state.nodeDetails) {
+ nodeDetails = makeOrderedMap(payload.state.nodeDetails.map(obj => [obj.id, obj]));
+ } else {
+ nodeDetails = nodeDetails.clear();
+ }
topologyOptions = Immutable.fromJS(payload.state.topologyOptions)
|| topologyOptions;
this.__emitChange();
diff --git a/client/app/scripts/utils/__tests__/string-utils-test.js b/client/app/scripts/utils/__tests__/string-utils-test.js
new file mode 100644
index 0000000000..4bf7d418e0
--- /dev/null
+++ b/client/app/scripts/utils/__tests__/string-utils-test.js
@@ -0,0 +1,13 @@
+jest.dontMock('../string-utils');
+
+describe('StringUtils', function() {
+ const StringUtils = require('../string-utils');
+
+ describe('formatMetric', function() {
+ const formatMetric = StringUtils.formatMetric;
+
+ it('it should render 0', function() {
+ expect(formatMetric(0)).toBe(0);
+ });
+ });
+});
diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js
new file mode 100644
index 0000000000..e317641e9d
--- /dev/null
+++ b/client/app/scripts/utils/string-utils.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import filesize from 'filesize';
+
+const formatters = {
+ filesize(value) {
+ const obj = filesize(value, {output: 'object'});
+ return formatters.metric(obj.value, obj.suffix);
+ },
+
+ number(value) {
+ return value;
+ },
+
+ percent(value) {
+ return formatters.metric(value, '%');
+ },
+
+ metric(text, unit) {
+ return (
+
+ {text}
+ {unit}
+
+ );
+ }
+};
+
+export function formatMetric(value, opts) {
+ const formatter = opts && formatters[opts.format] ? opts.format : 'number';
+ return formatters[formatter](value);
+}
diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js
index 2269aa8c91..b23d2dba12 100644
--- a/client/app/scripts/utils/web-api-utils.js
+++ b/client/app/scripts/utils/web-api-utils.js
@@ -4,7 +4,7 @@ import reqwest from 'reqwest';
import { clearControlError, closeWebsocket, openWebsocket, receiveError,
receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError,
receiveControlPipe, receiveControlPipeStatus, receiveControlSuccess,
- receiveTopologies } from '../actions/app-actions';
+ receiveTopologies, receiveNotFound } from '../actions/app-actions';
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = wsProto + '://' + location.host + location.pathname.replace(/\/$/, '');
@@ -118,23 +118,33 @@ export function getNodesDelta(topologyUrl, options) {
}
}
-export function getNodeDetails(topologyUrl, nodeId) {
- if (topologyUrl && nodeId) {
- const url = [topologyUrl, '/', encodeURIComponent(nodeId)]
+export function getNodeDetails(topologyUrlsById, nodeMap) {
+ // get details for all opened nodes
+ const obj = nodeMap.last();
+ if (obj && topologyUrlsById.has(obj.topologyId)) {
+ const topologyUrl = topologyUrlsById.get(obj.topologyId);
+ const url = [topologyUrl, '/', encodeURIComponent(obj.id)]
.join('').substr(1);
reqwest({
url: url,
success: function(res) {
- receiveNodeDetails(res.node);
+ // make sure node is still selected
+ if (nodeMap.has(res.node.id)) {
+ receiveNodeDetails(res.node);
+ }
},
error: function(err) {
log('Error in node details request: ' + err.responseText);
// dont treat missing node as error
- if (err.status !== 404) {
+ if (err.status === 404) {
+ receiveNotFound(obj.id);
+ } else {
receiveError(topologyUrl);
}
}
});
+ } else {
+ log('No details or url found for ', obj);
}
}
diff --git a/client/app/styles/main.less b/client/app/styles/main.less
index ecd0178d24..51082f9c6e 100644
--- a/client/app/styles/main.less
+++ b/client/app/styles/main.less
@@ -29,6 +29,7 @@
@text-color: lighten(@primary-color, 10%);
@text-secondary-color: lighten(@primary-color, 33%);
@text-tertiary-color: lighten(@primary-color, 50%);
+@border-light-color: lighten(@primary-color, 66%);
@text-darker-color: @primary-color;
@white: @background-secondary-color;
@@ -338,20 +339,38 @@ h2 {
}
-#details {
- position: fixed;
- z-index: 1024;
- display: block;
- right: @details-window-padding-left;
- top: 24px;
- bottom: 48px;
- width: @details-window-width;
-
- .details-tools-wrapper {
+.details {
+ &-wrapper {
+ position: fixed;
+ z-index: 1024;
+ right: @details-window-padding-left;
+ top: 24px;
+ bottom: 48px;
+ width: @details-window-width;
+ transition: transform 0.33333s cubic-bezier(0,0,0.21,1);
+ }
+}
+
+.node-details {
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.86);
+ display: flex;
+ flex-flow: column;
+ margin-bottom: 12px;
+ padding-bottom: 2px;
+ border-radius: 2px;
+ background-color: #fff;
+ .shadow-2;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ &-tools-wrapper {
position: relative;
}
- .details-tools {
+ &-tools {
position: absolute;
top: 6px;
right: 8px;
@@ -374,31 +393,11 @@ h2 {
}
}
- .details-wrapper {
- height: 100%;
- padding-bottom: 8px;
- border-radius: 2px;
- background-color: #fff;
- .shadow-2;
- }
-}
-
-.node-details {
- height: 100%;
- width: 100%;
- background-color: rgba(255, 255, 255, 0.86);
- display: flex;
- flex-flow: column;
-
&-header {
.colorable;
&-wrapper {
- padding: 36px 36px 16px 36px;
- }
-
- &-row {
- display: flex;
+ padding: 36px 36px 8px 36px;
}
&-label {
@@ -406,12 +405,6 @@ h2 {
margin: 0;
width: 348px;
padding-top: 0;
-
- &-minor {
- flex: 1;
- font-size: 120%;
- color: @white;
- }
}
.details-tools {
@@ -426,11 +419,50 @@ h2 {
}
+ &-relatives {
+ margin-top: 4px;
+ font-size: 120%;
+ color: @white;
+
+ &-link {
+ .truncate;
+ .palable;
+ display: inline-block;
+ margin-right: 0.5em;
+ cursor: pointer;
+ text-decoration: underline;
+ opacity: 0.8;
+ max-width: 12em;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ &-more {
+ .palable;
+ padding: 0 2px;
+ text-transform: uppercase;
+ cursor: pointer;
+ opacity: 0.7;
+ font-size: 60%;
+ font-weight: bold;
+ display: inline-block;
+ position: relative;
+ top: -5px;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+
&-controls {
white-space: nowrap;
+ padding: 8px 0;
&-wrapper {
- padding: 8px 36px 8px 32px;
+ padding: 0 36px 0 32px;
}
.node-control-button {
@@ -478,8 +510,7 @@ h2 {
&-content {
flex: 1;
padding: 0 36px 0 36px;
- overflow-y: scroll;
- width: 100%;
+ overflow-y: auto;
&-info {
margin-top: 16px;
@@ -492,36 +523,206 @@ h2 {
color: @background-medium-color;
opacity: 0.7;
}
+
+ &-section {
+ margin: 16px 0;
+
+ &-header {
+ text-transform: uppercase;
+ font-size: 90%;
+ color: @text-tertiary-color;
+ padding: 4px 0;
+ }
+ }
+ }
+
+ &-health {
+ display: flex;
+ justify-content: space-around;
+ align-content: center;
+ text-align: center;
+
+ &-expand {
+ .palable;
+ margin: 4px 16px 0;
+ border-top: 1px solid @border-light-color;
+ text-transform: uppercase;
+ font-size: 80%;
+ color: @text-secondary-color;
+ width: 100%;
+ cursor: pointer;
+ opacity: 0.8;
+
+ &:hover {
+ opacity: 1.0;
+ }
+ }
+
+ &-overflow {
+ .palable;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+ border-left: 1px solid @border-light-color;
+ opacity: 0.85;
+ cursor: pointer;
+ position: relative;
+ padding-bottom: 16px;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ &-expand {
+ text-transform: uppercase;
+ font-size: 70%;
+ color: @text-secondary-color;
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ right: 0;
+ }
+
+ &-item {
+ padding: 4px 8px;
+ line-height: 1.2;
+ flex-basis: 48%;
+
+ &-value {
+ color: @text-secondary-color;
+ font-size: 100%;
+ }
+
+ &-label {
+ color: @text-secondary-color;
+ text-transform: uppercase;
+ font-size: 60%;
+ }
+ }
+ }
+
+ &-item {
+ padding: 8px 16px;
+ width: 33%;
+
+ &-label {
+ color: @text-secondary-color;
+ text-transform: uppercase;
+ font-size: 80%;
+ }
+
+ &-value {
+ color: @text-secondary-color;
+ font-size: 150%;
+ padding-bottom: 0.5em;
+ }
+ }
+ }
+
+ &-info {
+ margin: 16px 0;
+
+ &-field {
+ display: flex;
+ align-items: baseline;
+
+ &-label {
+ text-align: right;
+ width: 30%;
+ color: @text-secondary-color;
+ padding: 0 0.5em 0 0;
+ white-space: nowrap;
+ text-transform: uppercase;
+ font-size: 80%;
+
+ &::after {
+ content: ':';
+ }
+ }
+
+ &-value {
+ font-size: 105%;
+ flex: 1;
+ color: @text-color;
+ }
+ }
}
&-table {
+ width: 100%;
+ border-spacing: 0;
+ /* need fixed for truncating, but that does not extend wide columns dynamically */
+ table-layout: fixed;
- &:last-child {
- margin-bottom: 1em;
+ &-wrapper {
+ margin: 24px 0;
}
- &-title {
+ &-header {
text-transform: uppercase;
- margin-bottom: 0;
+ color: @text-tertiary-color;
+ font-size: 90%;
+ text-align: right;
+ cursor: pointer;
+ padding: 0;
+
+ &-sorted {
+ color: @text-secondary-color;
+ }
+
+ &-sorter {
+ margin: 0 0.25em;
+ }
+
+ &:first-child {
+ margin-right: 0;
+ text-align: left;
+ }
+ }
+
+ &-more {
+ .palable;
+ padding: 2px 0;
+ text-transform: uppercase;
+ cursor: pointer;
color: @text-secondary-color;
- font-size: 100%;
+ opacity: 0.7;
+ font-size: 80%;
+ font-weight: bold;
+
+ &:hover {
+ opacity: 1;
+ }
}
- &-row {
- white-space: nowrap;
- clear: left;
+ &-node {
+ font-size: 105%;
+ line-height: 1.5;
- &-key {
- width: 11em;
- float: left;
+ > * {
+ padding: 0;
}
- &-value-major {
- margin-right: 0.5em;
+ &-link {
+ .palable;
+ text-decoration: underline;
+ cursor: pointer;
+ opacity: 0.8;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ &-value {
+ flex: 1;
+ margin-left: 0.5em;
+ text-align: right;
}
&-value-scalar {
- width: 2em;
+ // width: 2em;
text-align: right;
margin-right: 0.5em;
}
@@ -665,6 +866,12 @@ h2 {
visibility: hidden;
}
+.metric {
+ &-unit {
+ padding-left: 0.25em;
+ }
+}
+
.sidebar {
position: fixed;
bottom: 16px;
diff --git a/client/package.json b/client/package.json
index 435b617d4a..bdb70ca42c 100644
--- a/client/package.json
+++ b/client/package.json
@@ -10,6 +10,7 @@
"d3": "~3.5.5",
"dagre": "0.7.4",
"debug": "~2.2.0",
+ "filesize": "3.1.4",
"flux": "2.1.1",
"font-awesome": "4.4.0",
"font-awesome-webpack": "0.0.4",
diff --git a/integration/410_container_control_test.sh b/integration/410_container_control_test.sh
index b96e434b9e..fa28a33cdf 100755
--- a/integration/410_container_control_test.sh
+++ b/integration/410_container_control_test.sh
@@ -14,7 +14,7 @@ wait_for_containers $HOST1 60 alpine
assert "docker_on $HOST1 inspect --format='{{.State.Running}}' alpine" "true"
PROBEID=$(docker_on $HOST1 logs weavescope 2>&1 | grep "probe starting" | sed -n 's/^.*ID \([0-9a-f]*\)$/\1/p')
HOSTID=$(echo $HOST1 | cut -d"." -f1)
-assert_raises "curl -f -X POST 'http://$HOST1:4040/api/control/$PROBEID/$HOSTID;$CID/docker_stop_container'"
+assert_raises "curl -f -X POST 'http://$HOST1:4040/api/control/$PROBEID/$CID;
/docker_stop_container'"
sleep 5
assert "docker_on $HOST1 inspect --format='{{.State.Running}}' alpine" "false"
diff --git a/probe/docker/container.go b/probe/docker/container.go
index f848c2f465..95535f57a6 100644
--- a/probe/docker/container.go
+++ b/probe/docker/container.go
@@ -331,7 +331,11 @@ func (c *container) GetNode(hostID string, localAddrs []net.IP) report.Node {
ContainerIPsWithScopes: report.MakeStringSet(ipsWithScopes...),
}).WithLatest(
ContainerState, mtime.Now(), state,
- ).WithMetrics(c.metrics())
+ ).WithMetrics(
+ c.metrics(),
+ ).WithParents(report.Sets{
+ report.ContainerImage: report.MakeStringSet(report.MakeContainerImageNodeID(c.container.Image)),
+ })
if c.container.State.Paused {
result = result.WithControls(UnpauseContainer)
diff --git a/probe/docker/container_test.go b/probe/docker/container_test.go
index c83dbe8a62..91a540fd3d 100644
--- a/probe/docker/container_test.go
+++ b/probe/docker/container_test.go
@@ -92,6 +92,8 @@ func TestContainer(t *testing.T) {
).WithMetrics(report.Metrics{
"cpu_total_usage": report.MakeMetric(),
"memory_usage": report.MakeMetric().Add(now, 12345),
+ }).WithParents(report.Sets{
+ report.ContainerImage: report.MakeStringSet(report.MakeContainerImageNodeID("baz")),
})
test.Poll(t, 100*time.Millisecond, want, func() interface{} {
node := c.GetNode("scope", []net.IP{})
diff --git a/probe/docker/controls.go b/probe/docker/controls.go
index 2057069e4c..b0871e77fc 100644
--- a/probe/docker/controls.go
+++ b/probe/docker/controls.go
@@ -142,7 +142,7 @@ func (r *registry) execContainer(containerID string, req xfer.Request) xfer.Resp
func captureContainerID(f func(string, xfer.Request) xfer.Response) func(xfer.Request) xfer.Response {
return func(req xfer.Request) xfer.Response {
- _, containerID, ok := report.ParseContainerNodeID(req.NodeID)
+ containerID, ok := report.ParseContainerNodeID(req.NodeID)
if !ok {
return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID)
}
diff --git a/probe/docker/controls_test.go b/probe/docker/controls_test.go
index 97ab285f68..f6b355704a 100644
--- a/probe/docker/controls_test.go
+++ b/probe/docker/controls_test.go
@@ -30,7 +30,7 @@ func TestControls(t *testing.T) {
} {
result := controls.HandleControlRequest(xfer.Request{
Control: tc.command,
- NodeID: report.MakeContainerNodeID("", "a1b2c3d4e5"),
+ NodeID: report.MakeContainerNodeID("a1b2c3d4e5"),
})
if !reflect.DeepEqual(result, xfer.Response{
Error: tc.result,
@@ -72,7 +72,7 @@ func TestPipes(t *testing.T) {
} {
result := controls.HandleControlRequest(xfer.Request{
Control: tc,
- NodeID: report.MakeContainerNodeID("", "ping"),
+ NodeID: report.MakeContainerNodeID("ping"),
})
want := xfer.Response{
Pipe: "pipeid",
diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go
index 0de846be95..fd7800f7da 100644
--- a/probe/docker/reporter.go
+++ b/probe/docker/reporter.go
@@ -48,7 +48,7 @@ func (r *Reporter) ContainerUpdated(c Container) {
// Publish a 'short cut' report container just this container
rpt := report.MakeReport()
rpt.Shortcut = true
- rpt.Container.AddNode(report.MakeContainerNodeID(r.hostID, c.ID()), c.GetNode(r.hostID, localAddrs))
+ rpt.Container.AddNode(report.MakeContainerNodeID(c.ID()), c.GetNode(r.hostID, localAddrs))
r.probe.Publish(rpt)
}
@@ -104,7 +104,7 @@ func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology {
})
r.registry.WalkContainers(func(c Container) {
- nodeID := report.MakeContainerNodeID(r.hostID, c.ID())
+ nodeID := report.MakeContainerNodeID(c.ID())
result.AddNode(nodeID, c.GetNode(r.hostID, localAddrs))
})
@@ -124,7 +124,7 @@ func (r *Reporter) containerImageTopology() report.Topology {
nmd.Metadata[ImageName] = image.RepoTags[0]
}
- nodeID := report.MakeContainerNodeID(r.hostID, image.ID)
+ nodeID := report.MakeContainerImageNodeID(image.ID)
result.AddNode(nodeID, nmd)
})
diff --git a/probe/docker/reporter_test.go b/probe/docker/reporter_test.go
index 6af8898873..dc944e6b67 100644
--- a/probe/docker/reporter_test.go
+++ b/probe/docker/reporter_test.go
@@ -55,7 +55,7 @@ func TestReporter(t *testing.T) {
want := report.MakeReport()
want.Container = report.Topology{
Nodes: report.Nodes{
- report.MakeContainerNodeID("", "ping"): report.MakeNodeWith(map[string]string{
+ report.MakeContainerNodeID("ping"): report.MakeNodeWith(map[string]string{
docker.ContainerID: "ping",
docker.ContainerName: "pong",
docker.ImageID: "baz",
@@ -101,7 +101,7 @@ func TestReporter(t *testing.T) {
}
want.ContainerImage = report.Topology{
Nodes: report.Nodes{
- report.MakeContainerNodeID("", "baz"): report.MakeNodeWith(map[string]string{
+ report.MakeContainerImageNodeID("baz"): report.MakeNodeWith(map[string]string{
docker.ImageID: "baz",
docker.ImageName: "bang",
}),
@@ -109,7 +109,7 @@ func TestReporter(t *testing.T) {
Controls: report.Controls{},
}
- reporter := docker.NewReporter(mockRegistryInstance, "", nil)
+ reporter := docker.NewReporter(mockRegistryInstance, "host1", nil)
have, _ := reporter.Report()
if !reflect.DeepEqual(want, have) {
t.Errorf("%s", test.Diff(want, have))
diff --git a/probe/docker/tagger.go b/probe/docker/tagger.go
index 4582d70c1c..cd982fd417 100644
--- a/probe/docker/tagger.go
+++ b/probe/docker/tagger.go
@@ -84,6 +84,9 @@ func (t *Tagger) tag(tree process.Tree, topology *report.Topology) {
topology.AddNode(nodeID, report.MakeNodeWith(map[string]string{
ContainerID: c.ID(),
+ }).WithParents(report.Sets{
+ report.Container: report.MakeStringSet(report.MakeContainerNodeID(c.ID())),
+ report.ContainerImage: report.MakeStringSet(report.MakeContainerImageNodeID(c.Image())),
}))
}
}
diff --git a/probe/docker/tagger_test.go b/probe/docker/tagger_test.go
index 1415d4e1bf..3dc0427259 100644
--- a/probe/docker/tagger_test.go
+++ b/probe/docker/tagger_test.go
@@ -38,7 +38,12 @@ func TestTagger(t *testing.T) {
var (
pid1NodeID = report.MakeProcessNodeID("somehost.com", "2")
pid2NodeID = report.MakeProcessNodeID("somehost.com", "3")
- wantNode = report.MakeNodeWith(map[string]string{docker.ContainerID: "ping"})
+ wantNode = report.MakeNodeWith(map[string]string{
+ docker.ContainerID: "ping",
+ }).WithParents(report.Sets{
+ report.Container: report.MakeStringSet(report.MakeContainerNodeID("ping")),
+ report.ContainerImage: report.MakeStringSet(report.MakeContainerImageNodeID("baz")),
+ })
)
input := report.MakeReport()
diff --git a/probe/host/tagger.go b/probe/host/tagger.go
index 6b5237896b..c84654e4bf 100644
--- a/probe/host/tagger.go
+++ b/probe/host/tagger.go
@@ -26,16 +26,21 @@ func (Tagger) Name() string { return "Host" }
// Tag implements Tagger.
func (t Tagger) Tag(r report.Report) (report.Report, error) {
- metadata := map[string]string{
- report.HostNodeID: t.hostNodeID,
- report.ProbeID: t.probeID,
- }
+ var (
+ metadata = map[string]string{
+ report.HostNodeID: t.hostNodeID,
+ report.ProbeID: t.probeID,
+ }
+ parents = report.Sets{
+ report.Host: report.MakeStringSet(t.hostNodeID),
+ }
+ )
// Explicity don't tag Endpoints and Addresses - These topologies include pseudo nodes,
// and as such do their own host tagging
for _, topology := range []report.Topology{r.Process, r.Container, r.ContainerImage, r.Host, r.Overlay} {
for id, node := range topology.Nodes {
- topology.AddNode(id, node.WithMetadata(metadata))
+ topology.AddNode(id, node.WithMetadata(metadata).WithParents(parents))
}
}
return r, nil
diff --git a/probe/host/tagger_test.go b/probe/host/tagger_test.go
index 0a736f4ce6..011d56162c 100644
--- a/probe/host/tagger_test.go
+++ b/probe/host/tagger_test.go
@@ -22,6 +22,8 @@ func TestTagger(t *testing.T) {
want := nodeMetadata.Merge(report.MakeNodeWith(map[string]string{
report.HostNodeID: report.MakeHostNodeID(hostID),
report.ProbeID: probeID,
+ }).WithParents(report.Sets{
+ report.Host: report.MakeStringSet(report.MakeHostNodeID(hostID)),
}))
rpt, _ := host.NewTagger(hostID, probeID).Tag(r)
have := rpt.Process.Nodes[endpointNodeID].Copy()
diff --git a/probe/kubernetes/pod.go b/probe/kubernetes/pod.go
index 6cf86a2057..0cb8dcc4d9 100644
--- a/probe/kubernetes/pod.go
+++ b/probe/kubernetes/pod.go
@@ -84,5 +84,14 @@ func (p *pod) GetNode() report.Node {
if len(p.serviceIDs) > 0 {
n.Metadata[ServiceIDs] = strings.Join(p.serviceIDs, " ")
}
+ for _, serviceID := range p.serviceIDs {
+ segments := strings.SplitN(serviceID, "/", 2)
+ if len(segments) != 2 {
+ continue
+ }
+ n = n.WithParents(report.Sets{
+ report.Service: report.MakeStringSet(report.MakeServiceNodeID(p.Namespace(), segments[1])),
+ })
+ }
return n
}
diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go
index 816c0f0058..afb2f5a9aa 100644
--- a/probe/kubernetes/reporter.go
+++ b/probe/kubernetes/reporter.go
@@ -26,12 +26,13 @@ func (r *Reporter) Report() (report.Report, error) {
if err != nil {
return result, err
}
- podTopology, err := r.podTopology(services)
+ podTopology, containerTopology, err := r.podTopology(services)
if err != nil {
return result, err
}
result.Service = result.Service.Merge(serviceTopology)
result.Pod = result.Pod.Merge(podTopology)
+ result.Container = result.Container.Merge(containerTopology)
return result, nil
}
@@ -49,8 +50,8 @@ func (r *Reporter) serviceTopology() (report.Topology, []Service, error) {
return result, services, err
}
-func (r *Reporter) podTopology(services []Service) (report.Topology, error) {
- result := report.MakeTopology()
+func (r *Reporter) podTopology(services []Service) (report.Topology, report.Topology, error) {
+ pods, containers := report.MakeTopology(), report.MakeTopology()
err := r.client.WalkPods(func(p Pod) error {
for _, service := range services {
if service.Selector().Matches(p.Labels()) {
@@ -58,8 +59,18 @@ func (r *Reporter) podTopology(services []Service) (report.Topology, error) {
}
}
nodeID := report.MakePodNodeID(p.Namespace(), p.Name())
- result = result.AddNode(nodeID, p.GetNode())
+ pods = pods.AddNode(nodeID, p.GetNode())
+
+ container := report.MakeNodeWith(map[string]string{
+ PodID: p.ID(),
+ Namespace: p.Namespace(),
+ }).WithParents(report.Sets{
+ report.Pod: report.MakeStringSet(nodeID),
+ })
+ for _, containerID := range p.ContainerIDs() {
+ containers.AddNode(report.MakeContainerNodeID(containerID), container)
+ }
return nil
})
- return result, err
+ return pods, containers, err
}
diff --git a/probe/kubernetes/reporter_test.go b/probe/kubernetes/reporter_test.go
index 36b61f59f3..092ae94814 100644
--- a/probe/kubernetes/reporter_test.go
+++ b/probe/kubernetes/reporter_test.go
@@ -111,6 +111,7 @@ func TestReporter(t *testing.T) {
want := report.MakeReport()
pod1ID := report.MakePodNodeID("ping", "pong-a")
pod2ID := report.MakePodNodeID("ping", "pong-b")
+ serviceID := report.MakeServiceNodeID("ping", "pongservice")
want.Pod = report.MakeTopology().AddNode(pod1ID, report.MakeNodeWith(map[string]string{
kubernetes.PodID: "ping/pong-a",
kubernetes.PodName: "pong-a",
@@ -118,6 +119,8 @@ func TestReporter(t *testing.T) {
kubernetes.PodCreated: pod1.Created(),
kubernetes.PodContainerIDs: "container1 container2",
kubernetes.ServiceIDs: "ping/pongservice",
+ }).WithParents(report.Sets{
+ report.Service: report.MakeStringSet(serviceID),
})).AddNode(pod2ID, report.MakeNodeWith(map[string]string{
kubernetes.PodID: "ping/pong-b",
kubernetes.PodName: "pong-b",
@@ -125,13 +128,36 @@ func TestReporter(t *testing.T) {
kubernetes.PodCreated: pod1.Created(),
kubernetes.PodContainerIDs: "container3 container4",
kubernetes.ServiceIDs: "ping/pongservice",
+ }).WithParents(report.Sets{
+ report.Service: report.MakeStringSet(serviceID),
}))
- want.Service = report.MakeTopology().AddNode(report.MakeServiceNodeID("ping", "pongservice"), report.MakeNodeWith(map[string]string{
+ want.Service = report.MakeTopology().AddNode(serviceID, report.MakeNodeWith(map[string]string{
kubernetes.ServiceID: "ping/pongservice",
kubernetes.ServiceName: "pongservice",
kubernetes.Namespace: "ping",
kubernetes.ServiceCreated: pod1.Created(),
}))
+ want.Container = report.MakeTopology().AddNode(report.MakeContainerNodeID("container1"), report.MakeNodeWith(map[string]string{
+ kubernetes.PodID: "ping/pong-a",
+ kubernetes.Namespace: "ping",
+ }).WithParents(report.Sets{
+ report.Pod: report.MakeStringSet(pod1ID),
+ })).AddNode(report.MakeContainerNodeID("container2"), report.MakeNodeWith(map[string]string{
+ kubernetes.PodID: "ping/pong-a",
+ kubernetes.Namespace: "ping",
+ }).WithParents(report.Sets{
+ report.Pod: report.MakeStringSet(pod1ID),
+ })).AddNode(report.MakeContainerNodeID("container3"), report.MakeNodeWith(map[string]string{
+ kubernetes.PodID: "ping/pong-b",
+ kubernetes.Namespace: "ping",
+ }).WithParents(report.Sets{
+ report.Pod: report.MakeStringSet(pod2ID),
+ })).AddNode(report.MakeContainerNodeID("container4"), report.MakeNodeWith(map[string]string{
+ kubernetes.PodID: "ping/pong-b",
+ kubernetes.Namespace: "ping",
+ }).WithParents(report.Sets{
+ report.Pod: report.MakeStringSet(pod2ID),
+ }))
reporter := kubernetes.NewReporter(mockClientInstance)
have, _ := reporter.Report()
diff --git a/probe/overlay/weave.go b/probe/overlay/weave.go
index 67ccdb5051..f72c837624 100644
--- a/probe/overlay/weave.go
+++ b/probe/overlay/weave.go
@@ -195,7 +195,7 @@ func (w *Weave) Tag(r report.Report) (report.Report, error) {
if entry.Tombstone > 0 {
continue
}
- nodeID := report.MakeContainerNodeID(w.hostID, entry.ContainerID)
+ nodeID := report.MakeContainerNodeID(entry.ContainerID)
node, ok := r.Container.Nodes[nodeID]
if !ok {
continue
diff --git a/probe/overlay/weave_test.go b/probe/overlay/weave_test.go
index 9d551f7939..9449a5dbe1 100644
--- a/probe/overlay/weave_test.go
+++ b/probe/overlay/weave_test.go
@@ -46,7 +46,7 @@ func TestWeaveTaggerOverlayTopology(t *testing.T) {
}
{
- nodeID := report.MakeContainerNodeID(mockHostID, mockContainerID)
+ nodeID := report.MakeContainerNodeID(mockContainerID)
want := report.Report{
Container: report.MakeTopology().AddNode(nodeID, report.MakeNodeWith(map[string]string{
docker.ContainerID: mockContainerID,
diff --git a/probe/probe_internal_test.go b/probe/probe_internal_test.go
index 3370447e59..f27e7e92c7 100644
--- a/probe/probe_internal_test.go
+++ b/probe/probe_internal_test.go
@@ -33,8 +33,8 @@ func TestApply(t *testing.T) {
from report.Topology
via string
}{
- {endpointNode.Merge(report.MakeNodeWith(map[string]string{"topology": "endpoint"})), r.Endpoint, endpointNodeID},
- {addressNode.Merge(report.MakeNodeWith(map[string]string{"topology": "address"})), r.Address, addressNodeID},
+ {endpointNode.Merge(report.MakeNode().WithID("c").WithTopology(report.Endpoint)), r.Endpoint, endpointNodeID},
+ {addressNode.Merge(report.MakeNode().WithID("d").WithTopology(report.Address)), r.Address, addressNodeID},
} {
if want, have := tuple.want, tuple.from.Nodes[tuple.via]; !reflect.DeepEqual(want, have) {
t.Errorf("want %+v, have %+v", want, have)
diff --git a/probe/topology_tagger.go b/probe/topology_tagger.go
index 1c8f975f99..764875e15e 100644
--- a/probe/topology_tagger.go
+++ b/probe/topology_tagger.go
@@ -4,9 +4,6 @@ import (
"github.com/weaveworks/scope/report"
)
-// Topology is the Node key for the origin topology.
-const Topology = "topology"
-
type topologyTagger struct{}
// NewTopologyTagger tags each node with the topology that it comes from. It's
@@ -19,18 +16,19 @@ func (topologyTagger) Name() string { return "Topology" }
// Tag implements Tagger
func (topologyTagger) Tag(r report.Report) (report.Report, error) {
- for val, topology := range map[string]*report.Topology{
- "endpoint": &(r.Endpoint),
- "address": &(r.Address),
- "process": &(r.Process),
- "container": &(r.Container),
- "container_image": &(r.ContainerImage),
- "host": &(r.Host),
- "overlay": &(r.Overlay),
+ for name, t := range map[string]*report.Topology{
+ report.Endpoint: &(r.Endpoint),
+ report.Address: &(r.Address),
+ report.Process: &(r.Process),
+ report.Container: &(r.Container),
+ report.ContainerImage: &(r.ContainerImage),
+ report.Pod: &(r.Pod),
+ report.Service: &(r.Service),
+ report.Host: &(r.Host),
+ report.Overlay: &(r.Overlay),
} {
- metadata := map[string]string{Topology: val}
- for id, node := range topology.Nodes {
- topology.AddNode(id, node.WithMetadata(metadata))
+ for id, node := range t.Nodes {
+ t.AddNode(id, node.WithID(id).WithTopology(name))
}
}
return r, nil
diff --git a/render/benchmark_test.go b/render/benchmark_test.go
new file mode 100644
index 0000000000..baad3f4987
--- /dev/null
+++ b/render/benchmark_test.go
@@ -0,0 +1,97 @@
+package render_test
+
+import (
+ "encoding/json"
+ "flag"
+ "io/ioutil"
+ "testing"
+
+ "github.com/weaveworks/scope/render"
+ "github.com/weaveworks/scope/report"
+ "github.com/weaveworks/scope/test/fixture"
+)
+
+var (
+ benchReportFile = flag.String("bench-report-file", "", "json report file to use for benchmarking (relative to this package)")
+ benchmarkRenderResult map[string]render.RenderableNode
+ benchmarkStatsResult render.Stats
+)
+
+func BenchmarkEndpointRender(b *testing.B) { benchmarkRender(b, render.EndpointRenderer) }
+func BenchmarkEndpointStats(b *testing.B) { benchmarkStats(b, render.EndpointRenderer) }
+func BenchmarkProcessRender(b *testing.B) { benchmarkRender(b, render.ProcessRenderer) }
+func BenchmarkProcessStats(b *testing.B) { benchmarkStats(b, render.ProcessRenderer) }
+func BenchmarkProcessWithContainerNameRender(b *testing.B) {
+ benchmarkRender(b, render.ProcessWithContainerNameRenderer)
+}
+func BenchmarkProcessWithContainerNameStats(b *testing.B) {
+ benchmarkStats(b, render.ProcessWithContainerNameRenderer)
+}
+func BenchmarkProcessNameRender(b *testing.B) { benchmarkRender(b, render.ProcessNameRenderer) }
+func BenchmarkProcessNameStats(b *testing.B) { benchmarkStats(b, render.ProcessNameRenderer) }
+func BenchmarkContainerRender(b *testing.B) { benchmarkRender(b, render.ContainerRenderer) }
+func BenchmarkContainerStats(b *testing.B) { benchmarkStats(b, render.ContainerRenderer) }
+func BenchmarkContainerWithImageNameRender(b *testing.B) {
+ benchmarkRender(b, render.ContainerWithImageNameRenderer)
+}
+func BenchmarkContainerWithImageNameStats(b *testing.B) {
+ benchmarkStats(b, render.ContainerWithImageNameRenderer)
+}
+func BenchmarkContainerImageRender(b *testing.B) { benchmarkRender(b, render.ContainerImageRenderer) }
+func BenchmarkContainerImageStats(b *testing.B) { benchmarkStats(b, render.ContainerImageRenderer) }
+func BenchmarkContainerHostnameRender(b *testing.B) {
+ benchmarkRender(b, render.ContainerHostnameRenderer)
+}
+func BenchmarkContainerHostnameStats(b *testing.B) {
+ benchmarkStats(b, render.ContainerHostnameRenderer)
+}
+func BenchmarkHostRender(b *testing.B) { benchmarkRender(b, render.HostRenderer) }
+func BenchmarkHostStats(b *testing.B) { benchmarkStats(b, render.HostRenderer) }
+func BenchmarkPodRender(b *testing.B) { benchmarkRender(b, render.PodRenderer) }
+func BenchmarkPodStats(b *testing.B) { benchmarkStats(b, render.PodRenderer) }
+func BenchmarkPodServiceRender(b *testing.B) { benchmarkRender(b, render.PodServiceRenderer) }
+func BenchmarkPodServiceStats(b *testing.B) { benchmarkStats(b, render.PodServiceRenderer) }
+
+func benchmarkRender(b *testing.B, r render.Renderer) {
+ report, err := loadReport()
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ benchmarkRenderResult = r.Render(report)
+ if len(benchmarkRenderResult) == 0 {
+ b.Errorf("Rendered topology contained no nodes")
+ }
+ }
+}
+
+func benchmarkStats(b *testing.B, r render.Renderer) {
+ report, err := loadReport()
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ // No way to tell if this was successful :(
+ benchmarkStatsResult = r.Stats(report)
+ }
+}
+
+func loadReport() (report.Report, error) {
+ if *benchReportFile == "" {
+ return fixture.Report, nil
+ }
+
+ var rpt report.Report
+ b, err := ioutil.ReadFile(*benchReportFile)
+ if err != nil {
+ return rpt, err
+ }
+ err = json.Unmarshal(b, &rpt)
+ return rpt, err
+}
diff --git a/render/detailed/metadata.go b/render/detailed/metadata.go
new file mode 100644
index 0000000000..55ae0c09de
--- /dev/null
+++ b/render/detailed/metadata.go
@@ -0,0 +1,135 @@
+package detailed
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/weaveworks/scope/probe/docker"
+ "github.com/weaveworks/scope/probe/host"
+ "github.com/weaveworks/scope/probe/kubernetes"
+ "github.com/weaveworks/scope/probe/overlay"
+ "github.com/weaveworks/scope/probe/process"
+ "github.com/weaveworks/scope/report"
+)
+
+var (
+ processNodeMetadata = renderMetadata(
+ meta(process.PID, "PID"),
+ meta(process.PPID, "Parent PID"),
+ meta(process.Cmdline, "Command"),
+ meta(process.Threads, "# Threads"),
+ )
+ containerNodeMetadata = renderMetadata(
+ meta(docker.ContainerID, "ID"),
+ meta(docker.ImageID, "Image ID"),
+ ltst(docker.ContainerState, "State"),
+ sets(docker.ContainerIPs, "IPs"),
+ sets(docker.ContainerPorts, "Ports"),
+ meta(docker.ContainerCreated, "Created"),
+ meta(docker.ContainerCommand, "Command"),
+ meta(overlay.WeaveMACAddress, "Weave MAC"),
+ meta(overlay.WeaveDNSHostname, "Weave DNS Hostname"),
+ getDockerLabelRows,
+ )
+ containerImageNodeMetadata = renderMetadata(
+ meta(docker.ImageID, "Image ID"),
+ getDockerLabelRows,
+ )
+ podNodeMetadata = renderMetadata(
+ meta(kubernetes.PodID, "ID"),
+ meta(kubernetes.Namespace, "Namespace"),
+ meta(kubernetes.PodCreated, "Created"),
+ )
+ hostNodeMetadata = renderMetadata(
+ meta(host.HostName, "Hostname"),
+ meta(host.OS, "Operating system"),
+ meta(host.KernelVersion, "Kernel version"),
+ meta(host.Uptime, "Uptime"),
+ sets(host.LocalNetworks, "Local Networks"),
+ )
+)
+
+// MetadataRow is a row for the metadata table.
+type MetadataRow struct {
+ ID string `json:"id"`
+ Label string `json:"label"`
+ Value string `json:"value"`
+}
+
+// Copy returns a value copy of a metadata row.
+func (m MetadataRow) Copy() MetadataRow {
+ return MetadataRow{
+ ID: m.ID,
+ Label: m.Label,
+ Value: m.Value,
+ }
+}
+
+// NodeMetadata produces a table (to be consumed directly by the UI) based on
+// an origin ID, which is (optimistically) a node ID in one of our topologies.
+func NodeMetadata(n report.Node) []MetadataRow {
+ renderers := map[string]func(report.Node) []MetadataRow{
+ report.Process: processNodeMetadata,
+ report.Container: containerNodeMetadata,
+ report.ContainerImage: containerImageNodeMetadata,
+ report.Pod: podNodeMetadata,
+ report.Host: hostNodeMetadata,
+ }
+ if renderer, ok := renderers[n.Topology]; ok {
+ return renderer(n)
+ }
+ return nil
+}
+
+func renderMetadata(templates ...func(report.Node) []MetadataRow) func(report.Node) []MetadataRow {
+ return func(nmd report.Node) []MetadataRow {
+ rows := []MetadataRow{}
+ for _, template := range templates {
+ rows = append(rows, template(nmd)...)
+ }
+ return rows
+ }
+}
+
+func meta(id, label string) func(report.Node) []MetadataRow {
+ return func(n report.Node) []MetadataRow {
+ if val, ok := n.Metadata[id]; ok {
+ return []MetadataRow{{ID: id, Label: label, Value: val}}
+ }
+ return nil
+ }
+}
+
+func sets(id, label string) func(report.Node) []MetadataRow {
+ return func(n report.Node) []MetadataRow {
+ if val, ok := n.Sets[id]; ok && len(val) > 0 {
+ return []MetadataRow{{ID: id, Label: label, Value: strings.Join(val, ", ")}}
+ }
+ return nil
+ }
+}
+
+func ltst(id, label string) func(report.Node) []MetadataRow {
+ return func(n report.Node) []MetadataRow {
+ if val, ok := n.Latest.Lookup(id); ok {
+ return []MetadataRow{{ID: id, Label: label, Value: val}}
+ }
+ return nil
+ }
+}
+
+func getDockerLabelRows(nmd report.Node) []MetadataRow {
+ rows := []MetadataRow{}
+ // Add labels in alphabetical order
+ labels := docker.ExtractLabels(nmd)
+ labelKeys := make([]string, 0, len(labels))
+ for k := range labels {
+ labelKeys = append(labelKeys, k)
+ }
+ sort.Strings(labelKeys)
+ for _, labelKey := range labelKeys {
+ rows = append(rows, MetadataRow{ID: "label_" + labelKey, Label: fmt.Sprintf("Label %q", labelKey), Value: labels[labelKey]})
+ }
+ return rows
+}
diff --git a/render/detailed/metadata_test.go b/render/detailed/metadata_test.go
new file mode 100644
index 0000000000..9d2622a062
--- /dev/null
+++ b/render/detailed/metadata_test.go
@@ -0,0 +1,53 @@
+package detailed_test
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/weaveworks/scope/probe/docker"
+ "github.com/weaveworks/scope/render/detailed"
+ "github.com/weaveworks/scope/report"
+ "github.com/weaveworks/scope/test"
+ "github.com/weaveworks/scope/test/fixture"
+)
+
+func TestNodeMetadata(t *testing.T) {
+ inputs := []struct {
+ name string
+ node report.Node
+ want []detailed.MetadataRow
+ }{
+ {
+ name: "container",
+ node: report.MakeNodeWith(map[string]string{
+ docker.ContainerID: fixture.ClientContainerID,
+ docker.LabelPrefix + "label1": "label1value",
+ }).WithTopology(report.Container).WithSets(report.Sets{
+ docker.ContainerIPs: report.MakeStringSet("10.10.10.0/24", "10.10.10.1/24"),
+ }).WithLatest(docker.ContainerState, fixture.Now, docker.StateRunning),
+ want: []detailed.MetadataRow{
+ {ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID},
+ {ID: docker.ContainerState, Label: "State", Value: "running"},
+ {ID: docker.ContainerIPs, Label: "IPs", Value: "10.10.10.0/24, 10.10.10.1/24"},
+ {
+ ID: "label_label1",
+ Label: "Label \"label1\"",
+ Value: "label1value",
+ },
+ },
+ },
+ {
+ name: "unknown topology",
+ node: report.MakeNodeWith(map[string]string{
+ docker.ContainerID: fixture.ClientContainerID,
+ }).WithTopology("foobar").WithID(fixture.ClientContainerNodeID),
+ want: nil,
+ },
+ }
+ for _, input := range inputs {
+ have := detailed.NodeMetadata(input.node)
+ if !reflect.DeepEqual(input.want, have) {
+ t.Errorf("%s: %s", input.name, test.Diff(input.want, have))
+ }
+ }
+}
diff --git a/render/detailed/metrics.go b/render/detailed/metrics.go
new file mode 100644
index 0000000000..c023f3862c
--- /dev/null
+++ b/render/detailed/metrics.go
@@ -0,0 +1,121 @@
+package detailed
+
+import (
+ "encoding/json"
+ "math"
+
+ "github.com/weaveworks/scope/probe/docker"
+ "github.com/weaveworks/scope/probe/host"
+ "github.com/weaveworks/scope/probe/process"
+ "github.com/weaveworks/scope/report"
+)
+
+const (
+ defaultFormat = ""
+ filesizeFormat = "filesize"
+ percentFormat = "percent"
+)
+
+var (
+ processNodeMetrics = renderMetrics(
+ MetricRow{ID: process.CPUUsage, Label: "CPU", Format: percentFormat},
+ MetricRow{ID: process.MemoryUsage, Label: "Memory", Format: filesizeFormat},
+ )
+ containerNodeMetrics = renderMetrics(
+ MetricRow{ID: docker.CPUTotalUsage, Label: "CPU", Format: percentFormat},
+ MetricRow{ID: docker.MemoryUsage, Label: "Memory", Format: filesizeFormat},
+ )
+ hostNodeMetrics = renderMetrics(
+ MetricRow{ID: host.CPUUsage, Label: "CPU", Format: percentFormat},
+ MetricRow{ID: host.MemUsage, Label: "Memory", Format: filesizeFormat},
+ MetricRow{ID: host.Load1, Label: "Load (1m)", Format: defaultFormat, Group: "load"},
+ MetricRow{ID: host.Load5, Label: "Load (5m)", Format: defaultFormat, Group: "load"},
+ MetricRow{ID: host.Load15, Label: "Load (15m)", Format: defaultFormat, Group: "load"},
+ )
+)
+
+// MetricRow is a tuple of data used to render a metric as a sparkline and
+// accoutrements.
+type MetricRow struct {
+ ID string
+ Label string
+ Format string
+ Group string
+ Value float64
+ Metric *report.Metric
+}
+
+// Copy returns a value copy of the MetricRow
+func (m MetricRow) Copy() MetricRow {
+ row := MetricRow{
+ ID: m.ID,
+ Label: m.Label,
+ Format: m.Format,
+ Group: m.Group,
+ Value: m.Value,
+ }
+ if m.Metric != nil {
+ var metric = m.Metric.Copy()
+ row.Metric = &metric
+ }
+ return row
+}
+
+// MarshalJSON marshals this MetricRow to json. It takes the basic Metric
+// rendering, then adds some row-specific fields.
+func (m MetricRow) MarshalJSON() ([]byte, error) {
+ return json.Marshal(struct {
+ ID string `json:"id"`
+ Label string `json:"label"`
+ Format string `json:"format,omitempty"`
+ Group string `json:"group,omitempty"`
+ Value float64 `json:"value"`
+ report.WireMetrics
+ }{
+ ID: m.ID,
+ Label: m.Label,
+ Format: m.Format,
+ Group: m.Group,
+ Value: m.Value,
+ WireMetrics: m.Metric.ToIntermediate(),
+ })
+}
+
+// NodeMetrics produces a table (to be consumed directly by the UI) based on
+// an origin ID, which is (optimistically) a node ID in one of our topologies.
+func NodeMetrics(n report.Node) []MetricRow {
+ renderers := map[string]func(report.Node) []MetricRow{
+ report.Process: processNodeMetrics,
+ report.Container: containerNodeMetrics,
+ report.Host: hostNodeMetrics,
+ }
+ if renderer, ok := renderers[n.Topology]; ok {
+ return renderer(n)
+ }
+ return nil
+}
+
+func renderMetrics(templates ...MetricRow) func(report.Node) []MetricRow {
+ return func(n report.Node) []MetricRow {
+ rows := []MetricRow{}
+ for _, template := range templates {
+ metric, ok := n.Metrics[template.ID]
+ if !ok {
+ continue
+ }
+ t := template.Copy()
+ if s := metric.LastSample(); s != nil {
+ t.Value = toFixed(s.Value, 2)
+ }
+ t.Metric = &metric
+ rows = append(rows, t)
+ }
+ return rows
+ }
+}
+
+// toFixed truncates decimals of float64 down to specified precision
+func toFixed(num float64, precision int) float64 {
+ output := math.Pow(10, float64(precision))
+ return float64(int64(num*output)) / output
+}
diff --git a/render/detailed/metrics_test.go b/render/detailed/metrics_test.go
new file mode 100644
index 0000000000..a199ee0ca4
--- /dev/null
+++ b/render/detailed/metrics_test.go
@@ -0,0 +1,121 @@
+package detailed_test
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/weaveworks/scope/probe/docker"
+ "github.com/weaveworks/scope/probe/host"
+ "github.com/weaveworks/scope/probe/process"
+ "github.com/weaveworks/scope/render/detailed"
+ "github.com/weaveworks/scope/report"
+ "github.com/weaveworks/scope/test"
+ "github.com/weaveworks/scope/test/fixture"
+)
+
+func TestNodeMetrics(t *testing.T) {
+ inputs := []struct {
+ name string
+ node report.Node
+ want []detailed.MetricRow
+ }{
+ {
+ name: "process",
+ node: fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
+ want: []detailed.MetricRow{
+ {
+ ID: process.CPUUsage,
+ Label: "CPU",
+ Format: "percent",
+ Group: "",
+ Value: 0.01,
+ Metric: &fixture.CPUMetric,
+ },
+ {
+ ID: process.MemoryUsage,
+ Label: "Memory",
+ Format: "filesize",
+ Group: "",
+ Value: 0.01,
+ Metric: &fixture.MemoryMetric,
+ },
+ },
+ },
+ {
+ name: "container",
+ node: fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
+ want: []detailed.MetricRow{
+ {
+ ID: docker.CPUTotalUsage,
+ Label: "CPU",
+ Format: "percent",
+ Group: "",
+ Value: 0.01,
+ Metric: &fixture.CPUMetric,
+ },
+ {
+ ID: docker.MemoryUsage,
+ Label: "Memory",
+ Format: "filesize",
+ Group: "",
+ Value: 0.01,
+ Metric: &fixture.MemoryMetric,
+ },
+ },
+ },
+ {
+ name: "host",
+ node: fixture.Report.Host.Nodes[fixture.ClientHostNodeID],
+ want: []detailed.MetricRow{
+ {
+ ID: host.CPUUsage,
+ Label: "CPU",
+ Format: "percent",
+ Group: "",
+ Value: 0.01,
+ Metric: &fixture.CPUMetric,
+ },
+ {
+ ID: host.MemUsage,
+ Label: "Memory",
+ Format: "filesize",
+ Group: "",
+ Value: 0.01,
+ Metric: &fixture.MemoryMetric,
+ },
+ {
+ ID: host.Load1,
+ Label: "Load (1m)",
+ Group: "load",
+ Value: 0.01,
+ Metric: &fixture.LoadMetric,
+ },
+ {
+ ID: host.Load5,
+ Label: "Load (5m)",
+ Group: "load",
+ Value: 0.01,
+ Metric: &fixture.LoadMetric,
+ },
+ {
+ ID: host.Load15,
+ Label: "Load (15m)",
+ Group: "load",
+ Value: 0.01,
+ Metric: &fixture.LoadMetric,
+ },
+ },
+ },
+ {
+ name: "unknown topology",
+ node: report.MakeNode().WithTopology("foobar").WithID(fixture.ClientContainerNodeID),
+ want: nil,
+ },
+ }
+ for _, input := range inputs {
+ have := detailed.NodeMetrics(input.node)
+ if !reflect.DeepEqual(input.want, have) {
+ t.Errorf("%s: %s", input.name, test.Diff(input.want, have))
+ }
+ }
+}
diff --git a/render/detailed/node.go b/render/detailed/node.go
new file mode 100644
index 0000000000..5d9836ef1b
--- /dev/null
+++ b/render/detailed/node.go
@@ -0,0 +1,201 @@
+package detailed
+
+import (
+ "sort"
+
+ "github.com/weaveworks/scope/probe/docker"
+ "github.com/weaveworks/scope/probe/host"
+ "github.com/weaveworks/scope/probe/kubernetes"
+ "github.com/weaveworks/scope/probe/process"
+ "github.com/weaveworks/scope/render"
+ "github.com/weaveworks/scope/report"
+)
+
+// Node is the data type that's yielded to the JavaScript layer when
+// we want deep information about an individual node.
+type Node struct {
+ NodeSummary
+ Rank string `json:"rank,omitempty"`
+ Pseudo bool `json:"pseudo,omitempty"`
+ Controls []ControlInstance `json:"controls"`
+ Children []NodeSummaryGroup `json:"children,omitempty"`
+ Parents []Parent `json:"parents,omitempty"`
+}
+
+// Parent is the information needed to build a link to the parent of a Node.
+type Parent struct {
+ ID string `json:"id"`
+ Label string `json:"label"`
+ TopologyID string `json:"topologyId"`
+}
+
+// ControlInstance contains a control description, and all the info
+// needed to execute it.
+type ControlInstance struct {
+ ProbeID string `json:"probeId"`
+ NodeID string `json:"nodeId"`
+ report.Control
+}
+
+// MakeNode transforms a renderable node to a detailed node. It uses
+// aggregate metadata, plus the set of origin node IDs, to produce tables.
+func MakeNode(r report.Report, n render.RenderableNode) Node {
+ summary, _ := MakeNodeSummary(n.Node)
+ summary.ID = n.ID
+ summary.Label = n.LabelMajor
+ return Node{
+ NodeSummary: summary,
+ Rank: n.Rank,
+ Pseudo: n.Pseudo,
+ Controls: controls(r, n),
+ Children: children(n),
+ Parents: parents(r, n),
+ }
+}
+
+func controlsFor(topology report.Topology, nodeID string) []ControlInstance {
+ result := []ControlInstance{}
+ node, ok := topology.Nodes[nodeID]
+ if !ok {
+ return result
+ }
+
+ for _, id := range node.Controls.Controls {
+ if control, ok := topology.Controls[id]; ok {
+ result = append(result, ControlInstance{
+ ProbeID: node.Metadata[report.ProbeID],
+ NodeID: nodeID,
+ Control: control,
+ })
+ }
+ }
+ return result
+}
+
+func controls(r report.Report, n render.RenderableNode) []ControlInstance {
+ if _, ok := r.Process.Nodes[n.ControlNode]; ok {
+ return controlsFor(r.Process, n.ControlNode)
+ } else if _, ok := r.Container.Nodes[n.ControlNode]; ok {
+ return controlsFor(r.Container, n.ControlNode)
+ } else if _, ok := r.ContainerImage.Nodes[n.ControlNode]; ok {
+ return controlsFor(r.ContainerImage, n.ControlNode)
+ } else if _, ok := r.Host.Nodes[n.ControlNode]; ok {
+ return controlsFor(r.Host, n.ControlNode)
+ }
+ return []ControlInstance{}
+}
+
+var (
+ nodeSummaryGroupSpecs = []struct {
+ topologyID string
+ NodeSummaryGroup
+ }{
+ {report.Host, NodeSummaryGroup{TopologyID: "hosts", Label: "Hosts", Columns: []string{host.CPUUsage, host.MemUsage}}},
+ {report.Pod, NodeSummaryGroup{TopologyID: "pods", Label: "Pods", Columns: []string{}}},
+ {report.ContainerImage, NodeSummaryGroup{TopologyID: "containers-by-image", Label: "Container Images", Columns: []string{}}},
+ {report.Container, NodeSummaryGroup{TopologyID: "containers", Label: "Containers", Columns: []string{docker.CPUTotalUsage, docker.MemoryUsage}}},
+ {report.Process, NodeSummaryGroup{TopologyID: "applications", Label: "Applications", Columns: []string{process.PID, process.CPUUsage, process.MemoryUsage}}},
+ }
+)
+
+func children(n render.RenderableNode) []NodeSummaryGroup {
+ summaries := map[string][]NodeSummary{}
+ for _, child := range n.Children {
+ if child.ID == n.ID {
+ continue
+ }
+
+ if summary, ok := MakeNodeSummary(child); ok {
+ summaries[child.Topology] = append(summaries[child.Topology], summary)
+ }
+ }
+
+ nodeSummaryGroups := []NodeSummaryGroup{}
+ for _, spec := range nodeSummaryGroupSpecs {
+ if len(summaries[spec.topologyID]) > 0 {
+ sort.Sort(nodeSummariesByID(summaries[spec.TopologyID]))
+ group := spec.NodeSummaryGroup.Copy()
+ group.Nodes = summaries[spec.topologyID]
+ nodeSummaryGroups = append(nodeSummaryGroups, group)
+ }
+ }
+ return nodeSummaryGroups
+}
+
+// parents renders the parents of this report.Node, which have been aggregated
+// from the probe reports.
+func parents(r report.Report, n render.RenderableNode) (result []Parent) {
+ topologies := map[string]struct {
+ report.Topology
+ render func(report.Node) Parent
+ }{
+ report.Container: {r.Container, containerParent},
+ report.Pod: {r.Pod, podParent},
+ report.Service: {r.Service, serviceParent},
+ report.ContainerImage: {r.ContainerImage, containerImageParent},
+ report.Host: {r.Host, hostParent},
+ }
+ topologyIDs := []string{}
+ for topologyID := range topologies {
+ topologyIDs = append(topologyIDs, topologyID)
+ }
+ sort.Strings(topologyIDs)
+ for _, topologyID := range topologyIDs {
+ t := topologies[topologyID]
+ for _, id := range n.Node.Parents[topologyID] {
+ if topologyID == n.Node.Topology && id == n.ID {
+ continue
+ }
+
+ parent, ok := t.Nodes[id]
+ if !ok {
+ continue
+ }
+
+ result = append(result, t.render(parent))
+ }
+ }
+ return result
+}
+
+func containerParent(n report.Node) Parent {
+ label, _ := render.GetRenderableContainerName(n)
+ return Parent{
+ ID: render.MakeContainerID(n.Metadata[docker.ContainerID]),
+ Label: label,
+ TopologyID: "containers",
+ }
+}
+
+func podParent(n report.Node) Parent {
+ return Parent{
+ ID: render.MakePodID(n.Metadata[kubernetes.PodID]),
+ Label: n.Metadata[kubernetes.PodName],
+ TopologyID: "pods",
+ }
+}
+
+func serviceParent(n report.Node) Parent {
+ return Parent{
+ ID: render.MakeServiceID(n.Metadata[kubernetes.ServiceID]),
+ Label: n.Metadata[kubernetes.ServiceName],
+ TopologyID: "pods-by-service",
+ }
+}
+
+func containerImageParent(n report.Node) Parent {
+ imageName := n.Metadata[docker.ImageName]
+ return Parent{
+ ID: render.MakeContainerImageID(render.ImageNameWithoutVersion(imageName)),
+ Label: imageName,
+ TopologyID: "containers-by-image",
+ }
+}
+
+func hostParent(n report.Node) Parent {
+ return Parent{
+ ID: render.MakeHostID(n.Metadata[host.HostName]),
+ Label: n.Metadata[host.HostName],
+ TopologyID: "hosts",
+ }
+}
diff --git a/render/detailed/node_test.go b/render/detailed/node_test.go
new file mode 100644
index 0000000000..127817fcb0
--- /dev/null
+++ b/render/detailed/node_test.go
@@ -0,0 +1,191 @@
+package detailed_test
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/weaveworks/scope/probe/docker"
+ "github.com/weaveworks/scope/probe/host"
+ "github.com/weaveworks/scope/probe/process"
+ "github.com/weaveworks/scope/render"
+ "github.com/weaveworks/scope/render/detailed"
+ "github.com/weaveworks/scope/test"
+ "github.com/weaveworks/scope/test/fixture"
+)
+
+func TestMakeDetailedHostNode(t *testing.T) {
+ renderableNode := render.HostRenderer.Render(fixture.Report)[render.MakeHostID(fixture.ClientHostID)]
+ have := detailed.MakeNode(fixture.Report, renderableNode)
+
+ containerImageNodeSummary, _ := detailed.MakeNodeSummary(fixture.Report.ContainerImage.Nodes[fixture.ClientContainerImageNodeID])
+ containerNodeSummary, _ := detailed.MakeNodeSummary(fixture.Report.Container.Nodes[fixture.ClientContainerNodeID])
+ process1NodeSummary, _ := detailed.MakeNodeSummary(fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID])
+ process1NodeSummary.Linkable = true
+ process2NodeSummary, _ := detailed.MakeNodeSummary(fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID])
+ process2NodeSummary.Linkable = true
+ want := detailed.Node{
+ NodeSummary: detailed.NodeSummary{
+ ID: render.MakeHostID(fixture.ClientHostID),
+ Label: "client",
+ Linkable: true,
+ Metadata: []detailed.MetadataRow{
+ {
+ ID: "host_name",
+ Label: "Hostname",
+ Value: "client.hostname.com",
+ },
+ {
+ ID: "os",
+ Label: "Operating system",
+ Value: "Linux",
+ },
+ {
+ ID: "local_networks",
+ Label: "Local Networks",
+ Value: "10.10.10.0/24",
+ },
+ },
+ Metrics: []detailed.MetricRow{
+ {
+ ID: host.CPUUsage,
+ Format: "percent",
+ Label: "CPU",
+ Value: 0.01,
+ Metric: &fixture.CPUMetric,
+ },
+ {
+ ID: host.MemUsage,
+ Format: "filesize",
+ Label: "Memory",
+ Value: 0.01,
+ Metric: &fixture.MemoryMetric,
+ },
+ {
+ ID: host.Load1,
+ Group: "load",
+ Label: "Load (1m)",
+ Value: 0.01,
+ Metric: &fixture.LoadMetric,
+ },
+ {
+ ID: host.Load5,
+ Group: "load",
+ Label: "Load (5m)",
+ Value: 0.01,
+ Metric: &fixture.LoadMetric,
+ },
+ {
+ ID: host.Load15,
+ Label: "Load (15m)",
+ Group: "load",
+ Value: 0.01,
+ Metric: &fixture.LoadMetric,
+ },
+ },
+ },
+ Rank: "hostname.com",
+ Pseudo: false,
+ Controls: []detailed.ControlInstance{},
+ Children: []detailed.NodeSummaryGroup{
+ {
+ Label: "Container Images",
+ TopologyID: "containers-by-image",
+ Columns: []string{},
+ Nodes: []detailed.NodeSummary{containerImageNodeSummary},
+ },
+ {
+ Label: "Containers",
+ TopologyID: "containers",
+ Columns: []string{docker.CPUTotalUsage, docker.MemoryUsage},
+ Nodes: []detailed.NodeSummary{containerNodeSummary},
+ },
+ {
+ Label: "Applications",
+ TopologyID: "applications",
+ Columns: []string{process.PID, process.CPUUsage, process.MemoryUsage},
+ Nodes: []detailed.NodeSummary{process1NodeSummary, process2NodeSummary},
+ },
+ },
+ }
+ if !reflect.DeepEqual(want, have) {
+ t.Errorf("%s", test.Diff(want, have))
+ }
+}
+
+func TestMakeDetailedContainerNode(t *testing.T) {
+ id := render.MakeContainerID(fixture.ServerContainerID)
+ renderableNode, ok := render.ContainerRenderer.Render(fixture.Report)[id]
+ if !ok {
+ t.Fatalf("Node not found: %s", id)
+ }
+ have := detailed.MakeNode(fixture.Report, renderableNode)
+ want := detailed.Node{
+ NodeSummary: detailed.NodeSummary{
+ ID: id,
+ Label: "server",
+ Linkable: true,
+ Metadata: []detailed.MetadataRow{
+ {ID: "docker_container_id", Label: "ID", Value: fixture.ServerContainerID},
+ {ID: "docker_image_id", Label: "Image ID", Value: fixture.ServerContainerImageID},
+ {ID: "docker_container_state", Label: "State", Value: "running"},
+ {ID: "label_" + render.AmazonECSContainerNameLabel, Label: fmt.Sprintf(`Label %q`, render.AmazonECSContainerNameLabel), Value: `server`},
+ {ID: "label_foo1", Label: `Label "foo1"`, Value: `bar1`},
+ {ID: "label_foo2", Label: `Label "foo2"`, Value: `bar2`},
+ {ID: "label_io.kubernetes.pod.name", Label: `Label "io.kubernetes.pod.name"`, Value: "ping/pong-b"},
+ },
+ Metrics: []detailed.MetricRow{
+ {
+ ID: docker.CPUTotalUsage,
+ Format: "percent",
+ Label: "CPU",
+ Value: 0.01,
+ Metric: &fixture.CPUMetric,
+ },
+ {
+ ID: docker.MemoryUsage,
+ Format: "filesize",
+ Label: "Memory",
+ Value: 0.01,
+ Metric: &fixture.MemoryMetric,
+ },
+ },
+ },
+ Rank: "imageid456",
+ Pseudo: false,
+ Controls: []detailed.ControlInstance{},
+ Children: []detailed.NodeSummaryGroup{
+ {
+ Label: "Applications",
+ TopologyID: "applications",
+ Columns: []string{process.PID, process.CPUUsage, process.MemoryUsage},
+ Nodes: []detailed.NodeSummary{
+ {
+ ID: fmt.Sprintf("process:%s:%s", "server.hostname.com", fixture.ServerPID),
+ Label: "apache",
+ Linkable: true,
+ Metadata: []detailed.MetadataRow{
+ {ID: process.PID, Label: "PID", Value: fixture.ServerPID},
+ },
+ Metrics: []detailed.MetricRow{},
+ },
+ },
+ },
+ },
+ Parents: []detailed.Parent{
+ {
+ ID: render.MakeContainerImageID(fixture.ServerContainerImageName),
+ Label: fixture.ServerContainerImageName,
+ TopologyID: "containers-by-image",
+ },
+ {
+ ID: render.MakeHostID(fixture.ServerHostName),
+ Label: fixture.ServerHostName,
+ TopologyID: "hosts",
+ },
+ },
+ }
+ if !reflect.DeepEqual(want, have) {
+ t.Errorf("%s", test.Diff(want, have))
+ }
+}
diff --git a/render/detailed/summary.go b/render/detailed/summary.go
new file mode 100644
index 0000000000..b1bc7df8a9
--- /dev/null
+++ b/render/detailed/summary.go
@@ -0,0 +1,140 @@
+package detailed
+
+import (
+ "fmt"
+
+ "github.com/weaveworks/scope/probe/docker"
+ "github.com/weaveworks/scope/probe/host"
+ "github.com/weaveworks/scope/probe/kubernetes"
+ "github.com/weaveworks/scope/probe/process"
+ "github.com/weaveworks/scope/render"
+ "github.com/weaveworks/scope/report"
+)
+
+// NodeSummaryGroup is a topology-typed group of children for a Node.
+type NodeSummaryGroup struct {
+ Label string `json:"label"`
+ Nodes []NodeSummary `json:"nodes"`
+ TopologyID string `json:"topologyId"`
+ Columns []string `json:"columns"`
+}
+
+// Copy returns a value copy of the NodeSummaryGroup
+func (g NodeSummaryGroup) Copy() NodeSummaryGroup {
+ result := NodeSummaryGroup{
+ TopologyID: g.TopologyID,
+ Label: g.Label,
+ Columns: g.Columns,
+ }
+ for _, node := range g.Nodes {
+ result.Nodes = append(result.Nodes, node.Copy())
+ }
+ return result
+}
+
+// NodeSummary is summary information about a child for a Node.
+type NodeSummary struct {
+ ID string `json:"id"`
+ Label string `json:"label"`
+ Linkable bool `json:"linkable"` // Whether this node can be linked-to
+ Metadata []MetadataRow `json:"metadata,omitempty"`
+ Metrics []MetricRow `json:"metrics,omitempty"`
+}
+
+// MakeNodeSummary summarizes a node, if possible.
+func MakeNodeSummary(n report.Node) (NodeSummary, bool) {
+ renderers := map[string]func(report.Node) NodeSummary{
+ "process": processNodeSummary,
+ "container": containerNodeSummary,
+ "container_image": containerImageNodeSummary,
+ "pod": podNodeSummary,
+ "host": hostNodeSummary,
+ }
+ if renderer, ok := renderers[n.Topology]; ok {
+ return renderer(n), true
+ }
+ return NodeSummary{}, false
+}
+
+// Copy returns a value copy of the NodeSummary
+func (n NodeSummary) Copy() NodeSummary {
+ result := NodeSummary{
+ ID: n.ID,
+ Label: n.Label,
+ Linkable: n.Linkable,
+ }
+ for _, row := range n.Metadata {
+ result.Metadata = append(result.Metadata, row.Copy())
+ }
+ for _, row := range n.Metrics {
+ result.Metrics = append(result.Metrics, row.Copy())
+ }
+ return result
+}
+
+func processNodeSummary(nmd report.Node) NodeSummary {
+ var (
+ id string
+ label, nameFound = nmd.Metadata[process.Name]
+ )
+ if pid, ok := nmd.Metadata[process.PID]; ok {
+ if !nameFound {
+ label = fmt.Sprintf("(%s)", pid)
+ }
+ id = render.MakeProcessID(report.ExtractHostID(nmd), pid)
+ }
+ _, isConnected := nmd.Metadata[render.IsConnected]
+ return NodeSummary{
+ ID: id,
+ Label: label,
+ Linkable: isConnected,
+ Metadata: processNodeMetadata(nmd),
+ Metrics: processNodeMetrics(nmd),
+ }
+}
+
+func containerNodeSummary(nmd report.Node) NodeSummary {
+ label, _ := render.GetRenderableContainerName(nmd)
+ return NodeSummary{
+ ID: render.MakeContainerID(nmd.Metadata[docker.ContainerID]),
+ Label: label,
+ Linkable: true,
+ Metadata: containerNodeMetadata(nmd),
+ Metrics: containerNodeMetrics(nmd),
+ }
+}
+
+func containerImageNodeSummary(nmd report.Node) NodeSummary {
+ imageName := nmd.Metadata[docker.ImageName]
+ return NodeSummary{
+ ID: render.MakeContainerImageID(render.ImageNameWithoutVersion(imageName)),
+ Label: imageName,
+ Linkable: true,
+ Metadata: containerImageNodeMetadata(nmd),
+ }
+}
+
+func podNodeSummary(nmd report.Node) NodeSummary {
+ return NodeSummary{
+ ID: render.MakePodID(nmd.Metadata[kubernetes.PodID]),
+ Label: nmd.Metadata[kubernetes.PodName],
+ Linkable: true,
+ Metadata: podNodeMetadata(nmd),
+ }
+}
+
+func hostNodeSummary(nmd report.Node) NodeSummary {
+ return NodeSummary{
+ ID: render.MakeHostID(nmd.Metadata[host.HostName]),
+ Label: nmd.Metadata[host.HostName],
+ Linkable: true,
+ Metadata: hostNodeMetadata(nmd),
+ Metrics: hostNodeMetrics(nmd),
+ }
+}
+
+type nodeSummariesByID []NodeSummary
+
+func (s nodeSummariesByID) Len() int { return len(s) }
+func (s nodeSummariesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s nodeSummariesByID) Less(i, j int) bool { return s[i].ID < s[j].ID }
diff --git a/render/detailed_node.go b/render/detailed_node.go
deleted file mode 100644
index e8516562b1..0000000000
--- a/render/detailed_node.go
+++ /dev/null
@@ -1,572 +0,0 @@
-package render
-
-import (
- "fmt"
- "sort"
- "strconv"
-
- "github.com/weaveworks/scope/probe/docker"
- "github.com/weaveworks/scope/probe/host"
- "github.com/weaveworks/scope/probe/overlay"
- "github.com/weaveworks/scope/probe/process"
- "github.com/weaveworks/scope/report"
-)
-
-const (
- containerImageRank = 4
- containerRank = 3
- processRank = 2
- hostRank = 1
- connectionsRank = 0 // keep connections at the bottom until they are expandable in the UI
-)
-
-// DetailedNode is the data type that's yielded to the JavaScript layer when
-// we want deep information about an individual node.
-type DetailedNode struct {
- ID string `json:"id"`
- LabelMajor string `json:"label_major"`
- LabelMinor string `json:"label_minor,omitempty"`
- Rank string `json:"rank,omitempty"`
- Pseudo bool `json:"pseudo,omitempty"`
- Tables []Table `json:"tables"`
- Controls []ControlInstance `json:"controls"`
-}
-
-// Table is a dataset associated with a node. It will be displayed in the
-// detail panel when a user clicks on a node.
-type Table struct {
- Title string `json:"title"` // e.g. Bandwidth
- Numeric bool `json:"numeric"` // should the major column be right-aligned?
- Rank int `json:"-"` // used to sort tables; not emitted.
- Rows []Row `json:"rows"`
-}
-
-// Row is a single entry in a Table dataset.
-type Row struct {
- Key string `json:"key"` // e.g. Ingress
- ValueMajor string `json:"value_major"` // e.g. 25
- ValueMinor string `json:"value_minor,omitempty"` // e.g. KB/s
- Expandable bool `json:"expandable,omitempty"` // Whether it can be expanded (hidden by default)
- ValueType string `json:"value_type,omitempty"` // e.g. sparkline
- Metric *report.Metric `json:"metric,omitempty"` // e.g. sparkline data samples
-}
-
-// ControlInstance contains a control description, and all the info
-// needed to execute it.
-type ControlInstance struct {
- ProbeID string `json:"probeId"`
- NodeID string `json:"nodeId"`
- report.Control
-}
-
-type sortableRows []Row
-
-func (r sortableRows) Len() int { return len(r) }
-func (r sortableRows) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
-func (r sortableRows) Less(i, j int) bool {
- switch {
- case r[i].Key != r[j].Key:
- return r[i].Key < r[j].Key
-
- case r[i].ValueMajor != r[j].ValueMajor:
- return r[i].ValueMajor < r[j].ValueMajor
-
- default:
- return r[i].ValueMinor < r[j].ValueMinor
- }
-}
-
-type sortableTables []Table
-
-func (t sortableTables) Len() int { return len(t) }
-func (t sortableTables) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
-func (t sortableTables) Less(i, j int) bool { return t[i].Rank > t[j].Rank }
-
-// MakeDetailedNode transforms a renderable node to a detailed node. It uses
-// aggregate metadata, plus the set of origin node IDs, to produce tables.
-func MakeDetailedNode(r report.Report, n RenderableNode) DetailedNode {
- tables := sortableTables{}
-
- // Figure out if multiple hosts/containers are referenced by the renderableNode
- multiContainer, multiHost := getRenderingContext(r, n)
-
- // RenderableNode may be the result of merge operation(s), and so may have
- // multiple origins. The ultimate goal here is to generate tables to view
- // in the UI, so we skip the intermediate representations, but we could
- // add them later.
- connections := []Row{}
- for _, id := range n.Origins {
- if table, ok := OriginTable(r, id, multiHost, multiContainer); ok {
- tables = append(tables, table)
- } else if _, ok := r.Endpoint.Nodes[id]; ok {
- connections = append(connections, connectionDetailsRows(r.Endpoint, id)...)
- } else if _, ok := r.Address.Nodes[id]; ok {
- connections = append(connections, connectionDetailsRows(r.Address, id)...)
- }
- }
-
- if table, ok := connectionsTable(connections, r, n); ok {
- tables = append(tables, table)
- }
-
- // Sort tables by rank
- sort.Sort(tables)
-
- return DetailedNode{
- ID: n.ID,
- LabelMajor: n.LabelMajor,
- LabelMinor: n.LabelMinor,
- Rank: n.Rank,
- Pseudo: n.Pseudo,
- Tables: tables,
- Controls: controls(r, n),
- }
-}
-
-func getRenderingContext(r report.Report, n RenderableNode) (multiContainer, multiHost bool) {
- var (
- originHosts = map[string]struct{}{}
- originContainers = map[string]struct{}{}
- )
- for _, id := range n.Origins {
- for _, topology := range r.Topologies() {
- if nmd, ok := topology.Nodes[id]; ok {
- originHosts[report.ExtractHostID(nmd)] = struct{}{}
- if id, ok := nmd.Metadata[docker.ContainerID]; ok {
- originContainers[id] = struct{}{}
- }
- }
- // Return early if possible
- multiHost = len(originHosts) > 1
- multiContainer = len(originContainers) > 1
- if multiHost && multiContainer {
- return
- }
- }
- }
- return
-}
-
-func connectionsTable(connections []Row, r report.Report, n RenderableNode) (Table, bool) {
- sec := r.Window.Seconds()
- rate := func(u *uint64) (float64, bool) {
- if u == nil {
- return 0.0, false
- }
- if sec <= 0 {
- return 0.0, true
- }
- return float64(*u) / sec, true
- }
- shortenByteRate := func(rate float64) (major, minor string) {
- switch {
- case rate > 1024*1024:
- return fmt.Sprintf("%.2f", rate/1024/1024), "MBps"
- case rate > 1024:
- return fmt.Sprintf("%.1f", rate/1024), "KBps"
- default:
- return fmt.Sprintf("%.0f", rate), "Bps"
- }
- }
-
- rows := []Row{}
- if n.EdgeMetadata.MaxConnCountTCP != nil {
- rows = append(rows, Row{Key: "TCP connections", ValueMajor: strconv.FormatUint(*n.EdgeMetadata.MaxConnCountTCP, 10)})
- }
- if rate, ok := rate(n.EdgeMetadata.EgressPacketCount); ok {
- rows = append(rows, Row{Key: "Egress packet rate", ValueMajor: fmt.Sprintf("%.0f", rate), ValueMinor: "packets/sec"})
- }
- if rate, ok := rate(n.EdgeMetadata.IngressPacketCount); ok {
- rows = append(rows, Row{Key: "Ingress packet rate", ValueMajor: fmt.Sprintf("%.0f", rate), ValueMinor: "packets/sec"})
- }
- if rate, ok := rate(n.EdgeMetadata.EgressByteCount); ok {
- s, unit := shortenByteRate(rate)
- rows = append(rows, Row{Key: "Egress byte rate", ValueMajor: s, ValueMinor: unit})
- }
- if rate, ok := rate(n.EdgeMetadata.IngressByteCount); ok {
- s, unit := shortenByteRate(rate)
- rows = append(rows, Row{Key: "Ingress byte rate", ValueMajor: s, ValueMinor: unit})
- }
- if len(connections) > 0 {
- sort.Sort(sortableRows(connections))
- rows = append(rows, Row{Key: "Client", ValueMajor: "Server", Expandable: true})
- rows = append(rows, connections...)
- }
- if len(rows) > 0 {
- return Table{
- Title: "Connections",
- Numeric: false,
- Rank: connectionsRank,
- Rows: rows,
- }, true
- }
- return Table{}, false
-}
-
-func controlsFor(topology report.Topology, nodeID string) []ControlInstance {
- result := []ControlInstance{}
- node, ok := topology.Nodes[nodeID]
- if !ok {
- return result
- }
-
- for _, id := range node.Controls.Controls {
- if control, ok := topology.Controls[id]; ok {
- result = append(result, ControlInstance{
- ProbeID: node.Metadata[report.ProbeID],
- NodeID: nodeID,
- Control: control,
- })
- }
- }
- return result
-}
-
-func controls(r report.Report, n RenderableNode) []ControlInstance {
- if _, ok := r.Process.Nodes[n.ControlNode]; ok {
- return controlsFor(r.Process, n.ControlNode)
- } else if _, ok := r.Container.Nodes[n.ControlNode]; ok {
- return controlsFor(r.Container, n.ControlNode)
- } else if _, ok := r.ContainerImage.Nodes[n.ControlNode]; ok {
- return controlsFor(r.ContainerImage, n.ControlNode)
- } else if _, ok := r.Host.Nodes[n.ControlNode]; ok {
- return controlsFor(r.Host, n.ControlNode)
- }
- return []ControlInstance{}
-}
-
-// OriginTable produces a table (to be consumed directly by the UI) based on
-// an origin ID, which is (optimistically) a node ID in one of our topologies.
-func OriginTable(r report.Report, originID string, addHostTags bool, addContainerTags bool) (Table, bool) {
- result, show := Table{}, false
- if nmd, ok := r.Process.Nodes[originID]; ok {
- result, show = processOriginTable(nmd, addHostTags, addContainerTags)
- }
- if nmd, ok := r.Container.Nodes[originID]; ok {
- result, show = containerOriginTable(nmd, addHostTags)
- }
- if nmd, ok := r.ContainerImage.Nodes[originID]; ok {
- result, show = containerImageOriginTable(nmd)
- }
- if nmd, ok := r.Host.Nodes[originID]; ok {
- result, show = hostOriginTable(nmd)
- }
- return result, show
-}
-
-func connectionDetailsRows(topology report.Topology, originID string) []Row {
- rows := []Row{}
- labeler := func(nodeID string, sets report.Sets) (string, bool) {
- if _, addr, port, ok := report.ParseEndpointNodeID(nodeID); ok {
- if names, ok := sets["name"]; ok {
- return fmt.Sprintf("%s:%s", names[0], port), true
- }
- return fmt.Sprintf("%s:%s", addr, port), true
- }
- if _, addr, ok := report.ParseAddressNodeID(nodeID); ok {
- return addr, true
- }
- return "", false
- }
- local, ok := labeler(originID, topology.Nodes[originID].Sets)
- if !ok {
- return rows
- }
- // Firstly, collection outgoing connections from this node.
- for _, serverNodeID := range topology.Nodes[originID].Adjacency {
- remote, ok := labeler(serverNodeID, topology.Nodes[serverNodeID].Sets)
- if !ok {
- continue
- }
- rows = append(rows, Row{
- Key: local,
- ValueMajor: remote,
- Expandable: true,
- })
- }
- // Next, scan the topology for incoming connections to this node.
- for clientNodeID, clientNode := range topology.Nodes {
- if clientNodeID == originID {
- continue
- }
- serverNodeIDs := clientNode.Adjacency
- if !serverNodeIDs.Contains(originID) {
- continue
- }
- remote, ok := labeler(clientNodeID, clientNode.Sets)
- if !ok {
- continue
- }
- rows = append(rows, Row{
- Key: remote,
- ValueMajor: local,
- ValueMinor: "",
- Expandable: true,
- })
- }
- return rows
-}
-
-func processOriginTable(nmd report.Node, addHostTag bool, addContainerTag bool) (Table, bool) {
- rows := []Row{}
- for _, tuple := range []struct{ key, human string }{
- {process.PPID, "Parent PID"},
- {process.Cmdline, "Command"},
- {process.Threads, "# Threads"},
- } {
- if val, ok := nmd.Metadata[tuple.key]; ok {
- rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
- }
- }
-
- if containerID, ok := nmd.Metadata[docker.ContainerID]; ok && addContainerTag {
- rows = append([]Row{{Key: "Container ID", ValueMajor: containerID}}, rows...)
- }
-
- if addHostTag {
- rows = append([]Row{{Key: "Host", ValueMajor: report.ExtractHostID(nmd)}}, rows...)
- }
-
- for _, tuple := range []struct {
- key, human string
- fmt formatter
- }{
- {process.CPUUsage, "CPU Usage", formatPercent},
- {process.MemoryUsage, "Memory Usage", formatMemory},
- } {
- if val, ok := nmd.Metrics[tuple.key]; ok {
- rows = append(rows, sparklineRow(tuple.human, val, tuple.fmt))
- }
- }
-
- var (
- title = "Process"
- name, commFound = nmd.Metadata[process.Name]
- pid, pidFound = nmd.Metadata[process.PID]
- )
- if commFound {
- title += ` "` + name + `"`
- }
- if pidFound {
- title += " (" + pid + ")"
- }
- return Table{
- Title: title,
- Numeric: false,
- Rows: rows,
- Rank: processRank,
- }, len(rows) > 0 || commFound || pidFound
-}
-
-type formatter func(report.Metric) (report.Metric, string)
-
-func sparklineRow(human string, metric report.Metric, format formatter) Row {
- if format == nil {
- format = formatDefault
- }
- metric, lastStr := format(metric)
- return Row{Key: human, ValueMajor: lastStr, Metric: &metric, ValueType: "sparkline"}
-}
-
-func formatDefault(m report.Metric) (report.Metric, string) {
- if s := m.LastSample(); s != nil {
- return m, fmt.Sprintf("%0.2f", s.Value)
- }
- return m, ""
-}
-
-func memoryScale(n float64) (string, float64) {
- brackets := []struct {
- human string
- shift uint
- }{
- {"bytes", 0},
- {"KB", 10},
- {"MB", 20},
- {"GB", 30},
- {"TB", 40},
- {"PB", 50},
- }
- for _, bracket := range brackets {
- unit := (1 << bracket.shift)
- if n < float64(unit<<10) {
- return bracket.human, float64(unit)
- }
- }
- return "PB", float64(1 << 50)
-}
-
-func formatMemory(m report.Metric) (report.Metric, string) {
- s := m.LastSample()
- if s == nil {
- return m, ""
- }
- human, divisor := memoryScale(s.Value)
- return m.Div(divisor), fmt.Sprintf("%0.2f %s", s.Value/divisor, human)
-}
-
-func formatPercent(m report.Metric) (report.Metric, string) {
- if s := m.LastSample(); s != nil {
- return m, fmt.Sprintf("%0.2f%%", s.Value)
- }
- return m, ""
-}
-
-func containerOriginTable(nmd report.Node, addHostTag bool) (Table, bool) {
- rows := []Row{}
- for _, tuple := range []struct{ key, human string }{
- {docker.ContainerState, "State"},
- } {
- if val, ok := nmd.Latest.Lookup(tuple.key); ok && val != "" {
- rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
- }
- }
-
- for _, tuple := range []struct{ key, human string }{
- {docker.ContainerID, "ID"},
- {docker.ImageID, "Image ID"},
- {docker.ContainerPorts, "Ports"},
- {docker.ContainerCreated, "Created"},
- {docker.ContainerCommand, "Command"},
- {overlay.WeaveMACAddress, "Weave MAC"},
- {overlay.WeaveDNSHostname, "Weave DNS Hostname"},
- } {
- if val, ok := nmd.Metadata[tuple.key]; ok && val != "" {
- rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
- }
- }
-
- for _, ip := range docker.ExtractContainerIPs(nmd) {
- rows = append(rows, Row{Key: "IP Address", ValueMajor: ip, ValueMinor: ""})
- }
- rows = append(rows, getDockerLabelRows(nmd)...)
-
- if addHostTag {
- rows = append([]Row{{Key: "Host", ValueMajor: report.ExtractHostID(nmd)}}, rows...)
- }
-
- if val, ok := nmd.Metrics[docker.MemoryUsage]; ok {
- rows = append(rows, sparklineRow("Memory Usage", val, formatMemory))
- }
- if val, ok := nmd.Metrics[docker.CPUTotalUsage]; ok {
- rows = append(rows, sparklineRow("CPU Usage", val, formatPercent))
- }
-
- var (
- title = "Container"
- name, nameFound = GetRenderableContainerName(nmd)
- )
- if nameFound {
- title += ` "` + name + `"`
- }
-
- return Table{
- Title: title,
- Numeric: false,
- Rows: rows,
- Rank: containerRank,
- }, len(rows) > 0 || nameFound
-}
-
-func containerImageOriginTable(nmd report.Node) (Table, bool) {
- rows := []Row{}
- for _, tuple := range []struct{ key, human string }{
- {docker.ImageID, "Image ID"},
- } {
- if val, ok := nmd.Metadata[tuple.key]; ok {
- rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
- }
- }
- rows = append(rows, getDockerLabelRows(nmd)...)
- title := "Container Image"
- var (
- nameFound bool
- name string
- )
- if name, nameFound = nmd.Metadata[docker.ImageName]; nameFound {
- title += ` "` + name + `"`
- }
- return Table{
- Title: title,
- Numeric: false,
- Rows: rows,
- Rank: containerImageRank,
- }, len(rows) > 0 || nameFound
-}
-
-func getDockerLabelRows(nmd report.Node) []Row {
- rows := []Row{}
- // Add labels in alphabetical order
- labels := docker.ExtractLabels(nmd)
- labelKeys := make([]string, 0, len(labels))
- for k := range labels {
- labelKeys = append(labelKeys, k)
- }
- sort.Strings(labelKeys)
- for _, labelKey := range labelKeys {
- rows = append(rows, Row{Key: fmt.Sprintf("Label %q", labelKey), ValueMajor: labels[labelKey]})
- }
- return rows
-}
-
-func hostOriginTable(nmd report.Node) (Table, bool) {
- // Ensure that all metrics have the same max
- maxLoad := 0.0
- for _, key := range []string{host.Load1, host.Load5, host.Load15} {
- if metric, ok := nmd.Metrics[key]; ok {
- if metric.Len() == 0 {
- continue
- }
- if metric.Max > maxLoad {
- maxLoad = metric.Max
- }
- }
- }
-
- rows := []Row{}
- for _, tuple := range []struct{ key, human string }{
- {host.Load1, "Load (1m)"},
- {host.Load5, "Load (5m)"},
- {host.Load15, "Load (15m)"},
- } {
- if val, ok := nmd.Metrics[tuple.key]; ok {
- val.Max = maxLoad
- rows = append(rows, sparklineRow(tuple.human, val, nil))
- }
- }
- for _, tuple := range []struct {
- key, human string
- fmt formatter
- }{
- {host.CPUUsage, "CPU Usage", formatPercent},
- {host.MemUsage, "Memory Usage", formatMemory},
- } {
- if val, ok := nmd.Metrics[tuple.key]; ok {
- rows = append(rows, sparklineRow(tuple.human, val, tuple.fmt))
- }
- }
- for _, tuple := range []struct{ key, human string }{
- {host.OS, "Operating system"},
- {host.KernelVersion, "Kernel version"},
- {host.Uptime, "Uptime"},
- } {
- if val, ok := nmd.Metadata[tuple.key]; ok {
- rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
- }
- }
-
- title := "Host"
- var (
- name string
- foundName bool
- )
- if name, foundName = nmd.Metadata[host.HostName]; foundName {
- title += ` "` + name + `"`
- }
- return Table{
- Title: title,
- Numeric: false,
- Rows: rows,
- Rank: hostRank,
- }, len(rows) > 0 || foundName
-}
diff --git a/render/detailed_node_test.go b/render/detailed_node_test.go
deleted file mode 100644
index fa31f411e6..0000000000
--- a/render/detailed_node_test.go
+++ /dev/null
@@ -1,249 +0,0 @@
-package render_test
-
-import (
- "fmt"
- "reflect"
- "testing"
-
- "github.com/weaveworks/scope/render"
- "github.com/weaveworks/scope/test"
- "github.com/weaveworks/scope/test/fixture"
-)
-
-func TestOriginTable(t *testing.T) {
- if _, ok := render.OriginTable(fixture.Report, "not-found", false, false); ok {
- t.Errorf("unknown origin ID gave unexpected success")
- }
- for originID, want := range map[string]render.Table{
- fixture.ServerProcessNodeID: {
- Title: fmt.Sprintf(`Process "apache" (%s)`, fixture.ServerPID),
- Numeric: false,
- Rank: 2,
- Rows: []render.Row{},
- },
- fixture.ServerHostNodeID: {
- Title: fmt.Sprintf("Host %q", fixture.ServerHostName),
- Numeric: false,
- Rank: 1,
- Rows: []render.Row{
- {Key: "Load (1m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
- {Key: "Load (5m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
- {Key: "Load (15m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
- {Key: "Operating system", ValueMajor: "Linux"},
- },
- },
- } {
- have, ok := render.OriginTable(fixture.Report, originID, false, false)
- if !ok {
- t.Errorf("%q: not OK", originID)
- continue
- }
- if !reflect.DeepEqual(want, have) {
- t.Errorf("%q: %s", originID, test.Diff(want, have))
- }
- }
-
- // Test host/container tags
- for originID, want := range map[string]render.Table{
- fixture.ServerProcessNodeID: {
- Title: fmt.Sprintf(`Process "apache" (%s)`, fixture.ServerPID),
- Numeric: false,
- Rank: 2,
- Rows: []render.Row{
- {Key: "Host", ValueMajor: fixture.ServerHostID},
- {Key: "Container ID", ValueMajor: fixture.ServerContainerID},
- },
- },
- fixture.ServerContainerNodeID: {
- Title: `Container "server"`,
- Numeric: false,
- Rank: 3,
- Rows: []render.Row{
- {Key: "Host", ValueMajor: fixture.ServerHostID},
- {Key: "State", ValueMajor: "running"},
- {Key: "ID", ValueMajor: fixture.ServerContainerID},
- {Key: "Image ID", ValueMajor: fixture.ServerContainerImageID},
- {Key: fmt.Sprintf(`Label %q`, render.AmazonECSContainerNameLabel), ValueMajor: `server`},
- {Key: `Label "foo1"`, ValueMajor: `bar1`},
- {Key: `Label "foo2"`, ValueMajor: `bar2`},
- {Key: `Label "io.kubernetes.pod.name"`, ValueMajor: "ping/pong-b"},
- },
- },
- } {
- have, ok := render.OriginTable(fixture.Report, originID, true, true)
- if !ok {
- t.Errorf("%q: not OK", originID)
- continue
- }
- if !reflect.DeepEqual(want, have) {
- t.Errorf("%q: %s", originID, test.Diff(want, have))
- }
- }
-}
-
-func TestMakeDetailedHostNode(t *testing.T) {
- renderableNode := render.HostRenderer.Render(fixture.Report)[render.MakeHostID(fixture.ClientHostID)]
- have := render.MakeDetailedNode(fixture.Report, renderableNode)
- want := render.DetailedNode{
- ID: render.MakeHostID(fixture.ClientHostID),
- LabelMajor: "client",
- LabelMinor: "hostname.com",
- Rank: "hostname.com",
- Pseudo: false,
- Controls: []render.ControlInstance{},
- Tables: []render.Table{
- {
- Title: fmt.Sprintf("Host %q", fixture.ClientHostName),
- Numeric: false,
- Rank: 1,
- Rows: []render.Row{
- {
- Key: "Load (1m)",
- ValueMajor: "0.01",
- Metric: &fixture.LoadMetric,
- ValueType: "sparkline",
- },
- {
- Key: "Load (5m)",
- ValueMajor: "0.01",
- Metric: &fixture.LoadMetric,
- ValueType: "sparkline",
- },
- {
- Key: "Load (15m)",
- ValueMajor: "0.01",
- Metric: &fixture.LoadMetric,
- ValueType: "sparkline",
- },
- {
- Key: "Operating system",
- ValueMajor: "Linux",
- },
- },
- },
- {
- Title: "Connections",
- Numeric: false,
- Rank: 0,
- Rows: []render.Row{
- {
- Key: "TCP connections",
- ValueMajor: "3",
- },
- {
- Key: "Client",
- ValueMajor: "Server",
- Expandable: true,
- },
- {
- Key: "10.10.10.20",
- ValueMajor: "192.168.1.1",
- Expandable: true,
- },
- },
- },
- },
- }
- if !reflect.DeepEqual(want, have) {
- t.Errorf("%s", test.Diff(want, have))
- }
-}
-
-func TestMakeDetailedContainerNode(t *testing.T) {
- renderableNode := render.ContainerRenderer.Render(fixture.Report)[fixture.ServerContainerID]
- have := render.MakeDetailedNode(fixture.Report, renderableNode)
- want := render.DetailedNode{
- ID: fixture.ServerContainerID,
- LabelMajor: "server",
- LabelMinor: fixture.ServerHostName,
- Rank: "imageid456",
- Pseudo: false,
- Controls: []render.ControlInstance{},
- Tables: []render.Table{
- {
- Title: `Container Image "image/server"`,
- Numeric: false,
- Rank: 4,
- Rows: []render.Row{
- {Key: "Image ID", ValueMajor: fixture.ServerContainerImageID},
- {Key: `Label "foo1"`, ValueMajor: `bar1`},
- {Key: `Label "foo2"`, ValueMajor: `bar2`},
- },
- },
- {
- Title: `Container "server"`,
- Numeric: false,
- Rank: 3,
- Rows: []render.Row{
- {Key: "State", ValueMajor: "running"},
- {Key: "ID", ValueMajor: fixture.ServerContainerID},
- {Key: "Image ID", ValueMajor: fixture.ServerContainerImageID},
- {Key: fmt.Sprintf(`Label %q`, render.AmazonECSContainerNameLabel), ValueMajor: `server`},
- {Key: `Label "foo1"`, ValueMajor: `bar1`},
- {Key: `Label "foo2"`, ValueMajor: `bar2`},
- {Key: `Label "io.kubernetes.pod.name"`, ValueMajor: "ping/pong-b"},
- },
- },
- {
- Title: fmt.Sprintf(`Process "apache" (%s)`, fixture.ServerPID),
- Numeric: false,
- Rank: 2,
- Rows: []render.Row{},
- },
- {
- Title: fmt.Sprintf("Host %q", fixture.ServerHostName),
- Numeric: false,
- Rank: 1,
- Rows: []render.Row{
- {Key: "Load (1m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
- {Key: "Load (5m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
- {Key: "Load (15m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
- {Key: "Operating system", ValueMajor: "Linux"},
- },
- },
- {
- Title: "Connections",
- Numeric: false,
- Rank: 0,
- Rows: []render.Row{
- {Key: "Ingress packet rate", ValueMajor: "105", ValueMinor: "packets/sec"},
- {Key: "Ingress byte rate", ValueMajor: "1.0", ValueMinor: "KBps"},
- {Key: "Client", ValueMajor: "Server", Expandable: true},
- {
- Key: fmt.Sprintf("%s:%s", fixture.UnknownClient1IP, fixture.UnknownClient1Port),
- ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
- Expandable: true,
- },
- {
- Key: fmt.Sprintf("%s:%s", fixture.UnknownClient2IP, fixture.UnknownClient2Port),
- ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
- Expandable: true,
- },
- {
- Key: fmt.Sprintf("%s:%s", fixture.UnknownClient3IP, fixture.UnknownClient3Port),
- ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
- Expandable: true,
- },
- {
- Key: fmt.Sprintf("%s:%s", fixture.ClientIP, fixture.ClientPort54001),
- ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
- Expandable: true,
- },
- {
- Key: fmt.Sprintf("%s:%s", fixture.ClientIP, fixture.ClientPort54002),
- ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
- Expandable: true,
- },
- {
- Key: fmt.Sprintf("%s:%s", fixture.RandomClientIP, fixture.RandomClientPort),
- ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
- Expandable: true,
- },
- },
- },
- },
- }
- if !reflect.DeepEqual(want, have) {
- t.Errorf("%s", test.Diff(want, have))
- }
-}
diff --git a/render/expected/expected.go b/render/expected/expected.go
index a863d657da..b4946d7a93 100644
--- a/render/expected/expected.go
+++ b/render/expected/expected.go
@@ -23,10 +23,6 @@ var (
EgressPacketCount: newu64(70),
EgressByteCount: newu64(700),
},
- Origins: report.MakeIDList(
- fixture.UnknownClient1NodeID,
- fixture.UnknownClient2NodeID,
- ),
}
}
unknownPseudoNode2 = func(adjacent string) render.RenderableNode {
@@ -39,9 +35,6 @@ var (
EgressPacketCount: newu64(50),
EgressByteCount: newu64(500),
},
- Origins: report.MakeIDList(
- fixture.UnknownClient3NodeID,
- ),
}
}
theInternetNode = func(adjacent string) render.RenderableNode {
@@ -54,10 +47,6 @@ var (
EgressPacketCount: newu64(60),
EgressByteCount: newu64(600),
},
- Origins: report.MakeIDList(
- fixture.RandomClientNodeID,
- fixture.GoogleEndpointNodeID,
- ),
}
}
ClientProcess1ID = render.MakeProcessID(fixture.ClientHostID, fixture.Client1PID)
@@ -72,12 +61,7 @@ var (
LabelMinor: fmt.Sprintf("%s (%s)", fixture.ClientHostID, fixture.Client1PID),
Rank: fixture.Client1Name,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.Client54001NodeID,
- fixture.ClientProcess1NodeID,
- fixture.ClientHostNodeID,
- ),
- Node: report.MakeNode().WithAdjacent(ServerProcessID),
+ Node: report.MakeNode().WithAdjacent(ServerProcessID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(10),
EgressByteCount: newu64(100),
@@ -89,12 +73,7 @@ var (
LabelMinor: fmt.Sprintf("%s (%s)", fixture.ClientHostID, fixture.Client2PID),
Rank: fixture.Client2Name,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.Client54002NodeID,
- fixture.ClientProcess2NodeID,
- fixture.ClientHostNodeID,
- ),
- Node: report.MakeNode().WithAdjacent(ServerProcessID),
+ Node: report.MakeNode().WithAdjacent(ServerProcessID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(20),
EgressByteCount: newu64(200),
@@ -106,28 +85,18 @@ var (
LabelMinor: fmt.Sprintf("%s (%s)", fixture.ServerHostID, fixture.ServerPID),
Rank: fixture.ServerName,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.Server80NodeID,
- fixture.ServerProcessNodeID,
- fixture.ServerHostNodeID,
- ),
- Node: report.MakeNode(),
+ Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
IngressPacketCount: newu64(210),
IngressByteCount: newu64(2100),
},
},
nonContainerProcessID: {
- ID: nonContainerProcessID,
- LabelMajor: fixture.NonContainerName,
- LabelMinor: fmt.Sprintf("%s (%s)", fixture.ServerHostID, fixture.NonContainerPID),
- Rank: fixture.NonContainerName,
- Pseudo: false,
- Origins: report.MakeIDList(
- fixture.NonContainerProcessNodeID,
- fixture.ServerHostNodeID,
- fixture.NonContainerNodeID,
- ),
+ ID: nonContainerProcessID,
+ LabelMajor: fixture.NonContainerName,
+ LabelMinor: fmt.Sprintf("%s (%s)", fixture.ServerHostID, fixture.NonContainerPID),
+ Rank: fixture.NonContainerName,
+ Pseudo: false,
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
},
@@ -136,6 +105,10 @@ var (
render.TheInternetID: theInternetNode(ServerProcessID),
}).Prune()
+ ServerProcessRenderedID = render.MakeProcessID(fixture.ServerHostID, fixture.ServerPID)
+ ClientProcess1RenderedID = render.MakeProcessID(fixture.ClientHostID, fixture.Client1PID)
+ ClientProcess2RenderedID = render.MakeProcessID(fixture.ClientHostID, fixture.Client2PID)
+
RenderedProcessNames = (render.RenderableNodes{
fixture.Client1Name: {
ID: fixture.Client1Name,
@@ -143,12 +116,9 @@ var (
LabelMinor: "2 processes",
Rank: fixture.Client1Name,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.Client54001NodeID,
- fixture.Client54002NodeID,
- fixture.ClientProcess1NodeID,
- fixture.ClientProcess2NodeID,
- fixture.ClientHostNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
+ fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
),
Node: report.MakeNode().WithAdjacent(fixture.ServerName),
EdgeMetadata: report.EdgeMetadata{
@@ -162,10 +132,8 @@ var (
LabelMinor: "1 process",
Rank: fixture.ServerName,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.Server80NodeID,
- fixture.ServerProcessNodeID,
- fixture.ServerHostNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
@@ -179,10 +147,8 @@ var (
LabelMinor: "1 process",
Rank: fixture.NonContainerName,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.NonContainerProcessNodeID,
- fixture.ServerHostNodeID,
- fixture.NonContainerNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID],
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
@@ -192,41 +158,35 @@ var (
render.TheInternetID: theInternetNode(fixture.ServerName),
}).Prune()
+ ServerContainerRenderedID = render.MakeContainerID(fixture.ServerContainerID)
+ ClientContainerRenderedID = render.MakeContainerID(fixture.ClientContainerID)
+
RenderedContainers = (render.RenderableNodes{
- fixture.ClientContainerID: {
- ID: fixture.ClientContainerID,
+ ClientContainerRenderedID: {
+ ID: ClientContainerRenderedID,
LabelMajor: "client",
LabelMinor: fixture.ClientHostName,
Rank: fixture.ClientContainerImageName,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.ClientContainerImageNodeID,
- fixture.ClientContainerNodeID,
- fixture.Client54001NodeID,
- fixture.Client54002NodeID,
- fixture.ClientProcess1NodeID,
- fixture.ClientProcess2NodeID,
- fixture.ClientHostNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
+ fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
),
- Node: report.MakeNode().WithAdjacent(fixture.ServerContainerID),
+ Node: report.MakeNode().WithAdjacent(ServerContainerRenderedID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
},
ControlNode: fixture.ClientContainerNodeID,
},
- fixture.ServerContainerID: {
- ID: fixture.ServerContainerID,
+ ServerContainerRenderedID: {
+ ID: ServerContainerRenderedID,
LabelMajor: "server",
LabelMinor: fixture.ServerHostName,
Rank: fixture.ServerContainerImageName,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.ServerContainerImageNodeID,
- fixture.ServerContainerNodeID,
- fixture.Server80NodeID,
- fixture.ServerProcessNodeID,
- fixture.ServerHostNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
@@ -241,51 +201,46 @@ var (
LabelMinor: fixture.ServerHostName,
Rank: "",
Pseudo: true,
- Origins: report.MakeIDList(
- fixture.NonContainerProcessNodeID,
- fixture.ServerHostNodeID,
- fixture.NonContainerNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID],
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
},
- render.TheInternetID: theInternetNode(fixture.ServerContainerID),
+ render.TheInternetID: theInternetNode(ServerContainerRenderedID),
}).Prune()
+ ClientContainerImageRenderedName = render.MakeContainerImageID(fixture.ClientContainerImageName)
+ ServerContainerImageRenderedName = render.MakeContainerImageID(fixture.ServerContainerImageName)
+
RenderedContainerImages = (render.RenderableNodes{
- fixture.ClientContainerImageName: {
- ID: fixture.ClientContainerImageName,
+ ClientContainerImageRenderedName: {
+ ID: ClientContainerImageRenderedName,
LabelMajor: fixture.ClientContainerImageName,
LabelMinor: "1 container",
Rank: fixture.ClientContainerImageName,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.ClientContainerImageNodeID,
- fixture.ClientContainerNodeID,
- fixture.Client54001NodeID,
- fixture.Client54002NodeID,
- fixture.ClientProcess1NodeID,
- fixture.ClientProcess2NodeID,
- fixture.ClientHostNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
+ fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
+ fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
),
- Node: report.MakeNode().WithAdjacent(fixture.ServerContainerImageName),
+ Node: report.MakeNode().WithAdjacent(ServerContainerImageRenderedName),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
},
},
- fixture.ServerContainerImageName: {
- ID: fixture.ServerContainerImageName,
+ ServerContainerImageRenderedName: {
+ ID: ServerContainerImageRenderedName,
LabelMajor: fixture.ServerContainerImageName,
LabelMinor: "1 container",
Rank: fixture.ServerContainerImageName,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.ServerContainerImageNodeID,
- fixture.ServerContainerNodeID,
- fixture.Server80NodeID,
- fixture.ServerProcessNodeID,
- fixture.ServerHostNodeID),
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
+ fixture.Report.Container.Nodes[fixture.ServerContainerNodeID],
+ ),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
IngressPacketCount: newu64(210),
@@ -298,15 +253,13 @@ var (
LabelMinor: fixture.ServerHostName,
Rank: "",
Pseudo: true,
- Origins: report.MakeIDList(
- fixture.NonContainerNodeID,
- fixture.NonContainerProcessNodeID,
- fixture.ServerHostNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID],
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
},
- render.TheInternetID: theInternetNode(fixture.ServerContainerImageName),
+ render.TheInternetID: theInternetNode(ServerContainerImageRenderedName),
}).Prune()
ServerHostRenderedID = render.MakeHostID(fixture.ServerHostID)
@@ -321,13 +274,15 @@ var (
LabelMinor: "hostname.com", // after first .
Rank: "hostname.com",
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.ServerHostNodeID,
- fixture.ServerAddressNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Container.Nodes[fixture.ServerContainerNodeID],
+ fixture.Report.Container.Nodes[fixture.ServerProcessNodeID],
),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
- MaxConnCountTCP: newu64(3),
+ IngressPacketCount: newu64(210),
+ IngressByteCount: newu64(2100),
+ MaxConnCountTCP: newu64(3),
},
},
ClientHostRenderedID: {
@@ -336,13 +291,16 @@ var (
LabelMinor: "hostname.com", // after first .
Rank: "hostname.com",
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.ClientHostNodeID,
- fixture.ClientAddressNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
+ fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
+ fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
),
Node: report.MakeNode().WithAdjacent(ServerHostRenderedID),
EdgeMetadata: report.EdgeMetadata{
- MaxConnCountTCP: newu64(3),
+ EgressPacketCount: newu64(30),
+ EgressByteCount: newu64(300),
+ MaxConnCountTCP: newu64(3),
},
},
pseudoHostID1: {
@@ -351,7 +309,10 @@ var (
Pseudo: true,
Node: report.MakeNode().WithAdjacent(ServerHostRenderedID),
EdgeMetadata: report.EdgeMetadata{},
- Origins: report.MakeIDList(fixture.UnknownAddress1NodeID, fixture.UnknownAddress2NodeID),
+ Children: report.MakeNodeSet(
+ fixture.Report.Container.Nodes[fixture.ServerContainerNodeID],
+ fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
+ ),
},
pseudoHostID2: {
ID: pseudoHostID2,
@@ -359,7 +320,6 @@ var (
Pseudo: true,
Node: report.MakeNode().WithAdjacent(ServerHostRenderedID),
EdgeMetadata: report.EdgeMetadata{},
- Origins: report.MakeIDList(fixture.UnknownAddress3NodeID),
},
render.TheInternetID: {
ID: render.TheInternetID,
@@ -367,46 +327,43 @@ var (
Pseudo: true,
Node: report.MakeNode().WithAdjacent(ServerHostRenderedID),
EdgeMetadata: report.EdgeMetadata{},
- Origins: report.MakeIDList(fixture.RandomAddressNodeID),
},
}).Prune()
+ ClientPodRenderedID = render.MakePodID("ping/pong-a")
+ ServerPodRenderedID = render.MakePodID("ping/pong-b")
+
RenderedPods = (render.RenderableNodes{
- "ping/pong-a": {
- ID: "ping/pong-a",
+ ClientPodRenderedID: {
+ ID: ClientPodRenderedID,
LabelMajor: "pong-a",
LabelMinor: "1 container",
Rank: "ping/pong-a",
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.Client54001NodeID,
- fixture.Client54002NodeID,
- fixture.ClientProcess1NodeID,
- fixture.ClientProcess2NodeID,
- fixture.ClientHostNodeID,
- fixture.ClientContainerNodeID,
- fixture.ClientContainerImageNodeID,
- fixture.ClientPodNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
+ fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
+ fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
+ fixture.Report.ContainerImage.Nodes[fixture.ClientContainerImageNodeID],
+ fixture.Report.Pod.Nodes[fixture.ClientPodNodeID],
),
- Node: report.MakeNode().WithAdjacent("ping/pong-b"),
+ Node: report.MakeNode().WithAdjacent(ServerPodRenderedID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
},
},
- "ping/pong-b": {
- ID: "ping/pong-b",
+ ServerPodRenderedID: {
+ ID: ServerPodRenderedID,
LabelMajor: "pong-b",
LabelMinor: "1 container",
Rank: "ping/pong-b",
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.Server80NodeID,
- fixture.ServerPodNodeID,
- fixture.ServerProcessNodeID,
- fixture.ServerContainerNodeID,
- fixture.ServerHostNodeID,
- fixture.ServerContainerImageNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
+ fixture.Report.Container.Nodes[fixture.ServerContainerNodeID],
+ fixture.Report.ContainerImage.Nodes[fixture.ServerContainerImageNodeID],
+ fixture.Report.Pod.Nodes[fixture.ServerPodNodeID],
),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
@@ -420,10 +377,8 @@ var (
LabelMinor: fixture.ServerHostName,
Rank: "",
Pseudo: true,
- Origins: report.MakeIDList(
- fixture.ServerHostNodeID,
- fixture.NonContainerProcessNodeID,
- fixture.NonContainerNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID],
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
@@ -432,43 +387,35 @@ var (
ID: render.TheInternetID,
LabelMajor: render.TheInternetMajor,
Pseudo: true,
- Node: report.MakeNode().WithAdjacent("ping/pong-b"),
+ Node: report.MakeNode().WithAdjacent(ServerPodRenderedID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(60),
EgressByteCount: newu64(600),
},
- Origins: report.MakeIDList(
- fixture.RandomClientNodeID,
- fixture.GoogleEndpointNodeID,
- ),
},
}).Prune()
+ ServiceRenderedID = render.MakeServiceID("ping/pongservice")
+
RenderedPodServices = (render.RenderableNodes{
- "ping/pongservice": {
- ID: fixture.ServiceID,
+ ServiceRenderedID: {
+ ID: ServiceRenderedID,
LabelMajor: "pongservice",
LabelMinor: "2 pods",
Rank: fixture.ServiceID,
Pseudo: false,
- Origins: report.MakeIDList(
- fixture.Client54001NodeID,
- fixture.Client54002NodeID,
- fixture.ClientProcess1NodeID,
- fixture.ClientProcess2NodeID,
- fixture.ClientHostNodeID,
- fixture.ClientContainerNodeID,
- fixture.ClientContainerImageNodeID,
- fixture.ClientPodNodeID,
- fixture.Server80NodeID,
- fixture.ServerPodNodeID,
- fixture.ServiceNodeID,
- fixture.ServerProcessNodeID,
- fixture.ServerContainerNodeID,
- fixture.ServerHostNodeID,
- fixture.ServerContainerImageNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
+ fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
+ fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
+ fixture.Report.ContainerImage.Nodes[fixture.ClientContainerImageNodeID],
+ fixture.Report.Pod.Nodes[fixture.ClientPodNodeID],
+ fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
+ fixture.Report.Container.Nodes[fixture.ServerContainerNodeID],
+ fixture.Report.ContainerImage.Nodes[fixture.ServerContainerImageNodeID],
+ fixture.Report.Pod.Nodes[fixture.ServerPodNodeID],
),
- Node: report.MakeNode().WithAdjacent(fixture.ServiceID), // ?? Shouldn't be adjacent to itself?
+ Node: report.MakeNode().WithAdjacent(ServiceRenderedID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
@@ -482,10 +429,8 @@ var (
LabelMinor: fixture.ServerHostName,
Rank: "",
Pseudo: true,
- Origins: report.MakeIDList(
- fixture.ServerHostNodeID,
- fixture.NonContainerProcessNodeID,
- fixture.NonContainerNodeID,
+ Children: report.MakeNodeSet(
+ fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID],
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
@@ -494,15 +439,11 @@ var (
ID: render.TheInternetID,
LabelMajor: render.TheInternetMajor,
Pseudo: true,
- Node: report.MakeNode().WithAdjacent(fixture.ServiceID),
+ Node: report.MakeNode().WithAdjacent(ServiceRenderedID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(60),
EgressByteCount: newu64(600),
},
- Origins: report.MakeIDList(
- fixture.RandomClientNodeID,
- fixture.GoogleEndpointNodeID,
- ),
},
}).Prune()
)
diff --git a/render/filters.go b/render/filters.go
index 58f04045b9..4db23be33a 100644
--- a/render/filters.go
+++ b/render/filters.go
@@ -119,6 +119,17 @@ func (f Filter) Stats(rpt report.Report) Stats {
// to indicate a node has an edge pointing to it or from it
const IsConnected = "is_connected"
+// FilterPseudo produces a renderer that removes pseudo nodes from the given
+// renderer
+func FilterPseudo(r Renderer) Renderer {
+ return Filter{
+ Renderer: r,
+ FilterFunc: func(node RenderableNode) bool {
+ return !node.Pseudo
+ },
+ }
+}
+
// FilterUnconnected produces a renderer that filters unconnected nodes
// from the given renderer
func FilterUnconnected(r Renderer) Renderer {
diff --git a/render/filters_test.go b/render/filters_test.go
index 325e29d25d..6722462cf7 100644
--- a/render/filters_test.go
+++ b/render/filters_test.go
@@ -48,7 +48,7 @@ func TestFilterRender2(t *testing.T) {
}
}
-func TestFilterUnconnectedPesudoNodes(t *testing.T) {
+func TestFilterUnconnectedPseudoNodes(t *testing.T) {
// Test pseudo nodes that are made unconnected by filtering
// are also removed.
{
@@ -123,3 +123,21 @@ func TestFilterUnconnectedSelf(t *testing.T) {
}
}
}
+
+func TestFilterPseudo(t *testing.T) {
+ // Test pseudonodes are removed
+ {
+ nodes := render.RenderableNodes{
+ "foo": {ID: "foo", Node: report.MakeNode()},
+ "bar": {ID: "bar", Pseudo: true, Node: report.MakeNode()},
+ }
+ renderer := render.FilterPseudo(mockRenderer{RenderableNodes: nodes})
+ want := render.RenderableNodes{
+ "foo": {ID: "foo", Node: report.MakeNode()},
+ }
+ have := renderer.Render(report.MakeReport()).Prune()
+ if !reflect.DeepEqual(want, have) {
+ t.Error(test.Diff(want, have))
+ }
+ }
+}
diff --git a/render/id.go b/render/id.go
index 5bbf4c06af..952c3d3694 100644
--- a/render/id.go
+++ b/render/id.go
@@ -1,32 +1,56 @@
package render
import (
- "fmt"
"strings"
)
+// makeID is the generic ID maker
+func makeID(prefix string, parts ...string) string {
+ return strings.Join(append([]string{prefix}, parts...), ":")
+}
+
// MakeEndpointID makes an endpoint node ID for rendered nodes.
func MakeEndpointID(hostID, addr, port string) string {
- return fmt.Sprintf("endpoint:%s:%s:%s", hostID, addr, port)
+ return makeID("endpoint", hostID, addr, port)
}
// MakeProcessID makes a process node ID for rendered nodes.
func MakeProcessID(hostID, pid string) string {
- return fmt.Sprintf("process:%s:%s", hostID, pid)
+ return makeID("process", hostID, pid)
}
// MakeAddressID makes an address node ID for rendered nodes.
func MakeAddressID(hostID, addr string) string {
- return fmt.Sprintf("address:%s:%s", hostID, addr)
+ return makeID("address", hostID, addr)
+}
+
+// MakeContainerID makes a container node ID for rendered nodes.
+func MakeContainerID(containerID string) string {
+ return makeID("container", containerID)
+}
+
+// MakeContainerImageID makes a container image node ID for rendered nodes.
+func MakeContainerImageID(imageID string) string {
+ return makeID("container_image", imageID)
+}
+
+// MakePodID makes a pod node ID for rendered nodes.
+func MakePodID(podID string) string {
+ return makeID("pod", podID)
+}
+
+// MakeServiceID makes a service node ID for rendered nodes.
+func MakeServiceID(serviceID string) string {
+ return makeID("service", serviceID)
}
// MakeHostID makes a host node ID for rendered nodes.
func MakeHostID(hostID string) string {
- return fmt.Sprintf("host:%s", hostID)
+ return makeID("host", hostID)
}
// MakePseudoNodeID produces a pseudo node ID from its composite parts,
// for use in rendered nodes.
func MakePseudoNodeID(parts ...string) string {
- return strings.Join(append([]string{"pseudo"}, parts...), ":")
+ return makeID("pseudo", parts...)
}
diff --git a/render/mapping.go b/render/mapping.go
index 74b3feb81e..b3ded7a745 100644
--- a/render/mapping.go
+++ b/render/mapping.go
@@ -123,12 +123,13 @@ func MapProcessIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
// renderable node. As it is only ever run on container topology nodes, we
// expect that certain keys are present.
func MapContainerIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
- id, ok := m.Metadata[docker.ContainerID]
+ containerID, ok := m.Metadata[docker.ContainerID]
if !ok {
return RenderableNodes{}
}
var (
+ id = MakeContainerID(containerID)
major, _ = GetRenderableContainerName(m.Node)
minor = report.ExtractHostID(m.Node)
rank = m.Metadata[docker.ImageID]
@@ -136,10 +137,6 @@ func MapContainerIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
node := NewRenderableNodeWith(id, major, minor, rank, m)
node.ControlNode = m.ID
- if imageID, ok := m.Metadata[docker.ImageID]; ok {
- hostID, _, _ := report.ParseContainerNodeID(m.ID)
- node.Origins = node.Origins.Add(report.MakeContainerNodeID(hostID, imageID))
- }
return RenderableNodes{id: node}
}
@@ -171,14 +168,15 @@ func GetRenderableContainerName(nmd report.Node) (string, bool) {
// image renderable node. As it is only ever run on container image topology
// nodes, we expect that certain keys are present.
func MapContainerImageIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
- id, ok := m.Metadata[docker.ImageID]
+ imageID, ok := m.Metadata[docker.ImageID]
if !ok {
return RenderableNodes{}
}
var (
+ id = MakeContainerImageID(imageID)
major = m.Metadata[docker.ImageName]
- rank = m.Metadata[docker.ImageID]
+ rank = imageID
)
return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)}
@@ -188,12 +186,13 @@ func MapContainerImageIdentity(m RenderableNode, _ report.Networks) RenderableNo
// only ever run on pod topology nodes, we expect that certain keys
// are present.
func MapPodIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
- id, ok := m.Metadata[kubernetes.PodID]
+ podID, ok := m.Metadata[kubernetes.PodID]
if !ok {
return RenderableNodes{}
}
var (
+ id = MakePodID(podID)
major = m.Metadata[kubernetes.PodName]
rank = m.Metadata[kubernetes.PodID]
)
@@ -205,12 +204,13 @@ func MapPodIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
// only ever run on service topology nodes, we expect that certain keys
// are present.
func MapServiceIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
- id, ok := m.Metadata[kubernetes.ServiceID]
+ serviceID, ok := m.Metadata[kubernetes.ServiceID]
if !ok {
return RenderableNodes{}
}
var (
+ id = MakeServiceID(serviceID)
major = m.Metadata[kubernetes.ServiceName]
rank = m.Metadata[kubernetes.ServiceID]
)
@@ -308,6 +308,7 @@ func MapEndpoint2IP(m RenderableNode, local report.Networks) RenderableNodes {
// So we need to emit two nodes, for two different cases.
id := report.MakeScopedEndpointNodeID(scope, addr, "")
idWithPort := report.MakeScopedEndpointNodeID(scope, addr, port)
+ m = m.WithParents(nil)
return RenderableNodes{
id: NewRenderableNodeWith(id, "", "", "", m),
idWithPort: NewRenderableNodeWith(idWithPort, "", "", "", m),
@@ -340,7 +341,7 @@ func MapContainer2IP(m RenderableNode, _ report.Networks) RenderableNodes {
if mapping := portMappingMatch.FindStringSubmatch(portMapping); mapping != nil {
ip, port := mapping[1], mapping[2]
id := report.MakeScopedEndpointNodeID("", ip, port)
- node := NewRenderableNodeWith(id, "", "", "", m)
+ node := NewRenderableNodeWith(id, "", "", "", m.WithParents(nil))
node.Counters[containersKey] = 1
result[id] = node
}
@@ -367,12 +368,14 @@ func MapIP2Container(n RenderableNode, _ report.Networks) RenderableNodes {
// If this node is not a container, exclude it.
// This excludes all the nodes we've dragged in from endpoint
// that we failed to join to a container.
- id, ok := n.Node.Metadata[docker.ContainerID]
+ containerID, ok := n.Node.Metadata[docker.ContainerID]
if !ok {
return RenderableNodes{}
}
- return RenderableNodes{id: NewDerivedNode(id, n)}
+ id := MakeContainerID(containerID)
+
+ return RenderableNodes{id: NewDerivedNode(id, n.WithParents(nil))}
}
// MapEndpoint2Process maps endpoint RenderableNodes to process
@@ -397,7 +400,7 @@ func MapEndpoint2Process(n RenderableNode, _ report.Networks) RenderableNodes {
}
id := MakeProcessID(report.ExtractHostID(n.Node), pid)
- return RenderableNodes{id: NewDerivedNode(id, n)}
+ return RenderableNodes{id: NewDerivedNode(id, n.WithParents(nil))}
}
// MapProcess2Container maps process RenderableNodes to container
@@ -426,16 +429,23 @@ func MapProcess2Container(n RenderableNode, _ report.Networks) RenderableNodes {
// into an per-host "Uncontained" node. If for whatever reason
// this node doesn't have a host id in their nodemetadata, it'll
// all get grouped into a single uncontained node.
- id, ok := n.Node.Metadata[docker.ContainerID]
- if !ok {
- hostID := report.ExtractHostID(n.Node)
+ var (
+ id string
+ node RenderableNode
+ hostID = report.ExtractHostID(n.Node)
+ )
+ n = n.WithParents(nil)
+ if containerID, ok := n.Node.Metadata[docker.ContainerID]; ok {
+ id = MakeContainerID(containerID)
+ node = NewDerivedNode(id, n)
+ } else {
id = MakePseudoNodeID(UncontainedID, hostID)
- node := newDerivedPseudoNode(id, UncontainedMajor, n)
+ node = newDerivedPseudoNode(id, UncontainedMajor, n)
node.LabelMinor = hostID
- return RenderableNodes{id: node}
}
- return RenderableNodes{id: NewDerivedNode(id, n)}
+ node.Children = node.Children.Add(n.Node)
+ return RenderableNodes{id: node}
}
// MapProcess2Name maps process RenderableNodes to RenderableNodes
@@ -458,6 +468,9 @@ func MapProcess2Name(n RenderableNode, _ report.Networks) RenderableNodes {
node.LabelMajor = name
node.Rank = name
node.Node.Counters[processesKey] = 1
+ node.Node.Topology = "process_name"
+ node.Node.ID = name
+ node.Children = node.Children.Add(n.Node)
return RenderableNodes{name: node}
}
@@ -497,14 +510,21 @@ func MapContainer2ContainerImage(n RenderableNode, _ report.Networks) Renderable
// Otherwise, if some some reason the container doesn't have a image_id
// (maybe slightly out of sync reports), just drop it
- id, ok := n.Node.Metadata[docker.ImageID]
+ imageID, ok := n.Node.Metadata[docker.ImageID]
if !ok {
return RenderableNodes{}
}
// Add container id key to the counters, which will later be counted to produce the minor label
- result := NewDerivedNode(id, n)
+ id := MakeContainerImageID(imageID)
+ result := NewDerivedNode(id, n.WithParents(nil))
result.Node.Counters[containersKey] = 1
+
+ // Add the container as a child of the new image node
+ result.Children = result.Children.Add(n.Node)
+
+ result.Node.Topology = "container_image"
+ result.Node.ID = report.MakeContainerImageNodeID(imageID)
return RenderableNodes{id: result}
}
@@ -532,15 +552,19 @@ func MapPod2Service(n RenderableNode, _ report.Networks) RenderableNodes {
}
result := RenderableNodes{}
- for _, id := range strings.Fields(ids) {
- n := NewDerivedNode(id, n)
+ for _, serviceID := range strings.Fields(ids) {
+ id := MakeServiceID(serviceID)
+ n := NewDerivedNode(id, n.WithParents(nil))
n.Node.Counters[podsKey] = 1
+ n.Children = n.Children.Add(n.Node)
result[id] = n
}
return result
}
-func imageNameWithoutVersion(name string) string {
+// ImageNameWithoutVersion splits the image name apart, returning the name
+// without the version, if possible
+func ImageNameWithoutVersion(name string) string {
parts := strings.SplitN(name, "/", 3)
if len(parts) == 3 {
name = fmt.Sprintf("%s/%s", parts[1], parts[2])
@@ -565,13 +589,38 @@ func MapContainerImage2Name(n RenderableNode, _ report.Networks) RenderableNodes
return RenderableNodes{}
}
- name = imageNameWithoutVersion(name)
+ name = ImageNameWithoutVersion(name)
+ id := MakeContainerImageID(name)
- node := NewDerivedNode(name, n)
+ node := NewDerivedNode(id, n)
node.LabelMajor = name
node.Rank = name
node.Node = n.Node.Copy() // Propagate NMD for container counting.
- return RenderableNodes{name: node}
+ return RenderableNodes{id: node}
+}
+
+// MapX2Host maps any RenderableNodes to host
+// RenderableNodes.
+//
+// If this function is given a node without a hostname
+// (including other pseudo nodes), it will drop the node.
+//
+// Otherwise, this function will produce a node with the correct ID
+// format for a container, but without any Major or Minor labels.
+// It does not have enough info to do that, and the resulting graph
+// must be merged with a container graph to get that info.
+func MapX2Host(n RenderableNode, _ report.Networks) RenderableNodes {
+ // Propogate all pseudo nodes
+ if n.Pseudo {
+ return RenderableNodes{n.ID: n}
+ }
+ if _, ok := n.Node.Metadata[report.HostNodeID]; !ok {
+ return RenderableNodes{}
+ }
+ id := MakeHostID(report.ExtractHostID(n.Node))
+ result := NewDerivedNode(id, n.WithParents(nil))
+ result.Children = result.Children.Add(n.Node)
+ return RenderableNodes{id: result}
}
// MapContainer2Pod maps container RenderableNodes to pod
@@ -593,23 +642,27 @@ func MapContainer2Pod(n RenderableNode, _ report.Networks) RenderableNodes {
// Otherwise, if some some reason the container doesn't have a pod_id (maybe
// slightly out of sync reports, or its not in a pod), just drop it
- id, ok := n.Node.Metadata["docker_label_io.kubernetes.pod.name"]
+ podID, ok := n.Node.Metadata[kubernetes.PodID]
if !ok {
return RenderableNodes{}
}
+ id := MakePodID(podID)
// Add container- key to NMD, which will later be counted to produce the
// minor label
- result := NewRenderableNodeWith(id, "", "", id, n)
+ result := NewRenderableNodeWith(id, "", "", podID, n.WithParents(nil))
result.Node.Counters[containersKey] = 1
// Due to a bug in kubernetes, addon pods on the master node are not returned
// from the API. This is a workaround until
// https://github.com/kubernetes/kubernetes/issues/14738 is fixed.
- if s := strings.SplitN(id, "/", 2); len(s) == 2 {
+ if s := strings.SplitN(podID, "/", 2); len(s) == 2 {
result.LabelMajor = s[1]
result.Node.Metadata[kubernetes.Namespace] = s[0]
result.Node.Metadata[kubernetes.PodName] = s[1]
}
+
+ result.Children = result.Children.Add(n.Node)
+
return RenderableNodes{id: result}
}
@@ -633,6 +686,12 @@ func MapContainer2Hostname(n RenderableNode, _ report.Networks) RenderableNodes
// Add container id key to the counters, which will later be counted to produce the minor label
result.Node.Counters[containersKey] = 1
+
+ result.Node.Topology = "container_hostname"
+ result.Node.ID = id
+
+ result.Children = result.Children.Add(n.Node)
+
return RenderableNodes{id: result}
}
@@ -669,18 +728,6 @@ func MapCountPods(n RenderableNode, _ report.Networks) RenderableNodes {
return RenderableNodes{n.ID: n}
}
-// MapAddress2Host maps address RenderableNodes to host RenderableNodes.
-//
-// Otherthan pseudo nodes, we can assume all nodes have a HostID
-func MapAddress2Host(n RenderableNode, _ report.Networks) RenderableNodes {
- if n.Pseudo {
- return RenderableNodes{n.ID: n}
- }
-
- id := MakeHostID(report.ExtractHostID(n.Node))
- return RenderableNodes{id: NewDerivedNode(id, n)}
-}
-
// trySplitAddr is basically ParseArbitraryNodeID, since its callsites
// (pseudo funcs) just have opaque node IDs and don't know what topology they
// come from. Without changing how pseudo funcs work, we can't make it much
diff --git a/render/mapping_internal_test.go b/render/mapping_internal_test.go
index 75d2bfc8d1..c04e2778df 100644
--- a/render/mapping_internal_test.go
+++ b/render/mapping_internal_test.go
@@ -12,7 +12,7 @@ func TestDockerImageName(t *testing.T) {
{"docker-registry.domain.name:5000/repo/image1:ver", "repo/image1"},
{"foo", "foo"},
} {
- name := imageNameWithoutVersion(input.in)
+ name := ImageNameWithoutVersion(input.in)
if name != input.name {
t.Fatalf("%s: %s != %s", input.in, name, input.name)
}
diff --git a/render/renderable_node.go b/render/renderable_node.go
index 8519f3687f..368e0353b9 100644
--- a/render/renderable_node.go
+++ b/render/renderable_node.go
@@ -8,13 +8,13 @@ import (
// an element of a topology. It should contain information that's relevant
// to rendering a node when there are many nodes visible at once.
type RenderableNode struct {
- ID string `json:"id"` //
- LabelMajor string `json:"label_major"` // e.g. "process", human-readable
- LabelMinor string `json:"label_minor,omitempty"` // e.g. "hostname", human-readable, optional
- Rank string `json:"rank"` // to help the layout engine
- Pseudo bool `json:"pseudo,omitempty"` // sort-of a placeholder node, for rendering purposes
- Origins report.IDList `json:"origins,omitempty"` // Core node IDs that contributed information
- ControlNode string `json:"-"` // ID of node from which to show the controls in the UI
+ ID string `json:"id"` //
+ LabelMajor string `json:"label_major"` // e.g. "process", human-readable
+ LabelMinor string `json:"label_minor,omitempty"` // e.g. "hostname", human-readable, optional
+ Rank string `json:"rank"` // to help the layout engine
+ Pseudo bool `json:"pseudo,omitempty"` // sort-of a placeholder node, for rendering purposes
+ Children report.NodeSet `json:"children,omitempty"` // Nodes which have been grouped into this one
+ ControlNode string `json:"-"` // ID of node from which to show the controls in the UI
report.EdgeMetadata `json:"metadata"` // Numeric sums
report.Node
@@ -28,23 +28,22 @@ func NewRenderableNode(id string) RenderableNode {
LabelMinor: "",
Rank: "",
Pseudo: false,
- Origins: report.MakeIDList(),
EdgeMetadata: report.EdgeMetadata{},
Node: report.MakeNode(),
}
}
// NewRenderableNodeWith makes a new RenderableNode with some fields filled in
-func NewRenderableNodeWith(id, major, minor, rank string, rn RenderableNode) RenderableNode {
+func NewRenderableNodeWith(id, major, minor, rank string, node RenderableNode) RenderableNode {
return RenderableNode{
ID: id,
LabelMajor: major,
LabelMinor: minor,
Rank: rank,
Pseudo: false,
- Origins: rn.Origins.Copy(),
- EdgeMetadata: rn.EdgeMetadata.Copy(),
- Node: rn.Node.Copy(),
+ Children: node.Children.Copy(),
+ EdgeMetadata: node.EdgeMetadata.Copy(),
+ Node: node.Node.Copy(),
}
}
@@ -56,7 +55,7 @@ func NewDerivedNode(id string, node RenderableNode) RenderableNode {
LabelMinor: "",
Rank: "",
Pseudo: node.Pseudo,
- Origins: node.Origins.Copy(),
+ Children: node.Children.Copy(),
EdgeMetadata: node.EdgeMetadata.Copy(),
Node: node.Node.Copy(),
ControlNode: "", // Do not propagate ControlNode when making a derived node!
@@ -70,7 +69,7 @@ func newDerivedPseudoNode(id, major string, node RenderableNode) RenderableNode
LabelMinor: "",
Rank: "",
Pseudo: true,
- Origins: node.Origins.Copy(),
+ Children: node.Children.Copy(),
EdgeMetadata: node.EdgeMetadata.Copy(),
Node: node.Node.Copy(),
}
@@ -83,6 +82,13 @@ func (rn RenderableNode) WithNode(n report.Node) RenderableNode {
return result
}
+// WithParents creates a new RenderableNode based on rn, where n has the given parents set
+func (rn RenderableNode) WithParents(p report.Sets) RenderableNode {
+ result := rn.Copy()
+ result.Node.Parents = p
+ return result
+}
+
// Merge merges rn with other and returns a new RenderableNode
func (rn RenderableNode) Merge(other RenderableNode) RenderableNode {
result := rn.Copy()
@@ -107,7 +113,7 @@ func (rn RenderableNode) Merge(other RenderableNode) RenderableNode {
panic(result.ID)
}
- result.Origins = rn.Origins.Merge(other.Origins)
+ result.Children = rn.Children.Merge(other.Children)
result.EdgeMetadata = rn.EdgeMetadata.Merge(other.EdgeMetadata)
result.Node = rn.Node.Merge(other.Node)
@@ -122,7 +128,7 @@ func (rn RenderableNode) Copy() RenderableNode {
LabelMinor: rn.LabelMinor,
Rank: rn.Rank,
Pseudo: rn.Pseudo,
- Origins: rn.Origins.Copy(),
+ Children: rn.Children.Copy(),
EdgeMetadata: rn.EdgeMetadata.Copy(),
Node: rn.Node.Copy(),
ControlNode: rn.ControlNode,
@@ -135,6 +141,7 @@ func (rn RenderableNode) Copy() RenderableNode {
func (rn RenderableNode) Prune() RenderableNode {
cp := rn.Copy()
cp.Node = report.MakeNode().WithAdjacent(cp.Node.Adjacency...)
+ cp.Children = nil
return cp
}
diff --git a/render/renderable_node_test.go b/render/renderable_node_test.go
index d955dec6b0..9a9ad49a05 100644
--- a/render/renderable_node_test.go
+++ b/render/renderable_node_test.go
@@ -37,7 +37,7 @@ func TestMergeRenderableNode(t *testing.T) {
Rank: "",
Pseudo: false,
Node: report.MakeNode().WithAdjacent("a1"),
- Origins: report.MakeIDList("o1"),
+ Children: report.MakeNodeSet(report.MakeNode().WithID("child1")),
}
node2 := render.RenderableNode{
ID: "foo",
@@ -46,7 +46,7 @@ func TestMergeRenderableNode(t *testing.T) {
Rank: "rank",
Pseudo: false,
Node: report.MakeNode().WithAdjacent("a2"),
- Origins: report.MakeIDList("o2"),
+ Children: report.MakeNodeSet(report.MakeNode().WithID("child2")),
}
want := render.RenderableNode{
ID: "foo",
@@ -54,8 +54,8 @@ func TestMergeRenderableNode(t *testing.T) {
LabelMinor: "minor",
Rank: "rank",
Pseudo: false,
- Node: report.MakeNode().WithAdjacent("a1").WithAdjacent("a2"),
- Origins: report.MakeIDList("o1", "o2"),
+ Node: report.MakeNode().WithID("foo").WithAdjacent("a1").WithAdjacent("a2"),
+ Children: report.MakeNodeSet(report.MakeNode().WithID("child1"), report.MakeNode().WithID("child2")),
EdgeMetadata: report.EdgeMetadata{},
}.Prune()
have := node1.Merge(node2).Prune()
diff --git a/render/selectors.go b/render/selectors.go
index 9274aaa450..18032aac9e 100644
--- a/render/selectors.go
+++ b/render/selectors.go
@@ -22,12 +22,7 @@ func (t TopologySelector) Stats(r report.Report) Stats {
func MakeRenderableNodes(t report.Topology) RenderableNodes {
result := RenderableNodes{}
for id, nmd := range t.Nodes {
- rn := NewRenderableNode(id).WithNode(nmd)
- rn.Origins = report.MakeIDList(id)
- if hostNodeID, ok := nmd.Metadata[report.HostNodeID]; ok {
- rn.Origins = rn.Origins.Add(hostNodeID)
- }
- result[id] = rn
+ result[id] = NewRenderableNode(id).WithNode(nmd)
}
// Push EdgeMetadata to both ends of the edges
diff --git a/render/short_lived_connections_test.go b/render/short_lived_connections_test.go
index 0b98b4f5fa..74fa46a9b0 100644
--- a/render/short_lived_connections_test.go
+++ b/render/short_lived_connections_test.go
@@ -28,7 +28,7 @@ var (
containerID = "a1b2c3d4e5"
containerIP = "192.168.0.1"
containerName = "foo"
- containerNodeID = report.MakeContainerNodeID(serverHostID, containerID)
+ containerNodeID = report.MakeContainerNodeID(containerID)
rpt = report.Report{
Endpoint: report.Topology{
@@ -37,13 +37,13 @@ var (
endpoint.Addr: randomIP,
endpoint.Port: randomPort,
endpoint.Conntracked: "true",
- }).WithAdjacent(serverEndpointNodeID),
+ }).WithAdjacent(serverEndpointNodeID).WithID(randomEndpointNodeID).WithTopology(report.Endpoint),
serverEndpointNodeID: report.MakeNode().WithMetadata(map[string]string{
endpoint.Addr: serverIP,
endpoint.Port: serverPort,
endpoint.Conntracked: "true",
- }),
+ }).WithID(serverEndpointNodeID).WithTopology(report.Endpoint),
},
},
Container: report.Topology{
@@ -55,7 +55,7 @@ var (
}).WithSets(report.Sets{
docker.ContainerIPs: report.MakeStringSet(containerIP),
docker.ContainerPorts: report.MakeStringSet(fmt.Sprintf("%s:%s->%s/tcp", serverIP, serverPort, serverPort)),
- }),
+ }).WithID(containerNodeID).WithTopology(report.Container),
},
},
Host: report.Topology{
@@ -64,7 +64,7 @@ var (
report.HostNodeID: serverHostNodeID,
}).WithSets(report.Sets{
host.LocalNetworks: report.MakeStringSet("192.168.0.0/16"),
- }),
+ }).WithID(serverHostNodeID).WithTopology(report.Host),
},
},
}
@@ -74,16 +74,14 @@ var (
ID: render.TheInternetID,
LabelMajor: render.TheInternetMajor,
Pseudo: true,
- Node: report.MakeNode().WithAdjacent(containerID),
- Origins: report.MakeIDList(randomEndpointNodeID),
+ Node: report.MakeNode().WithAdjacent(render.MakeContainerID(containerID)),
},
- containerID: {
- ID: containerID,
+ render.MakeContainerID(containerID): {
+ ID: render.MakeContainerID(containerID),
LabelMajor: containerName,
LabelMinor: serverHostID,
Rank: "",
Pseudo: false,
- Origins: report.MakeIDList(containerNodeID, serverEndpointNodeID, serverHostNodeID),
Node: report.MakeNode(),
ControlNode: containerNodeID,
},
diff --git a/render/topologies.go b/render/topologies.go
index d01121efcf..edc914605a 100644
--- a/render/topologies.go
+++ b/render/topologies.go
@@ -49,7 +49,7 @@ func (r processWithContainerNameRenderer) Render(rpt report.Report) RenderableNo
if !ok {
continue
}
- container, ok := containers[containerID]
+ container, ok := containers[MakeContainerID(containerID)]
if !ok {
continue
}
@@ -86,15 +86,10 @@ var ContainerRenderer = MakeReduce(
_, isConnected := n.Node.Metadata[IsConnected]
return inContainer || isConnected
},
- Renderer: ColorConnected(Map{
+ Renderer: Map{
MapFunc: MapProcess2Container,
- Renderer: ProcessRenderer,
- }),
- },
-
- Map{
- MapFunc: MapContainerIdentity,
- Renderer: SelectContainer,
+ Renderer: ColorConnected(ProcessRenderer),
+ },
},
// This mapper brings in short lived connections by joining with container IPs.
@@ -114,6 +109,11 @@ var ContainerRenderer = MakeReduce(
},
),
}),
+
+ Map{
+ MapFunc: MapContainerIdentity,
+ Renderer: SelectContainer,
+ },
)
type containerWithImageNameRenderer struct {
@@ -135,11 +135,11 @@ func (r containerWithImageNameRenderer) Render(rpt report.Report) RenderableNode
if !ok {
continue
}
- image, ok := images[imageID]
+ image, ok := images[MakeContainerImageID(imageID)]
if !ok {
continue
}
- c.Rank = imageNameWithoutVersion(image.LabelMajor)
+ c.Rank = ImageNameWithoutVersion(image.LabelMajor)
c.Metadata = image.Metadata.Merge(c.Metadata)
containers[id] = c
}
@@ -191,7 +191,25 @@ var AddressRenderer = Map{
// graph from the host topology and address graph.
var HostRenderer = MakeReduce(
Map{
- MapFunc: MapAddress2Host,
+ MapFunc: MapX2Host,
+ Renderer: Map{
+ MapFunc: MapContainerImageIdentity,
+ Renderer: SelectContainerImage,
+ },
+ },
+ Map{
+ MapFunc: MapX2Host,
+ Renderer: FilterPseudo(ContainerRenderer),
+ },
+ Map{
+ MapFunc: MapX2Host,
+ Renderer: Map{
+ MapFunc: MapPodIdentity,
+ Renderer: SelectPod,
+ },
+ },
+ Map{
+ MapFunc: MapX2Host,
Renderer: AddressRenderer,
},
Map{
@@ -205,14 +223,14 @@ var HostRenderer = MakeReduce(
var PodRenderer = Map{
MapFunc: MapCountContainers,
Renderer: MakeReduce(
- Map{
- MapFunc: MapPodIdentity,
- Renderer: SelectPod,
- },
Map{
MapFunc: MapContainer2Pod,
Renderer: ContainerRenderer,
},
+ Map{
+ MapFunc: MapPodIdentity,
+ Renderer: SelectPod,
+ },
),
}
diff --git a/render/topologies_test.go b/render/topologies_test.go
index 2a70ce68fe..7a74424df2 100644
--- a/render/topologies_test.go
+++ b/render/topologies_test.go
@@ -43,7 +43,7 @@ func TestContainerFilterRenderer(t *testing.T) {
input.Container.Nodes[fixture.ClientContainerNodeID].Metadata[docker.LabelPrefix+"works.weave.role"] = "system"
have := render.FilterSystem(render.ContainerWithImageNameRenderer).Render(input).Prune()
want := expected.RenderedContainers.Copy()
- delete(want, fixture.ClientContainerID)
+ delete(want, expected.ClientContainerRenderedID)
if !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}
@@ -77,14 +77,14 @@ func TestPodFilterRenderer(t *testing.T) {
// tag on containers or pod namespace in the topology and ensure
// it is filtered out correctly.
input := fixture.Report.Copy()
- input.Pod.Nodes[fixture.ClientPodNodeID].Metadata[kubernetes.PodID] = "kube-system/foo"
+ input.Pod.Nodes[fixture.ClientPodNodeID].Metadata[kubernetes.PodID] = "pod:kube-system/foo"
input.Pod.Nodes[fixture.ClientPodNodeID].Metadata[kubernetes.Namespace] = "kube-system"
input.Pod.Nodes[fixture.ClientPodNodeID].Metadata[kubernetes.PodName] = "foo"
input.Container.Nodes[fixture.ClientContainerNodeID].Metadata[docker.LabelPrefix+"io.kubernetes.pod.name"] = "kube-system/foo"
have := render.FilterSystem(render.PodRenderer).Render(input).Prune()
want := expected.RenderedPods.Copy()
- delete(want, fixture.ClientPodID)
- delete(want, fixture.ClientContainerID)
+ delete(want, expected.ClientPodRenderedID)
+ delete(want, expected.ClientContainerRenderedID)
if !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}
diff --git a/report/id.go b/report/id.go
index 42b383611b..23561a7f6b 100644
--- a/report/id.go
+++ b/report/id.go
@@ -102,13 +102,18 @@ func MakeHostNodeID(hostID string) string {
}
// MakeContainerNodeID produces a container node ID from its composite parts.
-func MakeContainerNodeID(hostID, containerID string) string {
- return hostID + ScopeDelim + containerID
+func MakeContainerNodeID(containerID string) string {
+ return containerID + ScopeDelim + ""
+}
+
+// MakeContainerImageNodeID produces a container image node ID from its composite parts.
+func MakeContainerImageNodeID(containerImageID string) string {
+ return containerImageID + ScopeDelim + ""
}
// MakePodNodeID produces a pod node ID from its composite parts.
-func MakePodNodeID(hostID, podID string) string {
- return hostID + ScopeDelim + podID
+func MakePodNodeID(namespaceID, podID string) string {
+ return namespaceID + ScopeDelim + podID
}
// MakeServiceNodeID produces a service node ID from its composite parts.
@@ -143,14 +148,13 @@ func ParseEndpointNodeID(endpointNodeID string) (hostID, address, port string, o
return fields[0], fields[1], fields[2], true
}
-// ParseContainerNodeID produces the host and container id from an container
-// node ID.
-func ParseContainerNodeID(containerNodeID string) (hostID, containerID string, ok bool) {
+// ParseContainerNodeID produces the container id from an container node ID.
+func ParseContainerNodeID(containerNodeID string) (containerID string, ok bool) {
fields := strings.SplitN(containerNodeID, ScopeDelim, 2)
- if len(fields) != 2 {
- return "", "", false
+ if len(fields) != 2 || fields[1] != "" {
+ return "", false
}
- return fields[0], fields[1], true
+ return fields[0], true
}
// ParseAddressNodeID produces the host ID, address from an address node ID.
diff --git a/report/id_list.go b/report/id_list.go
index a530d3c92e..9c740e69bc 100644
--- a/report/id_list.go
+++ b/report/id_list.go
@@ -15,6 +15,11 @@ func (a IDList) Add(ids ...string) IDList {
return IDList(StringSet(a).Add(ids...))
}
+// Remove is the only correct way to remove IDs from an IDList.
+func (a IDList) Remove(ids ...string) IDList {
+ return IDList(StringSet(a).Remove(ids...))
+}
+
// Copy returns a copy of the IDList.
func (a IDList) Copy() IDList {
return IDList(StringSet(a).Copy())
diff --git a/report/merge_test.go b/report/merge_test.go
index a97585887c..e38d89bd12 100644
--- a/report/merge_test.go
+++ b/report/merge_test.go
@@ -193,7 +193,30 @@ func TestMergeNodes(t *testing.T) {
}),
},
},
- "Merge conflict": {
+ "Merge conflict with rank difference": {
+ a: report.Nodes{
+ ":192.168.1.1:12345": report.MakeNodeWith(map[string]string{
+ PID: "23128",
+ Name: "curl",
+ Domain: "node-a.local",
+ }),
+ },
+ b: report.Nodes{
+ ":192.168.1.1:12345": report.MakeNodeWith(map[string]string{ // <-- same ID
+ PID: "0",
+ Name: "curl",
+ Domain: "node-a.local",
+ }),
+ },
+ want: report.Nodes{
+ ":192.168.1.1:12345": report.MakeNodeWith(map[string]string{
+ PID: "23128",
+ Name: "curl",
+ Domain: "node-a.local",
+ }),
+ },
+ },
+ "Merge conflict with no rank difference": {
a: report.Nodes{
":192.168.1.1:12345": report.MakeNodeWith(map[string]string{
PID: "23128",
diff --git a/report/metrics.go b/report/metrics.go
index a098939250..3c7f893c51 100644
--- a/report/metrics.go
+++ b/report/metrics.go
@@ -235,7 +235,9 @@ func parseTime(s string) time.Time {
return t
}
-func (m Metric) toIntermediate() WireMetrics {
+// ToIntermediate converts the metric to a representation suitable
+// for serialization.
+func (m Metric) ToIntermediate() WireMetrics {
samples := []Sample{}
if m.Samples != nil {
m.Samples.Reverse().ForEach(func(s interface{}) {
@@ -268,7 +270,7 @@ func (m WireMetrics) fromIntermediate() Metric {
// MarshalJSON implements json.Marshaller
func (m Metric) MarshalJSON() ([]byte, error) {
buf := bytes.Buffer{}
- in := m.toIntermediate()
+ in := m.ToIntermediate()
err := json.NewEncoder(&buf).Encode(in)
return buf.Bytes(), err
}
@@ -286,7 +288,7 @@ func (m *Metric) UnmarshalJSON(input []byte) error {
// GobEncode implements gob.Marshaller
func (m Metric) GobEncode() ([]byte, error) {
buf := bytes.Buffer{}
- err := gob.NewEncoder(&buf).Encode(m.toIntermediate())
+ err := gob.NewEncoder(&buf).Encode(m.ToIntermediate())
return buf.Bytes(), err
}
diff --git a/report/node_set.go b/report/node_set.go
new file mode 100644
index 0000000000..c35950c162
--- /dev/null
+++ b/report/node_set.go
@@ -0,0 +1,94 @@
+package report
+
+import (
+ "sort"
+)
+
+// NodeSet is a sorted set of nodes keyed on (Topology, ID). Clients must use
+// the Add method to add nodes
+type NodeSet []Node
+
+// MakeNodeSet makes a new NodeSet with the given nodes.
+func MakeNodeSet(nodes ...Node) NodeSet {
+ if len(nodes) <= 0 {
+ return nil
+ }
+ result := make(NodeSet, len(nodes))
+ copy(result, nodes)
+ sort.Sort(result)
+ for i := 1; i < len(result); { // remove any duplicates
+ if result[i-1].Equal(result[i]) {
+ result = append(result[:i-1], result[i:]...)
+ continue
+ }
+ i++
+ }
+ return result
+}
+
+// Implementation of sort.Interface
+func (n NodeSet) Len() int { return len(n) }
+func (n NodeSet) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
+func (n NodeSet) Less(i, j int) bool { return n[i].Before(n[j]) }
+
+// Add adds the nodes to the NodeSet. Add is the only valid way to grow a
+// NodeSet. Add returns the NodeSet to enable chaining.
+func (n NodeSet) Add(nodes ...Node) NodeSet {
+ for _, node := range nodes {
+ i := sort.Search(len(n), func(i int) bool {
+ return n[i].Topology >= node.Topology && n[i].ID >= node.ID
+ })
+ if i < len(n) && n[i].Topology == node.Topology && n[i].ID == node.ID {
+ // The list already has the element.
+ continue
+ }
+ // It a new element, insert it in order.
+ n = append(n, Node{})
+ copy(n[i+1:], n[i:])
+ n[i] = node
+ }
+ return n
+}
+
+// Merge combines the two NodeSets and returns a new result.
+func (n NodeSet) Merge(other NodeSet) NodeSet {
+ switch {
+ case len(other) <= 0: // Optimise special case, to avoid allocating
+ return n // (note unit test DeepEquals breaks if we don't do this)
+ case len(n) <= 0:
+ return other
+ }
+
+ result := make([]Node, 0, len(n)+len(other))
+ for len(n) > 0 || len(other) > 0 {
+ switch {
+ case len(n) == 0:
+ return append(result, other...)
+ case len(other) == 0:
+ return append(result, n...)
+ case n[0].Before(other[0]):
+ result = append(result, n[0])
+ n = n[1:]
+ case n[0].After(other[0]):
+ result = append(result, other[0])
+ other = other[1:]
+ default: // equal
+ result = append(result, other[0])
+ n = n[1:]
+ other = other[1:]
+ }
+ }
+ return result
+}
+
+// Copy returns a value copy of the NodeSet.
+func (n NodeSet) Copy() NodeSet {
+ if n == nil {
+ return n
+ }
+ result := make(NodeSet, len(n))
+ for i, node := range n {
+ result[i] = node
+ }
+ return result
+}
diff --git a/report/node_set_test.go b/report/node_set_test.go
new file mode 100644
index 0000000000..0bb0ee8258
--- /dev/null
+++ b/report/node_set_test.go
@@ -0,0 +1,231 @@
+package report_test
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/weaveworks/scope/report"
+)
+
+var benchmarkResult report.NodeSet
+
+type nodeSpec struct {
+ topology string
+ id string
+}
+
+func TestMakeNodeSet(t *testing.T) {
+ for _, testcase := range []struct {
+ inputs []nodeSpec
+ wants []nodeSpec
+ }{
+ {inputs: nil, wants: nil},
+ {inputs: []nodeSpec{}, wants: []nodeSpec{}},
+ {
+ inputs: []nodeSpec{{"", "a"}},
+ wants: []nodeSpec{{"", "a"}},
+ },
+ {
+ inputs: []nodeSpec{{"", "a"}, {"", "a"}, {"1", "a"}},
+ wants: []nodeSpec{{"", "a"}, {"1", "a"}},
+ },
+ {
+ inputs: []nodeSpec{{"", "b"}, {"", "c"}, {"", "a"}},
+ wants: []nodeSpec{{"", "a"}, {"", "b"}, {"", "c"}},
+ },
+ {
+ inputs: []nodeSpec{{"2", "a"}, {"3", "a"}, {"1", "a"}},
+ wants: []nodeSpec{{"1", "a"}, {"2", "a"}, {"3", "a"}},
+ },
+ } {
+ var (
+ inputs []report.Node
+ wants []report.Node
+ )
+ for _, spec := range testcase.inputs {
+ inputs = append(inputs, report.MakeNode().WithTopology(spec.topology).WithID(spec.id))
+ }
+ for _, spec := range testcase.wants {
+ wants = append(wants, report.MakeNode().WithTopology(spec.topology).WithID(spec.id))
+ }
+ if want, have := report.NodeSet(wants), report.MakeNodeSet(inputs...); !reflect.DeepEqual(want, have) {
+ t.Errorf("%#v: want %#v, have %#v", inputs, wants, have)
+ }
+ }
+}
+
+func BenchmarkMakeNodeSet(b *testing.B) {
+ nodes := []report.Node{}
+ for i := 1000; i >= 0; i-- {
+ node := report.MakeNode().WithID(fmt.Sprint(i)).WithMetadata(map[string]string{
+ "a": "1",
+ "b": "2",
+ })
+ nodes = append(nodes, node)
+ }
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ benchmarkResult = report.MakeNodeSet(nodes...)
+ }
+}
+
+func TestNodeSetAdd(t *testing.T) {
+ for _, testcase := range []struct {
+ input report.NodeSet
+ nodes []report.Node
+ want report.NodeSet
+ }{
+ {input: report.NodeSet(nil), nodes: []report.Node{}, want: report.NodeSet(nil)},
+ {
+ input: report.MakeNodeSet(),
+ nodes: []report.Node{},
+ want: report.MakeNodeSet(),
+ },
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ nodes: []report.Node{},
+ want: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ },
+ {
+ input: report.MakeNodeSet(),
+ nodes: []report.Node{report.MakeNode().WithID("a")},
+ want: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ },
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ nodes: []report.Node{report.MakeNode().WithID("a")},
+ want: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ },
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("b")),
+ nodes: []report.Node{report.MakeNode().WithID("a"), report.MakeNode().WithID("b")},
+ want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b")),
+ },
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ nodes: []report.Node{report.MakeNode().WithID("c"), report.MakeNode().WithID("b")},
+ want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b"), report.MakeNode().WithID("c")),
+ },
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("c")),
+ nodes: []report.Node{report.MakeNode().WithID("b"), report.MakeNode().WithID("b"), report.MakeNode().WithID("b")},
+ want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b"), report.MakeNode().WithID("c")),
+ },
+ } {
+ originalLen := len(testcase.input)
+ if want, have := testcase.want, testcase.input.Add(testcase.nodes...); !reflect.DeepEqual(want, have) {
+ t.Errorf("%v + %v: want %v, have %v", testcase.input, testcase.nodes, want, have)
+ }
+ if len(testcase.input) != originalLen {
+ t.Errorf("%v + %v: modified the original input!", testcase.input, testcase.nodes)
+ }
+ }
+}
+
+func BenchmarkNodeSetAdd(b *testing.B) {
+ n := report.MakeNodeSet()
+ for i := 0; i < 600; i++ {
+ n = n.Add(
+ report.MakeNode().WithID(fmt.Sprint(i)).WithMetadata(map[string]string{
+ "a": "1",
+ "b": "2",
+ }),
+ )
+ }
+
+ node := report.MakeNode().WithID("401.5").WithMetadata(map[string]string{
+ "a": "1",
+ "b": "2",
+ })
+
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ benchmarkResult = n.Add(node)
+ }
+}
+
+func TestNodeSetMerge(t *testing.T) {
+ for _, testcase := range []struct {
+ input report.NodeSet
+ other report.NodeSet
+ want report.NodeSet
+ }{
+ {input: report.NodeSet(nil), other: report.NodeSet(nil), want: report.NodeSet(nil)},
+ {input: report.MakeNodeSet(), other: report.MakeNodeSet(), want: report.MakeNodeSet()},
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ other: report.MakeNodeSet(),
+ want: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ },
+ {
+ input: report.MakeNodeSet(),
+ other: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ want: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ },
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ other: report.MakeNodeSet(report.MakeNode().WithID("b")),
+ want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b")),
+ },
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("b")),
+ other: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b")),
+ },
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ other: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ want: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ },
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("c")),
+ other: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b")),
+ want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b"), report.MakeNode().WithID("c")),
+ },
+ {
+ input: report.MakeNodeSet(report.MakeNode().WithID("b")),
+ other: report.MakeNodeSet(report.MakeNode().WithID("a")),
+ want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b")),
+ },
+ } {
+ originalLen := len(testcase.input)
+ if want, have := testcase.want, testcase.input.Merge(testcase.other); !reflect.DeepEqual(want, have) {
+ t.Errorf("%v + %v: want %v, have %v", testcase.input, testcase.other, want, have)
+ }
+ if len(testcase.input) != originalLen {
+ t.Errorf("%v + %v: modified the original input!", testcase.input, testcase.other)
+ }
+ }
+}
+
+func BenchmarkNodeSetMerge(b *testing.B) {
+ n, other := report.MakeNodeSet(), report.MakeNodeSet()
+ for i := 0; i < 600; i++ {
+ n = n.Add(
+ report.MakeNode().WithID(fmt.Sprint(i)).WithMetadata(map[string]string{
+ "a": "1",
+ "b": "2",
+ }),
+ )
+ }
+
+ for i := 400; i < 1000; i++ {
+ other = other.Add(
+ report.MakeNode().WithID(fmt.Sprint(i)).WithMetadata(map[string]string{
+ "c": "1",
+ "d": "2",
+ }),
+ )
+ }
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ benchmarkResult = n.Merge(other)
+ }
+}
diff --git a/report/report.go b/report/report.go
index 88876ea6d8..a63639d206 100644
--- a/report/report.go
+++ b/report/report.go
@@ -6,6 +6,19 @@ import (
"time"
)
+// Names of the various topologies.
+const (
+ Endpoint = "endpoint"
+ Address = "address"
+ Process = "process"
+ Container = "container"
+ Pod = "pod"
+ Service = "service"
+ ContainerImage = "container_image"
+ Host = "host"
+ Overlay = "overlay"
+)
+
// Report is the core data type. It's produced by probes, and consumed and
// stored by apps. It's composed of multiple topologies, each representing
// a different (related, but not equivalent) view of the network.
diff --git a/report/topology.go b/report/topology.go
index 567ae2c41e..8ad0649556 100644
--- a/report/topology.go
+++ b/report/topology.go
@@ -84,6 +84,8 @@ func (n Nodes) Merge(other Nodes) Nodes {
// given node in a given topology, along with the edges emanating from the
// node and metadata about those edges.
type Node struct {
+ ID string `json:"id,omitempty"`
+ Topology string `json:"topology,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
Counters Counters `json:"counters,omitempty"`
Sets Sets `json:"sets,omitempty"`
@@ -92,6 +94,7 @@ type Node struct {
Controls NodeControls `json:"controls,omitempty"`
Latest LatestMap `json:"latest,omitempty"`
Metrics Metrics `json:"metrics,omitempty"`
+ Parents Sets `json:"parents,omitempty"`
}
// MakeNode creates a new Node with no initial metadata.
@@ -99,12 +102,12 @@ func MakeNode() Node {
return Node{
Metadata: Metadata{},
Counters: Counters{},
- Sets: Sets{},
Adjacency: MakeIDList(),
Edges: EdgeMetadatas{},
Controls: MakeNodeControls(),
Latest: MakeLatestMap(),
Metrics: Metrics{},
+ Parents: Sets{},
}
}
@@ -113,6 +116,35 @@ func MakeNodeWith(m map[string]string) Node {
return MakeNode().WithMetadata(m)
}
+// WithID returns a fresh copy of n, with ID changed.
+func (n Node) WithID(id string) Node {
+ result := n.Copy()
+ result.ID = id
+ return result
+}
+
+// WithTopology returns a fresh copy of n, with ID changed.
+func (n Node) WithTopology(topology string) Node {
+ result := n.Copy()
+ result.Topology = topology
+ return result
+}
+
+// Before is used for sorting nodes by topology and id
+func (n Node) Before(other Node) bool {
+ return n.Topology < other.Topology || (n.Topology == other.Topology && n.ID < other.ID)
+}
+
+// Equal is used for comparing nodes by topology and id
+func (n Node) Equal(other Node) bool {
+ return n.Topology == other.Topology && n.ID == other.ID
+}
+
+// After is used for sorting nodes by topology and id
+func (n Node) After(other Node) bool {
+ return other.Topology < n.Topology || (other.Topology == n.Topology && other.ID < n.ID)
+}
+
// WithMetadata returns a fresh copy of n, with Metadata m merged in.
func (n Node) WithMetadata(m map[string]string) Node {
result := n.Copy()
@@ -130,8 +162,7 @@ func (n Node) WithCounters(c map[string]int) Node {
// WithSet returns a fresh copy of n, with set merged in at key.
func (n Node) WithSet(key string, set StringSet) Node {
result := n.Copy()
- existing := n.Sets[key]
- result.Sets[key] = existing.Merge(set)
+ result.Sets = result.Sets.Merge(Sets{key: set})
return result
}
@@ -186,9 +217,18 @@ func (n Node) WithLatest(k string, ts time.Time, v string) Node {
return result
}
+// WithParents returns a fresh copy of n, with sets merged in.
+func (n Node) WithParents(parents Sets) Node {
+ result := n.Copy()
+ result.Parents = result.Parents.Merge(parents)
+ return result
+}
+
// Copy returns a value copy of the Node.
func (n Node) Copy() Node {
cp := MakeNode()
+ cp.ID = n.ID
+ cp.Topology = n.Topology
cp.Metadata = n.Metadata.Copy()
cp.Counters = n.Counters.Copy()
cp.Sets = n.Sets.Copy()
@@ -197,6 +237,7 @@ func (n Node) Copy() Node {
cp.Controls = n.Controls.Copy()
cp.Latest = n.Latest.Copy()
cp.Metrics = n.Metrics.Copy()
+ cp.Parents = n.Parents.Copy()
return cp
}
@@ -204,6 +245,12 @@ func (n Node) Copy() Node {
// fresh node.
func (n Node) Merge(other Node) Node {
cp := n.Copy()
+ if cp.ID == "" {
+ cp.ID = other.ID
+ }
+ if cp.Topology == "" {
+ cp.Topology = other.Topology
+ }
cp.Metadata = cp.Metadata.Merge(other.Metadata)
cp.Counters = cp.Counters.Merge(other.Counters)
cp.Sets = cp.Sets.Merge(other.Sets)
@@ -212,6 +259,7 @@ func (n Node) Merge(other Node) Node {
cp.Controls = cp.Controls.Merge(other.Controls)
cp.Latest = cp.Latest.Merge(other.Latest)
cp.Metrics = cp.Metrics.Merge(other.Metrics)
+ cp.Parents = cp.Parents.Merge(other.Parents)
return cp
}
@@ -268,6 +316,9 @@ type Sets map[string]StringSet
func (s Sets) Merge(other Sets) Sets {
result := s.Copy()
for k, v := range other {
+ if result == nil {
+ result = Sets{}
+ }
result[k] = result[k].Merge(v)
}
return result
@@ -275,6 +326,9 @@ func (s Sets) Merge(other Sets) Sets {
// Copy returns a value copy of the sets map.
func (s Sets) Copy() Sets {
+ if s == nil {
+ return s
+ }
result := Sets{}
for k, v := range s {
result[k] = v.Copy()
@@ -321,6 +375,21 @@ func (s StringSet) Add(strs ...string) StringSet {
return s
}
+// Remove removes the strings from the StringSet. Remove is the only valid way
+// to shrink a StringSet. Remove returns the StringSet to enable chaining.
+func (s StringSet) Remove(strs ...string) StringSet {
+ for _, str := range strs {
+ i := sort.Search(len(s), func(i int) bool { return s[i] >= str })
+ if i >= len(s) || s[i] != str {
+ // The list does not have the element.
+ continue
+ }
+ // has the element, remove it.
+ s = append(s[:i], s[i+1:]...)
+ }
+ return s
+}
+
// Merge combines the two StringSets and returns a new result.
func (s StringSet) Merge(other StringSet) StringSet {
switch {
diff --git a/report/topology_test.go b/report/topology_test.go
index c965f9b7d0..19cbf244b7 100644
--- a/report/topology_test.go
+++ b/report/topology_test.go
@@ -45,6 +45,27 @@ func TestStringSetAdd(t *testing.T) {
}
}
+func TestStringSetRemove(t *testing.T) {
+ for _, testcase := range []struct {
+ input report.StringSet
+ strs []string
+ want report.StringSet
+ }{
+ {input: report.StringSet(nil), strs: []string{}, want: report.StringSet(nil)},
+ {input: report.MakeStringSet(), strs: []string{}, want: report.MakeStringSet()},
+ {input: report.MakeStringSet("a"), strs: []string{}, want: report.MakeStringSet("a")},
+ {input: report.MakeStringSet(), strs: []string{"a"}, want: report.MakeStringSet()},
+ {input: report.MakeStringSet("a"), strs: []string{"a"}, want: report.StringSet{}},
+ {input: report.MakeStringSet("b"), strs: []string{"a", "b"}, want: report.StringSet{}},
+ {input: report.MakeStringSet("a"), strs: []string{"c", "b"}, want: report.MakeStringSet("a")},
+ {input: report.MakeStringSet("a", "c"), strs: []string{"b", "b", "b"}, want: report.MakeStringSet("a", "c")},
+ } {
+ if want, have := testcase.want, testcase.input.Remove(testcase.strs...); !reflect.DeepEqual(want, have) {
+ t.Errorf("%v - %v: want %#v, have %#v", testcase.input, testcase.strs, want, have)
+ }
+ }
+}
+
func TestStringSetMerge(t *testing.T) {
for _, testcase := range []struct {
input report.StringSet
@@ -66,3 +87,25 @@ func TestStringSetMerge(t *testing.T) {
}
}
}
+
+func TestNodeOrdering(t *testing.T) {
+ ids := [][2]string{{}, {"a", "0"}, {"a", "1"}, {"b", "0"}, {"b", "1"}, {"c", "3"}}
+ nodes := []report.Node{}
+ for _, id := range ids {
+ nodes = append(nodes, report.MakeNode().WithTopology(id[0]).WithID(id[1]))
+ }
+
+ for i, node := range nodes {
+ if !node.Equal(node) {
+ t.Errorf("Expected %q %q == %q %q, but was not", node.Topology, node.ID, node.Topology, node.ID)
+ }
+ if i > 0 {
+ if !node.After(nodes[i-1]) {
+ t.Errorf("Expected %q %q > %q %q, but was not", node.Topology, node.ID, nodes[i-1].Topology, nodes[i-1].ID)
+ }
+ if !nodes[i-1].Before(node) {
+ t.Errorf("Expected %q %q < %q %q, but was not", nodes[i-1].Topology, nodes[i-1].ID, node.Topology, node.ID)
+ }
+ }
+ }
+}
diff --git a/test/fixture/report_fixture.go b/test/fixture/report_fixture.go
index bf4f2eeed9..4239bc5f93 100644
--- a/test/fixture/report_fixture.go
+++ b/test/fixture/report_fixture.go
@@ -74,13 +74,13 @@ var (
ClientContainerID = "a1b2c3d4e5"
ServerContainerID = "5e4d3c2b1a"
- ClientContainerNodeID = report.MakeContainerNodeID(ClientHostID, ClientContainerID)
- ServerContainerNodeID = report.MakeContainerNodeID(ServerHostID, ServerContainerID)
+ ClientContainerNodeID = report.MakeContainerNodeID(ClientContainerID)
+ ServerContainerNodeID = report.MakeContainerNodeID(ServerContainerID)
ClientContainerImageID = "imageid123"
ServerContainerImageID = "imageid456"
- ClientContainerImageNodeID = report.MakeContainerNodeID(ClientHostID, ClientContainerImageID)
- ServerContainerImageNodeID = report.MakeContainerNodeID(ServerHostID, ServerContainerImageID)
+ ClientContainerImageNodeID = report.MakeContainerImageNodeID(ClientContainerImageID)
+ ServerContainerImageNodeID = report.MakeContainerImageNodeID(ServerContainerImageID)
ClientContainerImageName = "image/client"
ServerContainerImageName = "image/server"
@@ -91,12 +91,13 @@ var (
UnknownAddress3NodeID = report.MakeAddressNodeID(ServerHostID, UnknownClient3IP)
RandomAddressNodeID = report.MakeAddressNodeID(ServerHostID, RandomClientIP) // this should become an internet node
- ClientPodID = "ping/pong-a"
- ServerPodID = "ping/pong-b"
- ClientPodNodeID = report.MakePodNodeID("ping", "pong-a")
- ServerPodNodeID = report.MakePodNodeID("ping", "pong-b")
- ServiceID = "ping/pongservice"
- ServiceNodeID = report.MakeServiceNodeID("ping", "pongservice")
+ KubernetesNamespace = "ping"
+ ClientPodID = "ping/pong-a"
+ ServerPodID = "ping/pong-b"
+ ClientPodNodeID = report.MakePodNodeID(KubernetesNamespace, "pong-a")
+ ServerPodNodeID = report.MakePodNodeID(KubernetesNamespace, "pong-b")
+ ServiceID = "ping/pongservice"
+ ServiceNodeID = report.MakeServiceNodeID(KubernetesNamespace, "pongservice")
LoadMetric = report.MakeMetric().Add(Now, 0.01).WithFirst(Now.Add(-15 * time.Second))
LoadMetrics = report.Metrics{
@@ -105,6 +106,10 @@ var (
host.Load15: LoadMetric,
}
+ CPUMetric = report.MakeMetric().Add(Now, 0.01).WithFirst(Now.Add(-15 * time.Second))
+
+ MemoryMetric = report.MakeMetric().Add(Now, 0.01).WithFirst(Now.Add(-15 * time.Second))
+
Report = report.Report{
Endpoint: report.Topology{
Nodes: report.Nodes{
@@ -200,23 +205,40 @@ var (
process.Name: Client1Name,
docker.ContainerID: ClientContainerID,
report.HostNodeID: ClientHostNodeID,
+ }).WithID(ClientProcess1NodeID).WithTopology(report.Process).WithParents(report.Sets{
+ "host": report.MakeStringSet(ClientHostNodeID),
+ "container": report.MakeStringSet(ClientContainerNodeID),
+ "container_image": report.MakeStringSet(ClientContainerImageNodeID),
+ }).WithMetrics(report.Metrics{
+ process.CPUUsage: CPUMetric,
+ process.MemoryUsage: MemoryMetric,
}),
ClientProcess2NodeID: report.MakeNodeWith(map[string]string{
process.PID: Client2PID,
process.Name: Client2Name,
docker.ContainerID: ClientContainerID,
report.HostNodeID: ClientHostNodeID,
+ }).WithID(ClientProcess2NodeID).WithTopology(report.Process).WithParents(report.Sets{
+ "host": report.MakeStringSet(ClientHostNodeID),
+ "container": report.MakeStringSet(ClientContainerNodeID),
+ "container_image": report.MakeStringSet(ClientContainerImageNodeID),
}),
ServerProcessNodeID: report.MakeNodeWith(map[string]string{
process.PID: ServerPID,
process.Name: ServerName,
docker.ContainerID: ServerContainerID,
report.HostNodeID: ServerHostNodeID,
+ }).WithID(ServerProcessNodeID).WithTopology(report.Process).WithParents(report.Sets{
+ "host": report.MakeStringSet(ServerHostNodeID),
+ "container": report.MakeStringSet(ServerContainerNodeID),
+ "container_image": report.MakeStringSet(ServerContainerImageNodeID),
}),
NonContainerProcessNodeID: report.MakeNodeWith(map[string]string{
process.PID: NonContainerPID,
process.Name: NonContainerName,
report.HostNodeID: ServerHostNodeID,
+ }).WithID(NonContainerProcessNodeID).WithTopology(report.Process).WithParents(report.Sets{
+ "host": report.MakeStringSet(ServerHostNodeID),
}),
},
},
@@ -228,17 +250,36 @@ var (
docker.ImageID: ClientContainerImageID,
report.HostNodeID: ClientHostNodeID,
docker.LabelPrefix + "io.kubernetes.pod.name": ClientPodID,
- }).WithLatest(docker.ContainerState, Now, docker.StateRunning),
+ kubernetes.PodID: ClientPodID,
+ kubernetes.Namespace: KubernetesNamespace,
+ }).WithLatest(docker.ContainerState, Now, docker.StateRunning).WithID(ClientContainerNodeID).WithTopology(report.Container).WithParents(report.Sets{
+ "host": report.MakeStringSet(ClientHostNodeID),
+ "container_image": report.MakeStringSet(ClientContainerImageNodeID),
+ "pod": report.MakeStringSet(ClientPodID),
+ }).WithMetrics(report.Metrics{
+ docker.CPUTotalUsage: CPUMetric,
+ docker.MemoryUsage: MemoryMetric,
+ }),
ServerContainerNodeID: report.MakeNodeWith(map[string]string{
docker.ContainerID: ServerContainerID,
docker.ContainerName: "task-name-5-server-aceb93e2f2b797caba01",
+ docker.ContainerState: "running",
docker.ImageID: ServerContainerImageID,
report.HostNodeID: ServerHostNodeID,
docker.LabelPrefix + render.AmazonECSContainerNameLabel: "server",
docker.LabelPrefix + "foo1": "bar1",
docker.LabelPrefix + "foo2": "bar2",
docker.LabelPrefix + "io.kubernetes.pod.name": ServerPodID,
- }).WithLatest(docker.ContainerState, Now, docker.StateRunning),
+ kubernetes.PodID: ServerPodID,
+ kubernetes.Namespace: KubernetesNamespace,
+ }).WithLatest(docker.ContainerState, Now, docker.StateRunning).WithID(ServerContainerNodeID).WithTopology(report.Container).WithParents(report.Sets{
+ "host": report.MakeStringSet(ServerHostNodeID),
+ "container_image": report.MakeStringSet(ServerContainerImageNodeID),
+ "pod": report.MakeStringSet(ServerPodID),
+ }).WithMetrics(report.Metrics{
+ docker.CPUTotalUsage: CPUMetric,
+ docker.MemoryUsage: MemoryMetric,
+ }),
},
},
ContainerImage: report.Topology{
@@ -247,14 +288,18 @@ var (
docker.ImageID: ClientContainerImageID,
docker.ImageName: ClientContainerImageName,
report.HostNodeID: ClientHostNodeID,
- }),
+ }).WithParents(report.Sets{
+ "host": report.MakeStringSet(ClientHostNodeID),
+ }).WithID(ClientContainerImageNodeID).WithTopology(report.ContainerImage),
ServerContainerImageNodeID: report.MakeNodeWith(map[string]string{
docker.ImageID: ServerContainerImageID,
docker.ImageName: ServerContainerImageName,
report.HostNodeID: ServerHostNodeID,
docker.LabelPrefix + "foo1": "bar1",
docker.LabelPrefix + "foo2": "bar2",
- }),
+ }).WithParents(report.Sets{
+ "host": report.MakeStringSet(ServerHostNodeID),
+ }).WithID(ServerContainerImageNodeID).WithTopology(report.ContainerImage),
},
},
Address: report.Topology{
@@ -294,23 +339,27 @@ var (
"host_name": ClientHostName,
"os": "Linux",
report.HostNodeID: ClientHostNodeID,
- }).WithSets(report.Sets{
+ }).WithID(ClientHostNodeID).WithTopology(report.Host).WithSets(report.Sets{
host.LocalNetworks: report.MakeStringSet("10.10.10.0/24"),
}).WithMetrics(report.Metrics{
- host.Load1: LoadMetric,
- host.Load5: LoadMetric,
- host.Load15: LoadMetric,
+ host.CPUUsage: CPUMetric,
+ host.MemUsage: MemoryMetric,
+ host.Load1: LoadMetric,
+ host.Load5: LoadMetric,
+ host.Load15: LoadMetric,
}),
ServerHostNodeID: report.MakeNodeWith(map[string]string{
"host_name": ServerHostName,
"os": "Linux",
report.HostNodeID: ServerHostNodeID,
- }).WithSets(report.Sets{
+ }).WithID(ServerHostNodeID).WithTopology(report.Host).WithSets(report.Sets{
host.LocalNetworks: report.MakeStringSet("10.10.10.0/24"),
}).WithMetrics(report.Metrics{
- host.Load1: LoadMetric,
- host.Load5: LoadMetric,
- host.Load15: LoadMetric,
+ host.CPUUsage: CPUMetric,
+ host.MemUsage: MemoryMetric,
+ host.Load1: LoadMetric,
+ host.Load5: LoadMetric,
+ host.Load15: LoadMetric,
}),
},
},
@@ -319,16 +368,22 @@ var (
ClientPodNodeID: report.MakeNodeWith(map[string]string{
kubernetes.PodID: ClientPodID,
kubernetes.PodName: "pong-a",
- kubernetes.Namespace: "ping",
+ kubernetes.Namespace: KubernetesNamespace,
kubernetes.PodContainerIDs: ClientContainerID,
kubernetes.ServiceIDs: ServiceID,
+ }).WithID(ClientPodNodeID).WithTopology(report.Pod).WithParents(report.Sets{
+ "host": report.MakeStringSet(ClientHostNodeID),
+ "service": report.MakeStringSet(ServiceID),
}),
ServerPodNodeID: report.MakeNodeWith(map[string]string{
kubernetes.PodID: ServerPodID,
kubernetes.PodName: "pong-b",
- kubernetes.Namespace: "ping",
+ kubernetes.Namespace: KubernetesNamespace,
kubernetes.PodContainerIDs: ServerContainerID,
kubernetes.ServiceIDs: ServiceID,
+ }).WithID(ServerPodNodeID).WithTopology(report.Pod).WithParents(report.Sets{
+ "host": report.MakeStringSet(ServerHostNodeID),
+ "service": report.MakeStringSet(ServiceID),
}),
},
},
@@ -338,7 +393,7 @@ var (
kubernetes.ServiceID: ServiceID,
kubernetes.ServiceName: "pongservice",
kubernetes.Namespace: "ping",
- }),
+ }).WithID(ServiceNodeID).WithTopology(report.Service),
},
},
Sampling: report.Sampling{