Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add connection tables to details panel #1017

Merged
merged 6 commits into from
Mar 4, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app/api_topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,18 @@ func handleWs(ctx context.Context, rep Reporter, renderer render.Renderer, w htt
}

// Individual nodes.
func handleNode(nodeID string) func(context.Context, Reporter, render.Renderer, http.ResponseWriter, *http.Request) {
func handleNode(topologyID, nodeID string) func(context.Context, Reporter, render.Renderer, http.ResponseWriter, *http.Request) {
return func(ctx context.Context, rep Reporter, renderer render.Renderer, w http.ResponseWriter, r *http.Request) {
var (
rpt = rep.Report(ctx)
node, ok = renderer.Render(rep.Report(ctx))[nodeID]
rendered = renderer.Render(rep.Report(ctx))
node, ok = rendered[nodeID]
)
if !ok {
http.NotFound(w, r)
return
}
respondWith(w, http.StatusOK, APINode{Node: detailed.MakeNode(rpt, node)})
respondWith(w, http.StatusOK, APINode{Node: detailed.MakeNode(topologyID, rpt, rendered, node)})
}
}

Expand Down
25 changes: 2 additions & 23 deletions app/api_topology_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,6 @@ func TestAll(t *testing.T) {
}
}

func TestAPITopologyContainers(t *testing.T) {
ts := topologyServer()
{
body := getRawJSON(t, ts, "/api/topology/containers")
var topo app.APITopology
decoder := codec.NewDecoderBytes(body, &codec.JsonHandle{})
if err := decoder.Decode(&topo); err != nil {
t.Fatal(err)
}
want := expected.RenderedContainers.Copy()
for id, node := range want {
node.ControlNode = ""
want[id] = node
}

if have := topo.Nodes.Prune(); !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}
}
}

func TestAPITopologyProcesses(t *testing.T) {
ts := topologyServer()

This comment was marked as abuse.

defer ts.Close()
Expand Down Expand Up @@ -124,13 +103,13 @@ func TestAPITopologyHosts(t *testing.T) {
}
}
{
body := getRawJSON(t, ts, "/api/topology/hosts/"+expected.ServerHostRenderedID)
body := getRawJSON(t, ts, "/api/topology/hosts/"+expected.ServerHostID)
var node app.APINode
decoder := codec.NewDecoderBytes(body, &codec.JsonHandle{})
if err := decoder.Decode(&node); err != nil {
t.Fatal(err)
}
equals(t, expected.ServerHostRenderedID, node.Node.ID)
equals(t, expected.ServerHostID, node.Node.ID)
equals(t, "server", node.Node.Label)
equals(t, false, node.Node.Pseudo)
// Let's not unit-test the specific content of the detail tables
Expand Down
2 changes: 1 addition & 1 deletion app/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func TopologyHandler(c Reporter, preRoutes *mux.Router, postRoutes http.Handler)
}

handler := gzipHandler(requestContextDecorator(topologyRegistry.captureRendererWithoutFilters(
c, topologyID, handleNode(nodeID),
c, topologyID, handleNode(topologyID, nodeID),
)))
handler.ServeHTTP(w, r)
})
Expand Down
8 changes: 8 additions & 0 deletions client/app/scripts/components/node-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ export default class NodeDetails extends React.Component {
<NodeDetailsInfo rows={details.metadata} />
</div>}

{details.connections && details.connections.map(connections => {
return (
<div className="node-details-content-section" key={connections.id}>
<NodeDetailsTable {...connections} />
</div>
);
})}

