From 2a4a33f30ae3b44e3e7430f70453108915c5f8a5 Mon Sep 17 00:00:00 2001 From: Tom Wilkie Date: Mon, 15 Jun 2015 17:48:08 +0000 Subject: [PATCH] Move RenderableNode and DetailedNode into render/ --- app/api_topologies.go | 4 +- app/api_topology.go | 8 +- experimental/graphviz/handle.go | 2 +- {report => render}/detailed_node.go | 49 +++++++-- {report => render}/detailed_node_test.go | 23 +++-- render/mapping.go | 32 +++--- render/mapping_test.go | 15 +-- render/render.go | 16 +-- render/render_test.go | 125 +++++++++++++++-------- render/renderable_node.go | 58 +++++++++++ render/topology_diff.go | 10 +- render/topology_diff_test.go | 42 ++++---- report/merge.go | 36 ------- report/report.go | 42 -------- 14 files changed, 256 insertions(+), 206 deletions(-) rename {report => render}/detailed_node.go (70%) rename {report => render}/detailed_node_test.go (68%) create mode 100644 render/renderable_node.go diff --git a/app/api_topologies.go b/app/api_topologies.go index e5f3a07bda..6c31fb795d 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -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 diff --git a/app/api_topology.go b/app/api_topology.go index 81f75e8925..9dd2333227 100644 --- a/app/api_topology.go +++ b/app/api_topology.go @@ -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 { diff --git a/experimental/graphviz/handle.go b/experimental/graphviz/handle.go index 382ec937ff..5dc611a94b 100644 --- a/experimental/graphviz/handle.go +++ b/experimental/graphviz/handle.go @@ -56,7 +56,7 @@ func handleHTML(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "\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") diff --git a/report/detailed_node.go b/render/detailed_node.go similarity index 70% rename from report/detailed_node.go rename to render/detailed_node.go index a7ecda388d..4552a7823b 100644 --- a/report/detailed_node.go +++ b/render/detailed_node.go @@ -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, ""}) diff --git a/report/detailed_node_test.go b/render/detailed_node_test.go similarity index 68% rename from report/detailed_node_test.go rename to render/detailed_node_test.go index a69980c638..0ca88387f5 100644 --- a/report/detailed_node_test.go +++ b/render/detailed_node_test.go @@ -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 diff --git a/render/mapping.go b/render/mapping.go index 9771b0eb69..e1973a85b0 100644 --- a/render/mapping.go +++ b/render/mapping.go @@ -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 diff --git a/render/mapping_test.go b/render/mapping_test.go index fa2e3470cc..7579e60745 100644 --- a/render/mapping_test.go +++ b/render/mapping_test.go @@ -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", diff --git a/render/render.go b/render/render.go index a59b0bc81d..f89843683e 100644 --- a/render/render.go +++ b/render/render.go @@ -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 diff --git a/render/render_test.go b/render/render_test.go index a7a885b0b7..a144e6e752 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -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)) } diff --git a/render/renderable_node.go b/render/renderable_node.go new file mode 100644 index 0000000000..2b6fd9a038 --- /dev/null +++ b/render/renderable_node.go @@ -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) +} diff --git a/render/topology_diff.go b/render/topology_diff.go index b384f1c646..9e0c552534 100644 --- a/render/topology_diff.go +++ b/render/topology_diff.go @@ -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{}{} diff --git a/render/topology_diff_test.go b/render/topology_diff_test.go index 90723a6b9f..1aa54b4246 100644 --- a/render/topology_diff_test.go +++ b/render/topology_diff_test.go @@ -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}, }, }, } { diff --git a/report/merge.go b/report/merge.go index a4e95401e0..2ef6f1fc95 100644 --- a/report/merge.go +++ b/report/merge.go @@ -78,39 +78,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) -} diff --git a/report/report.go b/report/report.go index 1e43deddbb..15b683e935 100644 --- a/report/report.go +++ b/report/report.go @@ -40,48 +40,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