diff --git a/app/api_topologies.go b/app/api_topologies.go index f62511e3f..6a3af6f1d 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/report" "github.com/weaveworks/scope/xfer" ) @@ -27,6 +28,7 @@ type topologyStats struct { NodeCount int `json:"node_count"` NonpseudoNodeCount int `json:"nonpseudo_node_count"` EdgeCount int `json:"edge_count"` + FilteredNodes int `json:"filtered_nodes"` } // makeTopologyList returns a handler that yields an APITopologyList. @@ -41,16 +43,18 @@ func makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Req if def.parent != "" { continue } + decorateTopologyForRequest(r, &def) // Collect all sub-topologies of this one, depth=1 only. subTopologies := []APITopologyDesc{} for subName, subDef := range topologyRegistry { if subDef.parent == name { + decorateTopologyForRequest(r, &subDef) subTopologies = append(subTopologies, APITopologyDesc{ Name: subDef.human, URL: "/api/topology/" + subName, Options: makeTopologyOptions(subDef), - Stats: stats(subDef.renderer.Render(rpt)), + Stats: stats(subDef.renderer, rpt), }) } } @@ -61,7 +65,7 @@ func makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Req URL: "/api/topology/" + name, SubTopologies: subTopologies, Options: makeTopologyOptions(def), - Stats: stats(def.renderer.Render(rpt)), + Stats: stats(def.renderer, rpt), }) } respondWith(w, http.StatusOK, topologies) @@ -82,14 +86,14 @@ func makeTopologyOptions(view topologyView) map[string][]APITopologyOption { return options } -func stats(r render.RenderableNodes) *topologyStats { +func stats(renderer render.Renderer, rpt report.Report) *topologyStats { var ( nodes int realNodes int edges int ) - for _, n := range r { + for _, n := range renderer.Render(rpt) { nodes++ if !n.Pseudo { realNodes++ @@ -97,9 +101,12 @@ func stats(r render.RenderableNodes) *topologyStats { edges += len(n.Adjacency) } + renderStats := renderer.Stats(rpt) + return &topologyStats{ NodeCount: nodes, NonpseudoNodeCount: realNodes, EdgeCount: edges, + FilteredNodes: renderStats.FilteredNodes, } } diff --git a/app/router.go b/app/router.go index 9e99c3ed3..441580457 100644 --- a/app/router.go +++ b/app/router.go @@ -100,6 +100,17 @@ func makeReportPostHandler(a xfer.Adder) http.HandlerFunc { } } +func decorateTopologyForRequest(r *http.Request, topology *topologyView) { + for param, opts := range topology.options { + value := r.FormValue(param) + for _, opt := range opts { + if (value == "" && opt.def) || (opt.value != "" && opt.value == value) { + topology.renderer = opt.decorator(topology.renderer) + } + } + } +} + func captureTopology(rep xfer.Reporter, f func(xfer.Reporter, topologyView, http.ResponseWriter, *http.Request)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { topology, ok := topologyRegistry[mux.Vars(r)["topology"]] @@ -107,14 +118,7 @@ func captureTopology(rep xfer.Reporter, f func(xfer.Reporter, topologyView, http http.NotFound(w, r) return } - for param, opts := range topology.options { - value := r.FormValue(param) - for _, opt := range opts { - if (value == "" && opt.def) || (opt.value != "" && opt.value == value) { - topology.renderer = opt.decorator(topology.renderer) - } - } - } + decorateTopologyForRequest(r, &topology) f(rep, topology, w, r) } } @@ -135,7 +139,7 @@ var topologyRegistry = map[string]topologyView{ "applications": { human: "Applications", parent: "", - renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer{}), + renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer), }, "applications-by-name": { human: "by name", @@ -145,7 +149,7 @@ var topologyRegistry = map[string]topologyView{ "containers": { human: "Containers", parent: "", - renderer: render.ContainerWithImageNameRenderer{}, + renderer: render.ContainerWithImageNameRenderer, options: optionParams{"system": { {"show", "System containers shown", false, nop}, {"hide", "System containers hidden", true, render.FilterSystem}, diff --git a/render/render.go b/render/render.go index 0222c4fce..d964168e6 100644 --- a/render/render.go +++ b/render/render.go @@ -11,6 +11,18 @@ import ( type Renderer interface { Render(report.Report) RenderableNodes EdgeMetadata(rpt report.Report, localID, remoteID string) report.EdgeMetadata + Stats(report.Report) Stats +} + +// Stats is the type returned by Renderer.Stats +type Stats struct { + FilteredNodes int +} + +func (s Stats) merge(other Stats) Stats { + return Stats{ + FilteredNodes: s.FilteredNodes + other.FilteredNodes, + } } // Reduce renderer is a Renderer which merges together the output of several @@ -40,6 +52,15 @@ func (r Reduce) EdgeMetadata(rpt report.Report, localID, remoteID string) report return metadata } +// Stats implements Renderer +func (r Reduce) Stats(rpt report.Report) Stats { + var result Stats + for _, renderer := range r { + result = result.merge(renderer.Stats(rpt)) + } + return result +} + // Map is a Renderer which produces a set of RenderableNodes from the set of // RenderableNodes produced by another Renderer. type Map struct { @@ -54,6 +75,14 @@ func (m Map) Render(rpt report.Report) RenderableNodes { return output } +// Stats implements Renderer +func (m Map) Stats(rpt report.Report) Stats { + // There doesn't seem to be an instance where we want stats to recurse + // through Maps - for instance we don't want to see the number of filtered + // processes in the container renderer. + return Stats{} +} + func (m Map) render(rpt report.Report) (RenderableNodes, map[string]report.IDList) { var ( input = m.Renderer.Render(rpt) @@ -177,13 +206,21 @@ type Filter struct { // Render implements Renderer func (f Filter) Render(rpt report.Report) RenderableNodes { + nodes, _ := f.render(rpt) + return nodes +} + +func (f Filter) render(rpt report.Report) (RenderableNodes, int) { output := RenderableNodes{} inDegrees := map[string]int{} + filtered := 0 for id, node := range f.Renderer.Render(rpt) { if f.FilterFunc(node) { output[id] = node + inDegrees[id] = 0 + } else { + filtered++ } - inDegrees[id] = 0 } // Deleted nodes also need to be cut as destinations in adjacency lists. @@ -209,8 +246,17 @@ func (f Filter) Render(rpt report.Report) RenderableNodes { continue } delete(output, id) + filtered++ } - return output + return output, filtered +} + +// Stats implements Renderer +func (f Filter) Stats(rpt report.Report) Stats { + _, filtered := f.render(rpt) + var upstream = f.Renderer.Stats(rpt) + upstream.FilteredNodes += filtered + return upstream } // IsConnected is the key added to Node.Metadata by ColorConnected diff --git a/render/render_test.go b/render/render_test.go index f12f080b9..ed27099b4 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -20,6 +20,9 @@ func (m mockRenderer) Render(rpt report.Report) render.RenderableNodes { func (m mockRenderer) EdgeMetadata(rpt report.Report, localID, remoteID string) report.EdgeMetadata { return m.edgeMetadata } +func (m mockRenderer) Stats(rpt report.Report) render.Stats { + return render.Stats{} +} func TestReduceRender(t *testing.T) { renderer := render.Reduce([]render.Renderer{ diff --git a/render/selectors.go b/render/selectors.go index 88f186202..aafab995f 100644 --- a/render/selectors.go +++ b/render/selectors.go @@ -29,6 +29,11 @@ func (t TopologySelector) EdgeMetadata(rpt report.Report, srcID, dstID string) r return metadata } +// Stats implements Renderer +func (t TopologySelector) Stats(r report.Report) Stats { + return Stats{} +} + // MakeRenderableNodes converts a topology to a set of RenderableNodes func MakeRenderableNodes(t report.Topology) RenderableNodes { result := RenderableNodes{} diff --git a/render/topologies.go b/render/topologies.go index aff608093..13c10e700 100644 --- a/render/topologies.go +++ b/render/topologies.go @@ -27,14 +27,14 @@ var ProcessRenderer = MakeReduce( }, ) -// ProcessWithContainerNameRenderer is a Renderer which produces a process +// processWithContainerNameRenderer is a Renderer which produces a process // graph enriched with container names where appropriate -type ProcessWithContainerNameRenderer struct{} +type processWithContainerNameRenderer struct { + Renderer +} -// Render produces a process graph where the minor labels contain the -// container name, if found. -func (r ProcessWithContainerNameRenderer) Render(rpt report.Report) RenderableNodes { - processes := ProcessRenderer.Render(rpt) +func (r processWithContainerNameRenderer) Render(rpt report.Report) RenderableNodes { + processes := r.Renderer.Render(rpt) containers := Map{ MapFunc: MapContainerIdentity, Renderer: SelectContainer, @@ -60,10 +60,9 @@ func (r ProcessWithContainerNameRenderer) Render(rpt report.Report) RenderableNo return processes } -// EdgeMetadata produces an EdgeMetadata for a given edge. -func (r ProcessWithContainerNameRenderer) EdgeMetadata(rpt report.Report, localID, remoteID string) report.EdgeMetadata { - return ProcessRenderer.EdgeMetadata(rpt, localID, remoteID) -} +// ProcessWithContainerNameRenderer is a Renderer which produces a process +// graph enriched with container names where appropriate +var ProcessWithContainerNameRenderer = processWithContainerNameRenderer{ProcessRenderer} // ProcessRenderer is a Renderer which produces a renderable process // name graph by munging the progess graph. @@ -120,14 +119,14 @@ var ContainerRenderer = MakeReduce( }, ) -// ContainerWithImageNameRenderer is a Renderer which produces a container -// graph where the ranks are the image names, not their IDs -type ContainerWithImageNameRenderer struct{} +type containerWithImageNameRenderer struct { + Renderer +} // Render produces a process graph where the minor labels contain the // container name, if found. -func (r ContainerWithImageNameRenderer) Render(rpt report.Report) RenderableNodes { - containers := ContainerRenderer.Render(rpt) +func (r containerWithImageNameRenderer) Render(rpt report.Report) RenderableNodes { + containers := r.Renderer.Render(rpt) images := Map{ MapFunc: MapContainerImageIdentity, Renderer: SelectContainerImage, @@ -149,10 +148,9 @@ func (r ContainerWithImageNameRenderer) Render(rpt report.Report) RenderableNode return containers } -// EdgeMetadata produces an EdgeMetadata for a given edge. -func (r ContainerWithImageNameRenderer) EdgeMetadata(rpt report.Report, localID, remoteID string) report.EdgeMetadata { - return ContainerRenderer.EdgeMetadata(rpt, localID, remoteID) -} +// ContainerWithImageNameRenderer is a Renderer which produces a container +// graph where the ranks are the image names, not their IDs +var ContainerWithImageNameRenderer = containerWithImageNameRenderer{ContainerRenderer} // ContainerImageRenderer is a Renderer which produces a renderable container // image graph by merging the container graph and the container image topology. diff --git a/render/topologies_test.go b/render/topologies_test.go index e87e0b229..07c5ab865 100644 --- a/render/topologies_test.go +++ b/render/topologies_test.go @@ -27,7 +27,7 @@ func TestProcessNameRenderer(t *testing.T) { } func TestContainerRenderer(t *testing.T) { - have := (render.ContainerWithImageNameRenderer{}.Render(test.Report)).Prune() + have := (render.ContainerWithImageNameRenderer.Render(test.Report)).Prune() want := expected.RenderedContainers if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) @@ -39,7 +39,7 @@ func TestContainerFilterRenderer(t *testing.T) { // it is filtered out correctly. input := test.Report.Copy() input.Container.Nodes[test.ClientContainerNodeID].Metadata[docker.LabelPrefix+"works.weave.role"] = "system" - have := render.FilterSystem(render.ContainerWithImageNameRenderer{}).Render(input).Prune() + have := render.FilterSystem(render.ContainerWithImageNameRenderer).Render(input).Prune() want := expected.RenderedContainers.Copy() delete(want, test.ClientContainerID) if !reflect.DeepEqual(want, have) {