Merge pull request #242 from tomwilkie/228-more-moves

Move RenderableNode and DetailedNode into render/
This commit is contained in:
Tom Wilkie
2015-06-16 18:36:31 +02:00
14 changed files with 256 additions and 206 deletions

View File

@@ -3,7 +3,7 @@ package main
import (
"net/http"
"github.com/weaveworks/scope/report"
"github.com/weaveworks/scope/render"
)
// APITopologyDesc is returned in a list by the /api/topology handler.
@@ -52,7 +52,7 @@ func makeTopologyList(rep Reporter) func(w http.ResponseWriter, r *http.Request)
}
}
func stats(r report.RenderableNodes) *topologyStats {
func stats(r render.RenderableNodes) *topologyStats {
var (
nodes int
realNodes int

View File

@@ -18,12 +18,12 @@ const (
// APITopology is returned by the /api/topology/{name} handler.
type APITopology struct {
Nodes report.RenderableNodes `json:"nodes"`
Nodes render.RenderableNodes `json:"nodes"`
}
// APINode is returned by the /api/topology/{name}/{id} handler.
type APINode struct {
Node report.DetailedNode `json:"node"`
Node render.DetailedNode `json:"node"`
}
// APIEdge is returned by the /api/topology/*/*/* handlers.
@@ -67,7 +67,7 @@ func handleNode(rep Reporter, t topologyView, w http.ResponseWriter, r *http.Req
http.NotFound(w, r)
return
}
respondWith(w, http.StatusOK, APINode{Node: report.MakeDetailedNode(rpt, node)})
respondWith(w, http.StatusOK, APINode{Node: render.MakeDetailedNode(rpt, node)})
}
// Individual edges.
@@ -112,7 +112,7 @@ func handleWebsocket(
}(conn)
var (
previousTopo report.RenderableNodes
previousTopo render.RenderableNodes
tick = time.Tick(loop)
)
for {

View File

@@ -56,7 +56,7 @@ func handleHTML(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "</body></html>\n")
}
func dot(w io.Writer, m map[string]report.RenderableNode) {
func dot(w io.Writer, m map[string]render.RenderableNode) {
fmt.Fprintf(w, "digraph G {\n")
fmt.Fprintf(w, "\tgraph [ overlap=false ];\n")
fmt.Fprintf(w, "\tnode [ shape=circle, style=filled ];\n")

View File

@@ -1,23 +1,50 @@
package report
package render
import (
"reflect"
"strconv"
"github.com/weaveworks/scope/report"
)
// 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"`
Pseudo bool `json:"pseudo,omitempty"`
Tables []Table `json:"tables"`
}
// 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?
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
}
// 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, n RenderableNode) DetailedNode {
func MakeDetailedNode(r report.Report, n RenderableNode) DetailedNode {
tables := []Table{}
{
rows := []Row{}
if val, ok := n.Metadata[KeyMaxConnCountTCP]; ok {
if val, ok := n.Metadata[report.KeyMaxConnCountTCP]; ok {
rows = append(rows, Row{"TCP connections", strconv.FormatInt(int64(val), 10), ""})
}
if val, ok := n.Metadata[KeyBytesIngress]; ok {
if val, ok := n.Metadata[report.KeyBytesIngress]; ok {
rows = append(rows, Row{"Bytes ingress", strconv.FormatInt(int64(val), 10), ""})
}
if val, ok := n.Metadata[KeyBytesEgress]; ok {
if val, ok := n.Metadata[report.KeyBytesEgress]; ok {
rows = append(rows, Row{"Bytes egress", strconv.FormatInt(int64(val), 10), ""})
}
if len(rows) > 0 {
@@ -53,7 +80,7 @@ outer:
// 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, originID string) (Table, bool) {
func OriginTable(r report.Report, originID string) (Table, bool) {
if nmd, ok := r.Endpoint.NodeMetadatas[originID]; ok {
return endpointOriginTable(nmd)
}
@@ -72,7 +99,7 @@ func OriginTable(r Report, originID string) (Table, bool) {
return Table{}, false
}
func endpointOriginTable(nmd NodeMetadata) (Table, bool) {
func endpointOriginTable(nmd report.NodeMetadata) (Table, bool) {
rows := []Row{}
for _, tuple := range []struct{ key, human string }{
{"endpoint", "Endpoint"},
@@ -91,7 +118,7 @@ func endpointOriginTable(nmd NodeMetadata) (Table, bool) {
}, len(rows) > 0
}
func addressOriginTable(nmd NodeMetadata) (Table, bool) {
func addressOriginTable(nmd report.NodeMetadata) (Table, bool) {
rows := []Row{}
if val, ok := nmd["address"]; ok {
rows = append(rows, Row{"Address", val, ""})
@@ -106,7 +133,7 @@ func addressOriginTable(nmd NodeMetadata) (Table, bool) {
}, len(rows) > 0
}
func processOriginTable(nmd NodeMetadata) (Table, bool) {
func processOriginTable(nmd report.NodeMetadata) (Table, bool) {
rows := []Row{}
if val, ok := nmd["comm"]; ok {
rows = append(rows, Row{"Name (comm)", val, ""})
@@ -124,7 +151,7 @@ func processOriginTable(nmd NodeMetadata) (Table, bool) {
}, len(rows) > 0
}
func containerOriginTable(nmd NodeMetadata) (Table, bool) {
func containerOriginTable(nmd report.NodeMetadata) (Table, bool) {
rows := []Row{}
for _, tuple := range []struct{ key, human string }{
{"docker_container_id", "Container ID"},
@@ -143,7 +170,7 @@ func containerOriginTable(nmd NodeMetadata) (Table, bool) {
}, len(rows) > 0
}
func hostOriginTable(nmd NodeMetadata) (Table, bool) {
func hostOriginTable(nmd report.NodeMetadata) (Table, bool) {
rows := []Row{}
if val, ok := nmd["host_name"]; ok {
rows = append(rows, Row{"Host name", val, ""})

View File

@@ -1,9 +1,10 @@
package report_test
package render_test
import (
"reflect"
"testing"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/report"
)
@@ -12,26 +13,30 @@ func TestMakeDetailedNode(t *testing.T) {
}
func TestOriginTable(t *testing.T) {
if _, ok := report.OriginTable(reportFixture, "not-found"); ok {
if _, ok := render.OriginTable(rpt, "not-found"); ok {
t.Errorf("unknown origin ID gave unexpected success")
}
for originID, want := range map[string]report.Table{
client54001EndpointNodeID: {
for originID, want := range map[string]render.Table{
client54001NodeID: {
Title: "Origin Endpoint",
Numeric: false,
Rows: []report.Row{{"Host name", clientHostName, ""}},
Rows: []render.Row{
{"Host name", clientHostName, ""},
{"PID", "10001", ""},
{"Process name", "curl", ""},
},
},
clientAddressNodeID: {
Title: "Origin Address",
Numeric: false,
Rows: []report.Row{
Rows: []render.Row{
{"Host name", clientHostName, ""},
},
},
report.MakeProcessNodeID(clientHostID, "4242"): {
Title: "Origin Process",
Numeric: false,
Rows: []report.Row{
Rows: []render.Row{
{"Name (comm)", "curl", ""},
{"PID", "4242", ""},
},
@@ -39,14 +44,14 @@ func TestOriginTable(t *testing.T) {
serverHostNodeID: {
Title: "Origin Host",
Numeric: false,
Rows: []report.Row{
Rows: []render.Row{
{"Host name", serverHostName, ""},
{"Load", "0.01 0.01 0.01", ""},
{"Operating system", "Linux", ""},
},
},
} {
have, ok := report.OriginTable(reportFixture, originID)
have, ok := render.OriginTable(rpt, originID)
if !ok {
t.Errorf("%q: not OK", originID)
continue

View File

@@ -9,8 +9,8 @@ import (
const humanTheInternet = "the Internet"
func newRenderableNode(id, major, minor, rank string) report.RenderableNode {
return report.RenderableNode{
func newRenderableNode(id, major, minor, rank string) RenderableNode {
return RenderableNode{
ID: id,
LabelMajor: major,
LabelMinor: minor,
@@ -20,8 +20,8 @@ func newRenderableNode(id, major, minor, rank string) report.RenderableNode {
}
}
func newPseudoNode(id, major, minor string) report.RenderableNode {
return report.RenderableNode{
func newPseudoNode(id, major, minor string) RenderableNode {
return RenderableNode{
ID: id,
LabelMajor: major,
LabelMinor: minor,
@@ -41,18 +41,18 @@ func newPseudoNode(id, major, minor string) report.RenderableNode {
//
// If the final output parameter is false, the node shall be omitted from the
// rendered topology.
type MapFunc func(report.NodeMetadata) (report.RenderableNode, bool)
type MapFunc func(report.NodeMetadata) (RenderableNode, bool)
// PseudoFunc creates RenderableNode representing pseudo nodes given the dstNodeID.
// The srcNode renderable node is essentially from MapFunc, representing one of
// the rendered nodes this pseudo node refers to. srcNodeID and dstNodeID are
// node IDs prior to mapping.
type PseudoFunc func(srcNodeID string, srcNode report.RenderableNode, dstNodeID string) (report.RenderableNode, bool)
type PseudoFunc func(srcNodeID string, srcNode RenderableNode, dstNodeID string) (RenderableNode, bool)
// ProcessPID takes a node NodeMetadata from topology, and returns a
// representation with the ID based on the process PID and the labels based on
// the process name.
func ProcessPID(m report.NodeMetadata) (report.RenderableNode, bool) {
func ProcessPID(m report.NodeMetadata) (RenderableNode, bool) {
var (
identifier = fmt.Sprintf("%s:%s:%s", "pid", m["domain"], m["pid"])
minor = fmt.Sprintf("%s (%s)", m["domain"], m["pid"])
@@ -65,7 +65,7 @@ func ProcessPID(m report.NodeMetadata) (report.RenderableNode, bool) {
// ProcessName takes a node NodeMetadata from a topology, and returns a
// representation with the ID based on the process name (grouping all
// processes with the same name together).
func ProcessName(m report.NodeMetadata) (report.RenderableNode, bool) {
func ProcessName(m report.NodeMetadata) (RenderableNode, bool) {
show := m["pid"] != "" && m["name"] != ""
return newRenderableNode(m["name"], m["name"], "", m["name"]), show
}
@@ -74,7 +74,7 @@ func ProcessName(m report.NodeMetadata) (report.RenderableNode, bool) {
// in. We consider container and image IDs to be globally unique, and so don't
// scope them further by e.g. host. If no container metadata is found, nodes are
// grouped into the Uncontained node.
func MapEndpoint2Container(m report.NodeMetadata) (report.RenderableNode, bool) {
func MapEndpoint2Container(m report.NodeMetadata) (RenderableNode, bool) {
var id, major, minor, rank string
if m["docker_container_id"] == "" {
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
@@ -86,7 +86,7 @@ func MapEndpoint2Container(m report.NodeMetadata) (report.RenderableNode, bool)
}
// MapContainerIdentity maps container topology node to container mapped nodes.
func MapContainerIdentity(m report.NodeMetadata) (report.RenderableNode, bool) {
func MapContainerIdentity(m report.NodeMetadata) (RenderableNode, bool) {
var id, major, minor, rank string
if m["docker_container_id"] == "" {
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
@@ -100,7 +100,7 @@ func MapContainerIdentity(m report.NodeMetadata) (report.RenderableNode, bool) {
// ProcessContainerImage maps topology nodes to the container images they run
// on. If no container metadata is found, nodes are grouped into the
// Uncontained node.
func ProcessContainerImage(m report.NodeMetadata) (report.RenderableNode, bool) {
func ProcessContainerImage(m report.NodeMetadata) (RenderableNode, bool) {
var id, major, minor, rank string
if m["docker_image_id"] == "" {
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
@@ -114,7 +114,7 @@ func ProcessContainerImage(m report.NodeMetadata) (report.RenderableNode, bool)
// NetworkHostname takes a node NodeMetadata and returns a representation
// based on the hostname. Major label is the hostname, the minor label is the
// domain, if any.
func NetworkHostname(m report.NodeMetadata) (report.RenderableNode, bool) {
func NetworkHostname(m report.NodeMetadata) (RenderableNode, bool) {
var (
name = m["name"]
domain = ""
@@ -130,7 +130,7 @@ func NetworkHostname(m report.NodeMetadata) (report.RenderableNode, bool) {
// GenericPseudoNode contains heuristics for building sensible pseudo nodes.
// It should go away.
func GenericPseudoNode(src string, srcMapped report.RenderableNode, dst string) (report.RenderableNode, bool) {
func GenericPseudoNode(src string, srcMapped RenderableNode, dst string) (RenderableNode, bool) {
var maj, min, outputID string
if dst == report.TheInternet {
@@ -151,7 +151,7 @@ func GenericPseudoNode(src string, srcMapped report.RenderableNode, dst string)
// GenericGroupedPseudoNode contains heuristics for building sensible pseudo nodes.
// It should go away.
func GenericGroupedPseudoNode(src string, srcMapped report.RenderableNode, dst string) (report.RenderableNode, bool) {
func GenericGroupedPseudoNode(src string, srcMapped RenderableNode, dst string) (RenderableNode, bool) {
var maj, min, outputID string
if dst == report.TheInternet {
@@ -169,11 +169,11 @@ func GenericGroupedPseudoNode(src string, srcMapped report.RenderableNode, dst s
}
// InternetOnlyPseudoNode never creates a pseudo node, unless it's the Internet.
func InternetOnlyPseudoNode(_ string, _ report.RenderableNode, dst string) (report.RenderableNode, bool) {
func InternetOnlyPseudoNode(_ string, _ RenderableNode, dst string) (RenderableNode, bool) {
if dst == report.TheInternet {
return newPseudoNode(report.TheInternet, humanTheInternet, ""), true
}
return report.RenderableNode{}, false
return RenderableNode{}, false
}
// trySplitAddr is basically ParseArbitraryNodeID, since its callsites

View File

@@ -1,22 +1,23 @@
package render
package render_test
import (
"fmt"
"testing"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/report"
)
func TestUngroupedMapping(t *testing.T) {
for i, c := range []struct {
f MapFunc
f render.MapFunc
id string
meta report.NodeMetadata
wantOK bool
wantID, wantMajor, wantMinor, wantRank string
}{
{
f: NetworkHostname,
f: render.NetworkHostname,
id: report.MakeAddressNodeID("", "1.2.3.4"),
meta: report.NodeMetadata{
"name": "my.host",
@@ -28,7 +29,7 @@ func TestUngroupedMapping(t *testing.T) {
wantRank: "my",
},
{
f: NetworkHostname,
f: render.NetworkHostname,
id: report.MakeAddressNodeID("", "1.2.3.4"),
meta: report.NodeMetadata{
"name": "localhost",
@@ -40,7 +41,7 @@ func TestUngroupedMapping(t *testing.T) {
wantRank: "localhost",
},
{
f: ProcessPID,
f: render.ProcessPID,
id: "not-used-beta",
meta: report.NodeMetadata{
"pid": "42",
@@ -54,7 +55,7 @@ func TestUngroupedMapping(t *testing.T) {
wantRank: "42",
},
{
f: MapEndpoint2Container,
f: render.MapEndpoint2Container,
id: "foo-id",
meta: report.NodeMetadata{
"pid": "42",
@@ -68,7 +69,7 @@ func TestUngroupedMapping(t *testing.T) {
wantRank: "uncontained",
},
{
f: MapEndpoint2Container,
f: render.MapEndpoint2Container,
id: "bar-id",
meta: report.NodeMetadata{
"pid": "42",

View File

@@ -8,7 +8,7 @@ import (
// Renderer is something that can render a report to a set of RenderableNodes
type Renderer interface {
Render(report.Report) report.RenderableNodes
Render(report.Report) RenderableNodes
AggregateMetadata(rpt report.Report, localID, remoteID string) report.AggregateMetadata
}
@@ -17,8 +17,8 @@ type Renderer interface {
type Reduce []Renderer
// Render produces a set of RenderableNodes given a Report
func (r Reduce) Render(rpt report.Report) report.RenderableNodes {
result := report.RenderableNodes{}
func (r Reduce) Render(rpt report.Report) RenderableNodes {
result := RenderableNodes{}
for _, renderer := range r {
result.Merge(renderer.Render(rpt))
}
@@ -43,18 +43,18 @@ type Map struct {
}
// Render produces a set of RenderableNodes given a Report
func (m Map) Render(rpt report.Report) report.RenderableNodes {
return renderTopology(m.Selector(rpt), m.Mapper, m.Pseudo)
func (m Map) Render(rpt report.Report) RenderableNodes {
return Topology(m.Selector(rpt), m.Mapper, m.Pseudo)
}
// RenderBy transforms a given Topology into a set of RenderableNodes, which
// Topology transforms a given Topology into a set of RenderableNodes, which
// the UI will render collectively as a graph. Note that a RenderableNode will
// always be rendered with other nodes, and therefore contains limited detail.
//
// RenderBy takes a a MapFunc, which defines how to group and label nodes. Npdes
// with the same mapped IDs will be merged.
func renderTopology(t report.Topology, mapFunc MapFunc, pseudoFunc PseudoFunc) report.RenderableNodes {
nodes := report.RenderableNodes{}
func Topology(t report.Topology, mapFunc MapFunc, pseudoFunc PseudoFunc) RenderableNodes {
nodes := RenderableNodes{}
// Build a set of RenderableNodes for all non-pseudo probes, and an
// addressID to nodeID lookup map. Multiple addressIDs can map to the same

View File

@@ -1,9 +1,10 @@
package render
package render_test
import (
"reflect"
"testing"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/report"
"github.com/davecgh/go-spew/spew"
@@ -15,11 +16,11 @@ func init() {
}
type mockRenderer struct {
report.RenderableNodes
render.RenderableNodes
aggregateMetadata report.AggregateMetadata
}
func (m mockRenderer) Render(rpt report.Report) report.RenderableNodes {
func (m mockRenderer) Render(rpt report.Report) render.RenderableNodes {
return m.RenderableNodes
}
func (m mockRenderer) AggregateMetadata(rpt report.Report, localID, remoteID string) report.AggregateMetadata {
@@ -27,12 +28,12 @@ func (m mockRenderer) AggregateMetadata(rpt report.Report, localID, remoteID str
}
func TestReduceRender(t *testing.T) {
renderer := Reduce([]Renderer{
mockRenderer{RenderableNodes: report.RenderableNodes{"foo": {ID: "foo"}}},
mockRenderer{RenderableNodes: report.RenderableNodes{"bar": {ID: "bar"}}},
renderer := render.Reduce([]render.Renderer{
mockRenderer{RenderableNodes: render.RenderableNodes{"foo": {ID: "foo"}}},
mockRenderer{RenderableNodes: render.RenderableNodes{"bar": {ID: "bar"}}},
})
want := report.RenderableNodes{"foo": {ID: "foo"}, "bar": {ID: "bar"}}
want := render.RenderableNodes{"foo": {ID: "foo"}, "bar": {ID: "bar"}}
have := renderer.Render(report.MakeReport())
if !reflect.DeepEqual(want, have) {
@@ -41,7 +42,7 @@ func TestReduceRender(t *testing.T) {
}
func TestReduceEdge(t *testing.T) {
renderer := Reduce([]Renderer{
renderer := render.Reduce([]render.Renderer{
mockRenderer{aggregateMetadata: report.AggregateMetadata{"foo": 1}},
mockRenderer{aggregateMetadata: report.AggregateMetadata{"bar": 2}},
})
@@ -60,42 +61,46 @@ var (
randomHostID = "random.hostname.com"
unknownHostID = ""
clientHostName = clientHostID
serverHostName = serverHostID
clientHostNodeID = report.MakeHostNodeID(clientHostID)
serverHostNodeID = report.MakeHostNodeID(serverHostID)
randomHostNodeID = report.MakeHostNodeID(randomHostID)
client54001 = report.MakeEndpointNodeID(clientHostID, "10.10.10.20", "54001") // curl (1)
client54002 = report.MakeEndpointNodeID(clientHostID, "10.10.10.20", "54002") // curl (2)
unknownClient1 = report.MakeEndpointNodeID(serverHostID, "10.10.10.10", "54010") // we want to ensure two unknown clients, connnected
unknownClient2 = report.MakeEndpointNodeID(serverHostID, "10.10.10.10", "54020") // to the same server, are deduped.
unknownClient3 = report.MakeEndpointNodeID(serverHostID, "10.10.10.11", "54020") // Check this one isn't deduped
server80 = report.MakeEndpointNodeID(serverHostID, "192.168.1.1", "80") // apache
client54001NodeID = report.MakeEndpointNodeID(clientHostID, "10.10.10.20", "54001") // curl (1)
client54002NodeID = report.MakeEndpointNodeID(clientHostID, "10.10.10.20", "54002") // curl (2)
unknownClient1 = report.MakeEndpointNodeID(serverHostID, "10.10.10.10", "54010") // we want to ensure two unknown clients, connnected
unknownClient2 = report.MakeEndpointNodeID(serverHostID, "10.10.10.10", "54020") // to the same server, are deduped.
unknownClient3 = report.MakeEndpointNodeID(serverHostID, "10.10.10.11", "54020") // Check this one isn't deduped
server80 = report.MakeEndpointNodeID(serverHostID, "192.168.1.1", "80") // apache
clientIP = report.MakeAddressNodeID(clientHostID, "10.10.10.20")
serverIP = report.MakeAddressNodeID(serverHostID, "192.168.1.1")
randomIP = report.MakeAddressNodeID(randomHostID, "172.16.11.9") // only in Address topology
unknownIP = report.MakeAddressNodeID(unknownHostID, "10.10.10.10")
clientAddressNodeID = report.MakeAddressNodeID(clientHostID, "10.10.10.20")
serverAddressNodeID = report.MakeAddressNodeID(serverHostID, "192.168.1.1")
randomAddressNodeID = report.MakeAddressNodeID(randomHostID, "172.16.11.9") // only in Address topology
unknownAddressNodeID = report.MakeAddressNodeID(unknownHostID, "10.10.10.10")
)
var (
rpt = report.Report{
Endpoint: report.Topology{
Adjacency: report.Adjacency{
report.MakeAdjacencyID(client54001): report.MakeIDList(server80),
report.MakeAdjacencyID(client54002): report.MakeIDList(server80),
report.MakeAdjacencyID(server80): report.MakeIDList(client54001, client54002, unknownClient1, unknownClient2, unknownClient3),
report.MakeAdjacencyID(client54001NodeID): report.MakeIDList(server80),
report.MakeAdjacencyID(client54002NodeID): report.MakeIDList(server80),
report.MakeAdjacencyID(server80): report.MakeIDList(client54001NodeID, client54002NodeID, unknownClient1, unknownClient2, unknownClient3),
},
NodeMetadatas: report.NodeMetadatas{
// NodeMetadata is arbitrary. We're free to put only precisely what we
// care to test into the fixture. Just be sure to include the bits
// that the mapping funcs extract :)
client54001: report.NodeMetadata{
client54001NodeID: report.NodeMetadata{
"name": "curl",
"domain": "client-54001-domain",
"pid": "10001",
report.HostNodeID: clientHostNodeID,
"host_name": clientHostName,
},
client54002: report.NodeMetadata{
client54002NodeID: report.NodeMetadata{
"name": "curl", // should be same as above!
"domain": "client-54002-domain", // may be different than above
"pid": "10001", // should be same as above!
@@ -109,23 +114,23 @@ var (
},
},
EdgeMetadatas: report.EdgeMetadatas{
report.MakeEdgeID(client54001, server80): report.EdgeMetadata{
report.MakeEdgeID(client54001NodeID, server80): report.EdgeMetadata{
WithBytes: true,
BytesIngress: 100,
BytesEgress: 10,
},
report.MakeEdgeID(client54002, server80): report.EdgeMetadata{
report.MakeEdgeID(client54002NodeID, server80): report.EdgeMetadata{
WithBytes: true,
BytesIngress: 200,
BytesEgress: 20,
},
report.MakeEdgeID(server80, client54001): report.EdgeMetadata{
report.MakeEdgeID(server80, client54001NodeID): report.EdgeMetadata{
WithBytes: true,
BytesIngress: 10,
BytesEgress: 100,
},
report.MakeEdgeID(server80, client54002): report.EdgeMetadata{
report.MakeEdgeID(server80, client54002NodeID): report.EdgeMetadata{
WithBytes: true,
BytesIngress: 20,
BytesEgress: 200,
@@ -147,50 +152,84 @@ var (
},
},
},
Process: report.Topology{
Adjacency: report.Adjacency{},
NodeMetadatas: report.NodeMetadatas{
report.MakeProcessNodeID(clientHostID, "4242"): report.NodeMetadata{
"host_name": "client.host.com",
"pid": "4242",
"comm": "curl",
"docker_container_id": "a1b2c3d4e5",
"docker_container_name": "fixture-container",
"docker_image_id": "0000000000",
"docker_image_name": "fixture/container:latest",
},
report.MakeProcessNodeID(serverHostID, "215"): report.NodeMetadata{
"pid": "215",
"process_name": "apache",
},
"no-container": report.NodeMetadata{},
},
EdgeMetadatas: report.EdgeMetadatas{},
},
Address: report.Topology{
Adjacency: report.Adjacency{
report.MakeAdjacencyID(clientIP): report.MakeIDList(serverIP),
report.MakeAdjacencyID(randomIP): report.MakeIDList(serverIP),
report.MakeAdjacencyID(serverIP): report.MakeIDList(clientIP, unknownIP), // no backlink to random
report.MakeAdjacencyID(clientAddressNodeID): report.MakeIDList(serverAddressNodeID),
report.MakeAdjacencyID(randomAddressNodeID): report.MakeIDList(serverAddressNodeID),
report.MakeAdjacencyID(serverAddressNodeID): report.MakeIDList(clientAddressNodeID, unknownAddressNodeID), // no backlink to random
},
NodeMetadatas: report.NodeMetadatas{
clientIP: report.NodeMetadata{
clientAddressNodeID: report.NodeMetadata{
"name": "client.hostname.com", // hostname
"host_name": "client.hostname.com",
report.HostNodeID: clientHostNodeID,
},
randomIP: report.NodeMetadata{
randomAddressNodeID: report.NodeMetadata{
"name": "random.hostname.com", // hostname
report.HostNodeID: randomHostNodeID,
},
serverIP: report.NodeMetadata{
serverAddressNodeID: report.NodeMetadata{
"name": "server.hostname.com", // hostname
report.HostNodeID: serverHostNodeID,
},
},
EdgeMetadatas: report.EdgeMetadatas{
report.MakeEdgeID(clientIP, serverIP): report.EdgeMetadata{
report.MakeEdgeID(clientAddressNodeID, serverAddressNodeID): report.EdgeMetadata{
WithConnCountTCP: true,
MaxConnCountTCP: 3,
},
report.MakeEdgeID(randomIP, serverIP): report.EdgeMetadata{
report.MakeEdgeID(randomAddressNodeID, serverAddressNodeID): report.EdgeMetadata{
WithConnCountTCP: true,
MaxConnCountTCP: 20, // dangling connections, weird but possible
},
report.MakeEdgeID(serverIP, clientIP): report.EdgeMetadata{
report.MakeEdgeID(serverAddressNodeID, clientAddressNodeID): report.EdgeMetadata{
WithConnCountTCP: true,
MaxConnCountTCP: 3,
},
report.MakeEdgeID(serverIP, unknownIP): report.EdgeMetadata{
report.MakeEdgeID(serverAddressNodeID, unknownAddressNodeID): report.EdgeMetadata{
WithConnCountTCP: true,
MaxConnCountTCP: 7,
},
},
},
Host: report.Topology{
Adjacency: report.Adjacency{},
NodeMetadatas: report.NodeMetadatas{
serverHostNodeID: report.NodeMetadata{
"host_name": serverHostName,
"local_networks": "10.10.10.0/24",
"os": "Linux",
"load": "0.01 0.01 0.01",
},
},
EdgeMetadatas: report.EdgeMetadatas{},
},
}
)
func TestRenderByEndpointPID(t *testing.T) {
want := report.RenderableNodes{
want := render.RenderableNodes{
"pid:client-54001-domain:10001": {
ID: "pid:client-54001-domain:10001",
LabelMajor: "curl",
@@ -248,7 +287,7 @@ func TestRenderByEndpointPID(t *testing.T) {
Metadata: report.AggregateMetadata{},
},
}
have := renderTopology(rpt.Endpoint, ProcessPID, GenericPseudoNode)
have := render.Topology(rpt.Endpoint, render.ProcessPID, render.GenericPseudoNode)
if !reflect.DeepEqual(want, have) {
t.Error("\n" + diff(want, have))
}
@@ -258,7 +297,7 @@ func TestRenderByEndpointPIDGrouped(t *testing.T) {
// For grouped, I've somewhat arbitrarily chosen to squash together all
// processes with the same name by removing the PID and domain (host)
// dimensions from the ID. That could be changed.
want := report.RenderableNodes{
want := render.RenderableNodes{
"curl": {
ID: "curl",
LabelMajor: "curl",
@@ -302,14 +341,14 @@ func TestRenderByEndpointPIDGrouped(t *testing.T) {
Metadata: report.AggregateMetadata{},
},
}
have := renderTopology(rpt.Endpoint, ProcessName, GenericGroupedPseudoNode)
have := render.Topology(rpt.Endpoint, render.ProcessName, render.GenericGroupedPseudoNode)
if !reflect.DeepEqual(want, have) {
t.Error("\n" + diff(want, have))
}
}
func TestRenderByNetworkHostname(t *testing.T) {
want := report.RenderableNodes{
want := render.RenderableNodes{
"host:client.hostname.com": {
ID: "host:client.hostname.com",
LabelMajor: "client", // before first .
@@ -357,7 +396,7 @@ func TestRenderByNetworkHostname(t *testing.T) {
Metadata: report.AggregateMetadata{},
},
}
have := renderTopology(rpt.Address, NetworkHostname, GenericPseudoNode)
have := render.Topology(rpt.Address, render.NetworkHostname, render.GenericPseudoNode)
if !reflect.DeepEqual(want, have) {
t.Error("\n" + diff(want, have))
}

58
render/renderable_node.go Normal file
View File

@@ -0,0 +1,58 @@
package render
import (
"github.com/weaveworks/scope/report"
)
// RenderableNode is the data type that's yielded to the JavaScript layer as
// 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
Adjacency report.IDList `json:"adjacency,omitempty"` // Node IDs (in the same topology domain)
Origins report.IDList `json:"origins,omitempty"` // Core node IDs that contributed information
Metadata report.AggregateMetadata `json:"metadata"` // Numeric sums
}
// RenderableNodes is a set of RenderableNodes
type RenderableNodes map[string]RenderableNode
// Merge merges two sets of RenderableNodes
func (rns RenderableNodes) Merge(other RenderableNodes) {
for key, value := range other {
if existing, ok := rns[key]; ok {
existing.Merge(value)
rns[key] = existing
} else {
rns[key] = value
}
}
}
// Merge merges in another RenderableNode
func (rn *RenderableNode) Merge(other RenderableNode) {
if rn.LabelMajor == "" {
rn.LabelMajor = other.LabelMajor
}
if rn.LabelMinor == "" {
rn.LabelMinor = other.LabelMinor
}
if rn.Rank == "" {
rn.Rank = other.Rank
}
if rn.Pseudo != other.Pseudo {
panic(rn.ID)
}
rn.Adjacency = rn.Adjacency.Add(other.Adjacency...)
rn.Origins = rn.Origins.Add(other.Origins...)
rn.Metadata.Merge(other.Metadata)
}

View File

@@ -2,20 +2,18 @@ package render
import (
"reflect"
"github.com/weaveworks/scope/report"
)
// Diff is returned by TopoDiff. It represents the changes between two
// RenderableNode maps.
type Diff struct {
Add []report.RenderableNode `json:"add"`
Update []report.RenderableNode `json:"update"`
Remove []string `json:"remove"`
Add []RenderableNode `json:"add"`
Update []RenderableNode `json:"update"`
Remove []string `json:"remove"`
}
// TopoDiff gives you the diff to get from A to B.
func TopoDiff(a, b report.RenderableNodes) Diff {
func TopoDiff(a, b RenderableNodes) Diff {
diff := Diff{}
notSeen := map[string]struct{}{}

View File

@@ -1,22 +1,22 @@
package render
package render_test
import (
"reflect"
"sort"
"testing"
"github.com/weaveworks/scope/report"
"github.com/weaveworks/scope/render"
)
// ByID is a sort interface for a RenderableNode slice.
type ByID []report.RenderableNode
type ByID []render.RenderableNode
func (r ByID) Len() int { return len(r) }
func (r ByID) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ByID) Less(i, j int) bool { return r[i].ID < r[j].ID }
func TestTopoDiff(t *testing.T) {
nodea := report.RenderableNode{
nodea := render.RenderableNode{
ID: "nodea",
LabelMajor: "Node A",
LabelMinor: "'ts an a",
@@ -30,14 +30,14 @@ func TestTopoDiff(t *testing.T) {
"nodeb",
"nodeq", // not the same anymore
}
nodeb := report.RenderableNode{
nodeb := render.RenderableNode{
ID: "nodeb",
LabelMajor: "Node B",
}
// Helper to make RenderableNode maps.
nodes := func(ns ...report.RenderableNode) report.RenderableNodes {
r := report.RenderableNodes{}
nodes := func(ns ...render.RenderableNode) render.RenderableNodes {
r := render.RenderableNodes{}
for _, n := range ns {
r[n.ID] = n
}
@@ -46,40 +46,40 @@ func TestTopoDiff(t *testing.T) {
for _, c := range []struct {
label string
have, want Diff
have, want render.Diff
}{
{
label: "basecase: empty -> something",
have: TopoDiff(nodes(), nodes(nodea, nodeb)),
want: Diff{
Add: []report.RenderableNode{nodea, nodeb},
have: render.TopoDiff(nodes(), nodes(nodea, nodeb)),
want: render.Diff{
Add: []render.RenderableNode{nodea, nodeb},
},
},
{
label: "basecase: something -> empty",
have: TopoDiff(nodes(nodea, nodeb), nodes()),
want: Diff{
have: render.TopoDiff(nodes(nodea, nodeb), nodes()),
want: render.Diff{
Remove: []string{"nodea", "nodeb"},
},
},
{
label: "add and remove",
have: TopoDiff(nodes(nodea), nodes(nodeb)),
want: Diff{
Add: []report.RenderableNode{nodeb},
have: render.TopoDiff(nodes(nodea), nodes(nodeb)),
want: render.Diff{
Add: []render.RenderableNode{nodeb},
Remove: []string{"nodea"},
},
},
{
label: "no change",
have: TopoDiff(nodes(nodea), nodes(nodea)),
want: Diff{},
have: render.TopoDiff(nodes(nodea), nodes(nodea)),
want: render.Diff{},
},
{
label: "change a single node",
have: TopoDiff(nodes(nodea), nodes(nodeap)),
want: Diff{
Update: []report.RenderableNode{nodeap},
have: render.TopoDiff(nodes(nodea), nodes(nodeap)),
want: render.Diff{
Update: []render.RenderableNode{nodeap},
},
},
} {

View File

@@ -79,39 +79,3 @@ func (m *EdgeMetadata) Flatten(other EdgeMetadata) {
m.MaxConnCountTCP += other.MaxConnCountTCP
}
}
// Merge merges two sets of RenderableNodes
func (rns RenderableNodes) Merge(other RenderableNodes) {
for key, value := range other {
if existing, ok := rns[key]; ok {
existing.Merge(value)
rns[key] = existing
} else {
rns[key] = value
}
}
}
// Merge merges in another RenderableNode
func (rn *RenderableNode) Merge(other RenderableNode) {
if rn.LabelMajor == "" {
rn.LabelMajor = other.LabelMajor
}
if rn.LabelMinor == "" {
rn.LabelMinor = other.LabelMinor
}
if rn.Rank == "" {
rn.Rank = other.Rank
}
if rn.Pseudo != other.Pseudo {
panic(rn.ID)
}
rn.Adjacency = rn.Adjacency.Add(other.Adjacency...)
rn.Origins = rn.Origins.Add(other.Origins...)
rn.Metadata.Merge(other.Metadata)
}

View File

@@ -45,48 +45,6 @@ const (
HostNodeID = "host_node_id"
)
// RenderableNode is the data type that's yielded to the JavaScript layer as
// 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
Adjacency IDList `json:"adjacency,omitempty"` // Node IDs (in the same topology domain)
Origins IDList `json:"origins,omitempty"` // Core node IDs that contributed information
Metadata AggregateMetadata `json:"metadata"` // Numeric sums
}
// RenderableNodes is a set of RenderableNodes
type RenderableNodes map[string]RenderableNode
// 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"`
Pseudo bool `json:"pseudo,omitempty"`
Tables []Table `json:"tables"`
}
// 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?
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
}
// TopologySelector selects a single topology from a report.
type TopologySelector func(r Report) Topology