mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-02 17:50:39 +00:00
Merge pull request #752 from weaveworks/301-details-pain
Details panel redesign
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/weaveworks/scope/render"
|
||||
"github.com/weaveworks/scope/render/detailed"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,7 +22,7 @@ type APITopology struct {
|
||||
|
||||
// APINode is returned by the /api/topology/{name}/{id} handler.
|
||||
type APINode struct {
|
||||
Node render.DetailedNode `json:"node"`
|
||||
Node detailed.Node `json:"node"`
|
||||
}
|
||||
|
||||
// Full topology.
|
||||
@@ -59,7 +60,7 @@ func handleNode(nodeID string) func(Reporter, render.Renderer, http.ResponseWrit
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
respondWith(w, http.StatusOK, APINode{Node: render.MakeDetailedNode(rpt, node)})
|
||||
respondWith(w, http.StatusOK, APINode{Node: detailed.MakeNode(rpt, node)})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,7 @@ func TestAPITopologyApplications(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
equals(t, expected.ServerProcessID, node.Node.ID)
|
||||
equals(t, "apache", node.Node.LabelMajor)
|
||||
equals(t, fmt.Sprintf("%s (server:%s)", fixture.ServerHostID, fixture.ServerPID), node.Node.LabelMinor)
|
||||
equals(t, "apache", node.Node.Label)
|
||||
equals(t, false, node.Node.Pseudo)
|
||||
// Let's not unit-test the specific content of the detail tables
|
||||
}
|
||||
@@ -96,8 +95,7 @@ func TestAPITopologyApplications(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
equals(t, fixture.Client1Name, node.Node.ID)
|
||||
equals(t, fixture.Client1Name, node.Node.LabelMajor)
|
||||
equals(t, "2 processes", node.Node.LabelMinor)
|
||||
equals(t, fixture.Client1Name, node.Node.Label)
|
||||
equals(t, false, node.Node.Pseudo)
|
||||
// Let's not unit-test the specific content of the detail tables
|
||||
}
|
||||
@@ -125,8 +123,7 @@ func TestAPITopologyHosts(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
equals(t, expected.ServerHostRenderedID, node.Node.ID)
|
||||
equals(t, "server", node.Node.LabelMajor)
|
||||
equals(t, "hostname.com", node.Node.LabelMinor)
|
||||
equals(t, "server", node.Node.Label)
|
||||
equals(t, false, node.Node.Pseudo)
|
||||
// Let's not unit-test the specific content of the detail tables
|
||||
}
|
||||
|
||||
@@ -26,14 +26,22 @@ export function changeTopologyOption(option, value, topologyId) {
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
getNodeDetails(
|
||||
AppStore.getCurrentTopologyUrl(),
|
||||
AppStore.getSelectedNodeId()
|
||||
AppStore.getTopologyUrlsById(),
|
||||
AppStore.getNodeDetails()
|
||||
);
|
||||
}
|
||||
|
||||
export function clickCloseDetails() {
|
||||
export function clickBackground() {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_DETAILS
|
||||
type: ActionTypes.CLICK_BACKGROUND
|
||||
});
|
||||
updateRoute();
|
||||
}
|
||||
|
||||
export function clickCloseDetails(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_DETAILS,
|
||||
nodeId
|
||||
});
|
||||
updateRoute();
|
||||
}
|
||||
@@ -49,15 +57,45 @@ export function clickCloseTerminal(pipeId, closePipe) {
|
||||
updateRoute();
|
||||
}
|
||||
|
||||
export function clickNode(nodeId) {
|
||||
export function clickNode(nodeId, label, origin) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_NODE,
|
||||
nodeId: nodeId
|
||||
origin,
|
||||
label,
|
||||
nodeId
|
||||
});
|
||||
updateRoute();
|
||||
getNodeDetails(
|
||||
AppStore.getTopologyUrlsById(),
|
||||
AppStore.getNodeDetails()
|
||||
);
|
||||
}
|
||||
|
||||
export function clickRelative(nodeId, topologyId, label, origin) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_RELATIVE,
|
||||
label,
|
||||
origin,
|
||||
nodeId,
|
||||
topologyId
|
||||
});
|
||||
updateRoute();
|
||||
getNodeDetails(
|
||||
AppStore.getTopologyUrlsById(),
|
||||
AppStore.getNodeDetails()
|
||||
);
|
||||
}
|
||||
|
||||
export function clickShowTopologyForNode(topologyId, nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE,
|
||||
topologyId,
|
||||
nodeId
|
||||
});
|
||||
updateRoute();
|
||||
getNodesDelta(
|
||||
AppStore.getCurrentTopologyUrl(),
|
||||
AppStore.getSelectedNodeId()
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,7 +159,7 @@ export function hitEsc() {
|
||||
type: ActionTypes.CLICK_CLOSE_TERMINAL,
|
||||
pipeId: controlPipe.id
|
||||
});
|
||||
// Dont deselect node on ESC if there is a controlPipe (keep terminal open)
|
||||
// Dont deselect node on ESC if there is a controlPipe (keep terminal open)
|
||||
} else if (AppStore.getSelectedNodeId() && !controlPipe) {
|
||||
AppDispatcher.dispatch({type: ActionTypes.DESELECT_NODE});
|
||||
}
|
||||
@@ -181,8 +219,8 @@ export function receiveTopologies(topologies) {
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
getNodeDetails(
|
||||
AppStore.getCurrentTopologyUrl(),
|
||||
AppStore.getSelectedNodeId()
|
||||
AppStore.getTopologyUrlsById(),
|
||||
AppStore.getNodeDetails()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,6 +233,7 @@ export function receiveApiDetails(apiDetails) {
|
||||
}
|
||||
|
||||
export function receiveControlPipeFromParams(pipeId, rawTty) {
|
||||
// TODO add nodeId
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_CONTROL_PIPE,
|
||||
pipeId: pipeId,
|
||||
@@ -216,6 +255,7 @@ export function receiveControlPipe(pipeId, nodeId, rawTty) {
|
||||
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_CONTROL_PIPE,
|
||||
nodeId: nodeId,
|
||||
pipeId: pipeId,
|
||||
rawTty: rawTty
|
||||
});
|
||||
@@ -238,6 +278,13 @@ export function receiveError(errorUrl) {
|
||||
});
|
||||
}
|
||||
|
||||
export function receiveNotFound(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
nodeId,
|
||||
type: ActionTypes.RECEIVE_NOT_FOUND
|
||||
});
|
||||
}
|
||||
|
||||
export function route(state) {
|
||||
AppDispatcher.dispatch({
|
||||
state: state,
|
||||
@@ -251,7 +298,7 @@ export function route(state) {
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
getNodeDetails(
|
||||
AppStore.getCurrentTopologyUrl(),
|
||||
AppStore.getSelectedNodeId()
|
||||
AppStore.getTopologyUrlsById(),
|
||||
AppStore.getNodeDetails()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
|
||||
import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
|
||||
@@ -99,14 +100,14 @@ export default class Node extends React.Component {
|
||||
|
||||
handleMouseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
clickNode(ev.currentTarget.id);
|
||||
clickNode(this.props.id, this.props.label, ReactDOM.findDOMNode(this).getBoundingClientRect());
|
||||
}
|
||||
|
||||
handleMouseEnter(ev) {
|
||||
enterNode(ev.currentTarget.id);
|
||||
handleMouseEnter() {
|
||||
enterNode(this.props.id);
|
||||
}
|
||||
|
||||
handleMouseLeave(ev) {
|
||||
leaveNode(ev.currentTarget.id);
|
||||
handleMouseLeave() {
|
||||
leaveNode(this.props.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import { Map as makeMap } from 'immutable';
|
||||
import timely from 'timely';
|
||||
|
||||
import { clickCloseDetails } from '../actions/app-actions';
|
||||
import { clickBackground } from '../actions/app-actions';
|
||||
import AppStore from '../stores/app-store';
|
||||
import Edge from './edge';
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
@@ -357,7 +357,7 @@ export default class NodesChart extends React.Component {
|
||||
|
||||
handleMouseClick() {
|
||||
if (!this.isZooming) {
|
||||
clickCloseDetails();
|
||||
clickBackground();
|
||||
} else {
|
||||
this.isZooming = false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import TestUtils from 'react/lib/ReactTestUtils';
|
||||
|
||||
jest.dontMock('../../dispatcher/app-dispatcher');
|
||||
jest.dontMock('../node-details.js');
|
||||
jest.dontMock('../node-details/node-details-controls.js');
|
||||
jest.dontMock('../node-details/node-details-relatives.js');
|
||||
jest.dontMock('../node-details/node-details-table.js');
|
||||
jest.dontMock('../../utils/color-utils');
|
||||
jest.dontMock('../../utils/title-utils');
|
||||
|
||||
@@ -22,14 +25,14 @@ describe('NodeDetails', () => {
|
||||
});
|
||||
|
||||
it('shows n/a when node was not found', () => {
|
||||
const c = TestUtils.renderIntoDocument(<NodeDetails nodes={nodes} nodeId={nodeId} />);
|
||||
const c = TestUtils.renderIntoDocument(<NodeDetails notFound />);
|
||||
const notFound = TestUtils.findRenderedDOMComponentWithClass(c, 'node-details-header-notavailable');
|
||||
expect(notFound).toBeDefined();
|
||||
});
|
||||
|
||||
it('show label of node with title', () => {
|
||||
nodes = nodes.set(nodeId, Immutable.fromJS({id: nodeId}));
|
||||
details = {label_major: 'Node 1', tables: []};
|
||||
details = {label: 'Node 1'};
|
||||
const c = TestUtils.renderIntoDocument(<NodeDetails nodes={nodes}
|
||||
nodeId={nodeId} details={details} />);
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ function getStateFromStores() {
|
||||
highlightedEdgeIds: AppStore.getHighlightedEdgeIds(),
|
||||
highlightedNodeIds: AppStore.getHighlightedNodeIds(),
|
||||
hostname: AppStore.getHostname(),
|
||||
selectedNodeId: AppStore.getSelectedNodeId(),
|
||||
nodeDetails: AppStore.getNodeDetails(),
|
||||
nodes: AppStore.getNodes(),
|
||||
selectedNodeId: AppStore.getSelectedNodeId(),
|
||||
topologies: AppStore.getTopologies(),
|
||||
topologiesLoaded: AppStore.isTopologiesLoaded(),
|
||||
version: AppStore.getVersion(),
|
||||
@@ -70,7 +70,7 @@ export default class App extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const showingDetails = this.state.selectedNodeId;
|
||||
const showingDetails = this.state.nodeDetails.size > 0;
|
||||
const showingTerminal = this.state.controlPipe;
|
||||
const footer = `Version ${this.state.version} on ${this.state.hostname}`;
|
||||
// width of details panel blocking a view
|
||||
@@ -81,13 +81,12 @@ export default class App extends React.Component {
|
||||
<div className="app">
|
||||
{showingDebugToolbar() && <DebugToolbar />}
|
||||
{showingDetails && <Details nodes={this.state.nodes}
|
||||
nodeId={this.state.selectedNodeId}
|
||||
controlStatus={this.state.controlStatus[this.state.selectedNodeId]}
|
||||
controlStatus={this.state.controlStatus}
|
||||
details={this.state.nodeDetails} />}
|
||||
|
||||
{showingTerminal && <EmbeddedTerminal
|
||||
pipe={this.state.controlPipe}
|
||||
nodeId={this.state.selectedNodeId}
|
||||
nodeId={this.state.controlPipe.nodeId}
|
||||
nodes={this.state.nodes} />}
|
||||
|
||||
<div className="header">
|
||||
|
||||
58
client/app/scripts/components/details-card.js
Normal file
58
client/app/scripts/components/details-card.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
|
||||
import NodeDetails from './node-details';
|
||||
|
||||
// card dimensions in px
|
||||
const marginTop = 24;
|
||||
const marginBottom = 48;
|
||||
const marginRight = 36;
|
||||
const panelWidth = 420;
|
||||
const offset = 8;
|
||||
|
||||
export default class DetailsCard extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
mounted: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
this.setState({mounted: true});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let transform;
|
||||
const origin = this.props.origin;
|
||||
const panelHeight = window.innerHeight - marginBottom - marginTop;
|
||||
if (origin && !this.state.mounted) {
|
||||
// render small panel near origin, will transition into normal panel after being mounted
|
||||
const scaleY = origin.height / (window.innerHeight - marginBottom - marginTop) / 2;
|
||||
const scaleX = origin.width / panelWidth / 2;
|
||||
const centerX = window.innerWidth - marginRight - (panelWidth / 2);
|
||||
const centerY = (panelHeight) / 2 + marginTop;
|
||||
const dx = (origin.left + origin.width / 2) - centerX;
|
||||
const dy = (origin.top + origin.height / 2) - centerY;
|
||||
transform = `translate(${dx}px, ${dy}px) scale(${scaleX},${scaleY})`;
|
||||
} else {
|
||||
// stack effect: shift top cards to the left, shrink lower cards vertically
|
||||
const shiftX = -1 * this.props.index * offset;
|
||||
const position = this.props.cardCount - this.props.index - 1; // reverse index
|
||||
const scaleY = position === 0 ? 1 : (panelHeight - 2 * offset * position) / panelHeight;
|
||||
if (scaleY !== 1) {
|
||||
transform = `translateX(${shiftX}px) scaleY(${scaleY})`;
|
||||
} else {
|
||||
// scale(1) is sometimes blurry
|
||||
transform = `translateX(${shiftX}px)`;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="details-wrapper" style={{transform}}>
|
||||
<NodeDetails nodeId={this.props.id} key={this.props.id} {...this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import { clickCloseDetails } from '../actions/app-actions';
|
||||
import NodeDetails from './node-details';
|
||||
import DetailsCard from './details-card';
|
||||
|
||||
export default class Details extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleClickClose = this.handleClickClose.bind(this);
|
||||
}
|
||||
|
||||
handleClickClose(ev) {
|
||||
ev.preventDefault();
|
||||
clickCloseDetails();
|
||||
}
|
||||
|
||||
// render all details as cards, later cards go on top
|
||||
render() {
|
||||
const details = this.props.details.toIndexedSeq();
|
||||
return (
|
||||
<div id="details">
|
||||
<div className="details-wrapper">
|
||||
<div className="details-tools-wrapper">
|
||||
<div className="details-tools">
|
||||
<span className="fa fa-close" onClick={this.handleClickClose} />
|
||||
</div>
|
||||
</div>
|
||||
<NodeDetails {...this.props} />
|
||||
</div>
|
||||
<div className="details">
|
||||
{details.map((obj, index) => {
|
||||
return (
|
||||
<DetailsCard key={obj.id} index={index} cardCount={details.size}
|
||||
nodes={this.props.nodes}
|
||||
nodeControlStatus={this.props.controlStatus[obj.id]} {...obj} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getNodeColor, getNodeColorDark } from '../utils/color-utils';
|
||||
import Terminal from './terminal';
|
||||
|
||||
export default function EmeddedTerminal({pipe, nodeId, nodes}) {
|
||||
const node = nodes.get(nodeId && nodeId.split(';').pop());
|
||||
const node = nodes.get(nodeId);
|
||||
const titleBarColor = node && getNodeColorDark(node.get('rank'), node.get('label_major'));
|
||||
const statusBarColor = node && getNodeColor(node.get('rank'), node.get('label_major'));
|
||||
const title = node && node.get('label_major');
|
||||
|
||||
@@ -2,11 +2,32 @@ import _ from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import NodeDetailsControls from './node-details/node-details-controls';
|
||||
import NodeDetailsHealth from './node-details/node-details-health';
|
||||
import NodeDetailsInfo from './node-details/node-details-info';
|
||||
import NodeDetailsRelatives from './node-details/node-details-relatives';
|
||||
import NodeDetailsTable from './node-details/node-details-table';
|
||||
import { brightenColor, getNodeColorDark } from '../utils/color-utils';
|
||||
import { clickCloseDetails, clickShowTopologyForNode } from '../actions/app-actions';
|
||||
import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color-utils';
|
||||
import { resetDocumentTitle, setDocumentTitle } from '../utils/title-utils';
|
||||
|
||||
export default class NodeDetails extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleClickClose = this.handleClickClose.bind(this);
|
||||
this.handleShowTopologyForNode = this.handleShowTopologyForNode.bind(this);
|
||||
}
|
||||
|
||||
handleClickClose(ev) {
|
||||
ev.preventDefault();
|
||||
clickCloseDetails(this.props.nodeId);
|
||||
}
|
||||
|
||||
handleShowTopologyForNode(ev) {
|
||||
ev.preventDefault();
|
||||
clickShowTopologyForNode(this.props.topologyId, this.props.nodeId);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateTitle();
|
||||
}
|
||||
@@ -15,9 +36,25 @@ export default class NodeDetails extends React.Component {
|
||||
resetDocumentTitle();
|
||||
}
|
||||
|
||||
renderTools() {
|
||||
const showSwitchTopology = this.props.index > 0;
|
||||
const topologyTitle = `View ${this.props.label} in ${this.props.topologyId}`;
|
||||
|
||||
return (
|
||||
<div className="node-details-tools-wrapper">
|
||||
<div className="node-details-tools">
|
||||
{showSwitchTopology && <span title={topologyTitle} className="fa fa-exchange" onClick={this.handleShowTopologyForNode} />}
|
||||
<span title="Close details" className="fa fa-close" onClick={this.handleClickClose} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
const node = this.props.nodes.get(this.props.nodeId);
|
||||
const nodeColor = getNodeColorDark(node.get('rank'), node.get('label_major'));
|
||||
const label = node ? node.get('label_major') : this.props.label;
|
||||
const nodeColor = node ? getNodeColorDark(node.get('rank'), label) : getNeutralColor();
|
||||
const tools = this.renderTools();
|
||||
const styles = {
|
||||
header: {
|
||||
'backgroundColor': nodeColor
|
||||
@@ -26,13 +63,14 @@ export default class NodeDetails extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="node-details">
|
||||
{tools}
|
||||
<div className="node-details-header" style={styles.header}>
|
||||
<div className="node-details-header-wrapper">
|
||||
<h2 className="node-details-header-label truncate">
|
||||
{node.get('label_major')}
|
||||
{label}
|
||||
</h2>
|
||||
<div className="node-details-header-label-minor truncate">
|
||||
{node.get('label_minor')}
|
||||
<div className="node-details-relatives truncate">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,38 +84,42 @@ export default class NodeDetails extends React.Component {
|
||||
}
|
||||
|
||||
renderNotAvailable() {
|
||||
const tools = this.renderTools();
|
||||
return (
|
||||
<div className="node-details">
|
||||
{tools}
|
||||
<div className="node-details-header node-details-header-notavailable">
|
||||
<div className="node-details-header-wrapper">
|
||||
<h2 className="node-details-header-label">
|
||||
n/a
|
||||
{this.props.label}
|
||||
</h2>
|
||||
<div className="node-details-header-label-minor truncate">
|
||||
{this.props.nodeId}
|
||||
<div className="node-details-relatives truncate">
|
||||
n/a
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="node-details-content">
|
||||
<p className="node-details-content-info">
|
||||
This node is not visible to Scope anymore.
|
||||
The node will re-appear if it communicates again.
|
||||
<strong>{this.props.label}</strong> is not visible to Scope when it is not communicating.
|
||||
Details will become available here when it communicates again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const details = this.props.details;
|
||||
const nodeExists = this.props.nodes && this.props.nodes.has(this.props.nodeId);
|
||||
renderTable(table) {
|
||||
const key = _.snakeCase(table.title);
|
||||
return <NodeDetailsTable title={table.title} key={key} rows={table.rows} isNumeric={table.numeric} />;
|
||||
}
|
||||
|
||||
if (details) {
|
||||
return this.renderDetails();
|
||||
render() {
|
||||
if (this.props.notFound) {
|
||||
return this.renderNotAvailable();
|
||||
}
|
||||
|
||||
if (!nodeExists) {
|
||||
return this.renderNotAvailable();
|
||||
if (this.props.details) {
|
||||
return this.renderDetails();
|
||||
}
|
||||
|
||||
return this.renderLoading();
|
||||
@@ -85,8 +127,11 @@ export default class NodeDetails extends React.Component {
|
||||
|
||||
renderDetails() {
|
||||
const details = this.props.details;
|
||||
const showSummary = details.metadata !== undefined || details.metrics !== undefined;
|
||||
const showControls = details.controls && details.controls.length > 0;
|
||||
const nodeColor = getNodeColorDark(details.rank, details.label_major);
|
||||
const {error, pending} = (this.props.controlStatus || {});
|
||||
const {error, pending} = (this.props.nodeControlStatus || {});
|
||||
const tools = this.renderTools();
|
||||
const styles = {
|
||||
controls: {
|
||||
'backgroundColor': brightenColor(nodeColor)
|
||||
@@ -98,18 +143,19 @@ export default class NodeDetails extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="node-details">
|
||||
{tools}
|
||||
<div className="node-details-header" style={styles.header}>
|
||||
<div className="node-details-header-wrapper">
|
||||
<h2 className="node-details-header-label truncate" title={details.label_major}>
|
||||
{details.label_major}
|
||||
<h2 className="node-details-header-label truncate" title={details.label}>
|
||||
{details.label}
|
||||
</h2>
|
||||
<div className="node-details-header-label-minor truncate" title={details.label_minor}>
|
||||
{details.label_minor}
|
||||
<div className="node-details-header-relatives">
|
||||
{details.parents && <NodeDetailsRelatives relatives={details.parents} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{details.controls && details.controls.length > 0 && <div className="node-details-controls-wrapper" style={styles.controls}>
|
||||
{showControls && <div className="node-details-controls-wrapper" style={styles.controls}>
|
||||
<NodeDetailsControls nodeId={this.props.nodeId}
|
||||
controls={details.controls}
|
||||
pending={pending}
|
||||
@@ -117,9 +163,18 @@ export default class NodeDetails extends React.Component {
|
||||
</div>}
|
||||
|
||||
<div className="node-details-content">
|
||||
{details.tables.map(function(table) {
|
||||
const key = _.snakeCase(table.title);
|
||||
return <NodeDetailsTable title={table.title} key={key} rows={table.rows} isNumeric={table.numeric} />;
|
||||
{showSummary && <div className="node-details-content-section">
|
||||
<div className="node-details-content-section-header">Status</div>
|
||||
{details.metrics && <NodeDetailsHealth metrics={details.metrics} />}
|
||||
{details.metadata && <NodeDetailsInfo metadata={details.metadata} />}
|
||||
</div>}
|
||||
|
||||
{details.children && details.children.map(children => {
|
||||
return (
|
||||
<div className="node-details-content-section" key={children.topologyId}>
|
||||
<NodeDetailsTable {...children} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import Sparkline from '../sparkline';
|
||||
import { formatMetric } from '../../utils/string-utils';
|
||||
|
||||
export default (props) => {
|
||||
return (
|
||||
<div className="node-details-health-item">
|
||||
<div className="node-details-health-item-value">{formatMetric(props.item.value, props.item)}</div>
|
||||
<div className="node-details-health-item-sparkline">
|
||||
<Sparkline data={props.item.samples} min={0} max={props.item.max}
|
||||
first={props.item.first} last={props.item.last} interpolate="none" />
|
||||
</div>
|
||||
<div className="node-details-health-item-label">{props.item.label}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { formatMetric } from '../../utils/string-utils';
|
||||
|
||||
export default class NodeDetailsHealthOverflowItem extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="node-details-health-overflow-item">
|
||||
<div className="node-details-health-overflow-item-value">{formatMetric(this.props.item.value, this.props.item)}</div>
|
||||
<div className="node-details-health-overflow-item-label truncate">{this.props.item.label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="node-details-health-overflow" onClick={this.props.handleClickMore}>
|
||||
{items.map(item => <NodeDetailsHealthOverflowItem key={item.id} item={item} />)}
|
||||
<div className="node-details-health-overflow-expand">
|
||||
Show more
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="node-details-health" style={{flexWrap, justifyContent}}>
|
||||
{primeMetrics.map(item => {
|
||||
return <NodeDetailsHealthItem key={item.id} item={item} />;
|
||||
})}
|
||||
{showOverflow && <NodeDetailsHealthOverflow items={overflowMetrics} handleClickMore={this.handleClickMore} />}
|
||||
{showLess && <div className="node-details-health-expand" onClick={this.handleClickMore}>show less</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
export default class NodeDetailsInfo extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="node-details-info">
|
||||
{this.props.metadata && this.props.metadata.map(field => {
|
||||
return (
|
||||
<div className="node-details-info-field" key={field.id}>
|
||||
<div className="node-details-info-field-label truncate" title={field.label}>
|
||||
{field.label}
|
||||
</div>
|
||||
<div className="node-details-info-field-value" title={field.value}>
|
||||
<div className="truncate">
|
||||
{field.value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<span className="node-details-relatives-link" title={title} onClick={this.handleClick}>
|
||||
{this.props.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="node-details-relatives">
|
||||
{relatives.map(relative => {
|
||||
return <NodeDetailsRelativesLink {...relative} key={relative.id} />;
|
||||
})}
|
||||
{showLimitAction && <span className="node-details-relatives-more" onClick={this.handleLimitClick}>{limitActionText}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<span className="node-details-table-node-link truncate" title={title}
|
||||
onClick={this.handleClick}>
|
||||
{this.props.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="node-details-table-node truncate" title={title}>
|
||||
{this.props.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default class NodeDetailsTableRowNumber extends React.Component {
|
||||
render() {
|
||||
const row = this.props.row;
|
||||
return (
|
||||
<div className="node-details-table-row-value">
|
||||
<div className="node-details-table-row-value-scalar">{row.value_major}</div>
|
||||
<div className="node-details-table-row-value-unit">{row.value_minor}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="node-details-table-row-value">
|
||||
<div className="node-details-table-row-value-sparkline"><Sparkline data={row.metric.samples} min={0} max={row.metric.max} first={row.metric.first} last={row.metric.last} interpolate="none" />{row.value_major}</div>
|
||||
<div className="node-details-table-row-value-unit">{row.value_minor}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default class NodeDetailsTableRowValue extends React.Component {
|
||||
render() {
|
||||
const row = this.props.row;
|
||||
return (
|
||||
<div className="node-details-table-row-value">
|
||||
<div className="node-details-table-row-value-major truncate" title={row.value_major}>
|
||||
{row.value_major}
|
||||
</div>
|
||||
{row.value_minor && <div className="node-details-table-row-value-minor truncate" title={row.value_minor}>
|
||||
{row.value_minor}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
render() {
|
||||
return (
|
||||
<div className="node-details-table">
|
||||
<h4 className="node-details-table-title truncate" title={this.props.title}>
|
||||
{this.props.title}
|
||||
</h4>
|
||||
|
||||
{this.props.rows.map(function(row) {
|
||||
let valueComponent;
|
||||
if (row.value_type === 'numeric') {
|
||||
valueComponent = <NodeDetailsTableRowNumber row={row} />;
|
||||
} else if (row.value_type === 'sparkline') {
|
||||
valueComponent = <NodeDetailsTableRowSparkline row={row} />;
|
||||
} else {
|
||||
valueComponent = <NodeDetailsTableRowValue row={row} />;
|
||||
}
|
||||
return (
|
||||
<div className="node-details-table-row" key={row.key + row.value_major + row.value_minor}>
|
||||
<div className="node-details-table-row-key truncate" title={row.key}>{row.key}</div>
|
||||
{valueComponent}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
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 (
|
||||
<tr>
|
||||
{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 (
|
||||
<td className={headerClasses.join(' ')} onClick={onHeaderClick} key={header.id}>
|
||||
{isSortedAsc && <span className="node-details-table-header-sorter fa fa-caret-up" />}
|
||||
{isSortedDesc && <span className="node-details-table-header-sorter fa fa-caret-down" />}
|
||||
{header.label}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
renderValues(node) {
|
||||
const fields = this.getValuesForNode(node);
|
||||
return this.props.columns.map(col => {
|
||||
const field = fields[col];
|
||||
if (field) {
|
||||
return (
|
||||
<td className="node-details-table-node-value" key={field.id}>
|
||||
{formatMetric(field.value, field)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="node-details-table-wrapper">
|
||||
<table className="node-details-table">
|
||||
<thead>
|
||||
{headers}
|
||||
</thead>
|
||||
<tbody>
|
||||
{nodes && nodes.map(node => {
|
||||
const values = this.renderValues(node);
|
||||
return (
|
||||
<tr className="node-details-table-node" key={node.id}>
|
||||
<td className="node-details-table-node-label truncate">
|
||||
<NodeDetailsTableNodeLink topologyId={this.props.topologyId} {...node} />
|
||||
</td>
|
||||
{values}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{showLimitAction && <div className="node-details-table-more" onClick={this.handleLimitClick}>{limitActionText}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export default class Sparkline extends React.Component {
|
||||
}
|
||||
|
||||
Sparkline.defaultProps = {
|
||||
width: 100,
|
||||
width: 80,
|
||||
height: 16,
|
||||
strokeColor: '#7d7da8',
|
||||
strokeWidth: '0.5px',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
13
client/app/scripts/utils/__tests__/string-utils-test.js
Normal file
13
client/app/scripts/utils/__tests__/string-utils-test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
31
client/app/scripts/utils/string-utils.js
Normal file
31
client/app/scripts/utils/string-utils.js
Normal file
@@ -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 (
|
||||
<span className="metric-formatted">
|
||||
<span className="metric-value">{text}</span>
|
||||
<span className="metric-unit">{unit}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function formatMetric(value, opts) {
|
||||
const formatter = opts && formatters[opts.format] ? opts.format : 'number';
|
||||
return formatters[formatter](value);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
&-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);
|
||||
}
|
||||
}
|
||||
|
||||
.details-tools-wrapper {
|
||||
.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-secondary-color;
|
||||
font-size: 100%;
|
||||
}
|
||||
color: @text-tertiary-color;
|
||||
font-size: 90%;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
&-row {
|
||||
white-space: nowrap;
|
||||
clear: left;
|
||||
|
||||
&-key {
|
||||
width: 11em;
|
||||
float: left;
|
||||
&-sorted {
|
||||
color: @text-secondary-color;
|
||||
}
|
||||
|
||||
&-value-major {
|
||||
margin-right: 0.5em;
|
||||
&-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;
|
||||
opacity: 0.7;
|
||||
font-size: 80%;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-node {
|
||||
font-size: 105%;
|
||||
line-height: 1.5;
|
||||
|
||||
> * {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;<container>/docker_stop_container'"
|
||||
|
||||
sleep 5
|
||||
assert "docker_on $HOST1 inspect --format='{{.State.Running}}' alpine" "false"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
97
render/benchmark_test.go
Normal file
97
render/benchmark_test.go
Normal file
@@ -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
|
||||
}
|
||||
135
render/detailed/metadata.go
Normal file
135
render/detailed/metadata.go
Normal file
@@ -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
|
||||
}
|
||||
53
render/detailed/metadata_test.go
Normal file
53
render/detailed/metadata_test.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
121
render/detailed/metrics.go
Normal file
121
render/detailed/metrics.go
Normal file
@@ -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
|
||||
}
|
||||
121
render/detailed/metrics_test.go
Normal file
121
render/detailed/metrics_test.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
201
render/detailed/node.go
Normal file
201
render/detailed/node.go
Normal file
@@ -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",
|
||||
}
|
||||
}
|
||||
191
render/detailed/node_test.go
Normal file
191
render/detailed/node_test.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
140
render/detailed/summary.go
Normal file
140
render/detailed/summary.go
Normal file
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
render/id.go
36
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...)
|
||||
}
|
||||
|
||||
@@ -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-<id> 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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
24
report/id.go
24
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 + "<container>"
|
||||
}
|
||||
|
||||
// MakeContainerImageNodeID produces a container image node ID from its composite parts.
|
||||
func MakeContainerImageNodeID(containerImageID string) string {
|
||||
return containerImageID + ScopeDelim + "<container_image>"
|
||||
}
|
||||
|
||||
// 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] != "<container>" {
|
||||
return "", false
|
||||
}
|
||||
return fields[0], fields[1], true
|
||||
return fields[0], true
|
||||
}
|
||||
|
||||
// ParseAddressNodeID produces the host ID, address from an address node ID.
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
94
report/node_set.go
Normal file
94
report/node_set.go
Normal file
@@ -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
|
||||
}
|
||||
231
report/node_set_test.go
Normal file
231
report/node_set_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user