{details.children && details.children.map(children => {
return (
<div className="node-details-content-section" key={children.topologyId}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import ShowMore from '../show-more';
import NodeDetailsTableNodeLink from './node-details-table-node-link';
import NodeDetailsTableNodeMetric from './node-details-table-node-metric';

function isNumberField(field) {
return field.dataType && field.dataType === 'number';
}

export default class NodeDetailsTable extends React.Component {

constructor(props, context) {
Expand Down Expand Up @@ -32,14 +36,28 @@ export default class NodeDetailsTable extends React.Component {
}

getDefaultSortBy() {
// first metric
// default sorter specified by columns
const defaultSortColumn = _.find(this.props.columns, {defaultSort: true});
if (defaultSortColumn) {
return defaultSortColumn.id;
}
// otherwise choose 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;
return node => {
const nodeMetadataField = node.metadata[index];
if (nodeMetadataField) {
if (isNumberField(nodeMetadataField)) {
return parseFloat(nodeMetadataField.value);
}
return nodeMetadataField.value;
}
return null;
};
});
}

Expand All @@ -49,6 +67,9 @@ export default class NodeDetailsTable extends React.Component {
if (sortBy !== null) {
const field = _.union(node.metrics, node.metadata).find(f => f.id === sortBy);
if (field) {
if (isNumberField(field)) {
return parseFloat(field.value);
}
return field.value;
}
}
Expand Down
6 changes: 2 additions & 4 deletions integration/300_internet_edge_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ do_connections() {
}
do_connections&

wait_for_containers $HOST1 60 nginx "Inbound"
wait_for_containers $HOST1 60 nginx "The Internet"

has_container $HOST1 nginx
has_container $HOST1 "Inbound"
has_connection containers $HOST1 "Inbound" nginx
has_connection_by_id containers $HOST1 "in-theinternet" $(node_id containers $HOST1 nginx)

kill %do_connections

Expand Down
21 changes: 16 additions & 5 deletions integration/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,13 @@ container_id() {
}

# this checks we have an edge from container 1 to container 2
has_connection() {
has_connection_by_id() {
local view="$1"
local host="$2"
local from="$3"
local to="$4"
local from_id="$3"
local to_id="$4"
local timeout="${5:-60}"
local from_id=$(node_id "${view}" "${host}" "${from}")
local to_id=$(node_id "${view}" "${host}" "${to}")

for i in $(seq $timeout); do
local nodes="$(curl -s http://$host:4040/api/topology/${view}?system=show)"
local edge=$(echo "$nodes" | jq -r ".nodes[\"$from_id\"].adjacency | contains([\"$to_id\"])" 2>/dev/null)
Expand All @@ -82,6 +81,18 @@ has_connection() {
assert "curl -s http://$host:4040/api/topology/${view}?system=show | jq -r '.nodes[\"$from_id\"].adjacency | contains([\"$to_id\"])'" true
}

has_connection() {
local view="$1"
local host="$2"
local from="$3"
local to="$4"
local timeout="${5:-60}"
local from_id="$(node_id "${view}" "${host}" "${from}")"
local to_id="$(node_id "${view}" "${host}" "${to}")"

has_connection_by_id "${view}" "${host}" "${from_id}" "${to_id}" "${timeout}"
}

wait_for() {
local view="$1"
local host="$2"
Expand Down
197 changes: 197 additions & 0 deletions render/detailed/connections.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package detailed

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.


import (
"sort"
"strconv"

"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/report"
)

const (
portKey = "port"
portLabel = "Port"
countKey = "count"
countLabel = "Count"
number = "number"
)

// Exported for testing
var (
NormalColumns = []Column{
{ID: portKey, Label: portLabel},
{ID: countKey, Label: countLabel, DefaultSort: true},
}
InternetColumns = []Column{
{ID: "foo", Label: "Remote"},
{ID: portKey, Label: portLabel},
{ID: countKey, Label: countLabel, DefaultSort: true},
}
)

type connectionsRow struct {
remoteNode, localNode *render.RenderableNode
remoteAddr, localAddr string
port string // always the server-side port
}

func incomingConnectionsTable(topologyID string, n render.RenderableNode, ns render.RenderableNodes) NodeSummaryGroup {
localEndpointIDs := endpointChildIDsOf(n)

// For each node which has an edge TO me
counts := map[connectionsRow]int{}
for _, node := range ns {
if !node.Adjacency.Contains(n.ID) {
continue
}
remoteNode := node.Copy()

// Work out what port they are talking to, and count the number of
// connections to that port.
// This is complicated as for internet nodes we break out individual
// address, both when the internet node is remote (an incoming
// connection from the internet) and 'local' (ie you are loading
// details on the internet node)
for _, child := range endpointChildrenOf(node) {
for _, localEndpointID := range child.Adjacency.Intersection(localEndpointIDs) {
_, localAddr, port, ok := render.ParseEndpointID(localEndpointID)
if !ok {
continue
}
key := connectionsRow{
localNode: &n,
remoteNode: &remoteNode,
port: port,
}
if isInternetNode(n) {
key.localAddr = localAddr
}
counts[key] = counts[key] + 1
}
}
}

columnHeaders := NormalColumns
if isInternetNode(n) {
columnHeaders = InternetColumns
}
return NodeSummaryGroup{
ID: "incoming-connections",
TopologyID: topologyID,
Label: "Inbound",
Columns: columnHeaders,
Nodes: connectionRows(counts, isInternetNode(n)),
}
}

func outgoingConnectionsTable(topologyID string, n render.RenderableNode, ns render.RenderableNodes) NodeSummaryGroup {
localEndpoints := endpointChildrenOf(n)

// For each node which has an edge FROM me
counts := map[connectionsRow]int{}
for _, node := range ns {
if !n.Adjacency.Contains(node.ID) {
continue
}
remoteNode := node.Copy()
remoteEndpointIDs := endpointChildIDsOf(remoteNode)

for _, localEndpoint := range localEndpoints {
_, localAddr, _, ok := render.ParseEndpointID(localEndpoint.ID)
if !ok {
continue
}

for _, remoteEndpointID := range localEndpoint.Adjacency.Intersection(remoteEndpointIDs) {
_, _, port, ok := render.ParseEndpointID(remoteEndpointID)
if !ok {
continue
}
key := connectionsRow{
localNode: &n,
remoteNode: &remoteNode,
port: port,
}
if isInternetNode(n) {
key.localAddr = localAddr
}
counts[key] = counts[key] + 1
}
}
}

columnHeaders := NormalColumns
if isInternetNode(n) {
columnHeaders = InternetColumns
}
return NodeSummaryGroup{
ID: "outgoing-connections",
TopologyID: topologyID,
Label: "Outbound",
Columns: columnHeaders,
Nodes: connectionRows(counts, isInternetNode(n)),
}
}

func endpointChildrenOf(n render.RenderableNode) []render.RenderableNode {
result := []render.RenderableNode{}
n.Children.ForEach(func(child render.RenderableNode) {
if _, _, _, ok := render.ParseEndpointID(child.ID); ok {
result = append(result, child)
}
})
return result
}

func endpointChildIDsOf(n render.RenderableNode) report.IDList {
result := report.MakeIDList()
n.Children.ForEach(func(child render.RenderableNode) {
if _, _, _, ok := render.ParseEndpointID(child.ID); ok {
result = append(result, child.ID)
}
})
return result
}

func isInternetNode(n render.RenderableNode) bool {
return n.ID == render.IncomingInternetID || n.ID == render.OutgoingInternetID
}

func connectionRows(in map[connectionsRow]int, includeLocal bool) []NodeSummary {
nodes := []NodeSummary{}
for row, count := range in {
id, label, linkable := row.remoteNode.ID, row.remoteNode.LabelMajor, true
if row.remoteAddr != "" {
id, label, linkable = row.remoteAddr+":"+row.port, row.remoteAddr, false
}
metadata := []MetadataRow{}
if includeLocal {
metadata = append(metadata,
MetadataRow{
ID: "foo",
Value: row.localAddr,
Datatype: number,
})

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

}
metadata = append(metadata,
MetadataRow{
ID: portKey,
Value: row.port,
Datatype: number,
},
MetadataRow{
ID: countKey,
Value: strconv.Itoa(count),
Datatype: number,
},
)
nodes = append(nodes, NodeSummary{
ID: id,
Label: label,
Linkable: linkable,
Metadata: metadata,
})
}
sort.Sort(nodeSummariesByID(nodes))
return nodes
}
Loading