Fixing grouped node count for filtered children nodes

Squash of:

* We have to keep all the container hostnames until the end so we can
  count how many we've filtered

* Adding tests for ContainerHostnameRenderer and PodServiceRenderer with
  filters

* Because we filter on image name we need the image name before
  filtering

* Alternative approach to passing decorators.

* Refactor out some of the decorator capture

* Don't memoise decorated calls to Render

* Fixing filtered counts on containers topology

  Tricky, because we need the filters to be silent sometimes (when they're
  in the middle), but not when they're at the top, so we take the "top"
  filter's stats. However, this means we have to compose all
  user-specified filters into a single Filter layer, so we can get all
  stats.

  There are no more Silent filters, as all filters are silent (unless they
  are at the top).

  Additionally, I clarified some of the filters as their usage/terminology
  was inconsistent and confused. Now Filter(IsFoo, ...) *keeps* only nodes
  where IsFoo is true.
This commit is contained in:
Paul Bellamy
2016-04-28 12:23:43 +01:00
parent fdaafd2208
commit 3d3aed2bb3
27 changed files with 554 additions and 263 deletions
+34 -39
View File
@@ -26,9 +26,9 @@ func init() {
ID: "system",
Default: "application",
Options: []APITopologyOption{
{"system", "System services", render.FilterApplication},
{"application", "Application services", render.FilterSystem},
{"both", "Both", render.FilterNoop},
{"system", "System services", render.IsSystem},
{"application", "Application services", render.IsApplication},
{"both", "Both", render.Noop},
},
},
}
@@ -38,9 +38,9 @@ func init() {
ID: "system",
Default: "application",
Options: []APITopologyOption{
{"system", "System pods", render.FilterApplication},
{"application", "Application pods", render.FilterSystem},
{"both", "Both", render.FilterNoop},
{"system", "System pods", render.IsSystem},
{"application", "Application pods", render.IsApplication},
{"both", "Both", render.Noop},
},
},
}
@@ -50,18 +50,18 @@ func init() {
ID: "system",
Default: "application",
Options: []APITopologyOption{
{"system", "System containers", render.FilterApplication},
{"application", "Application containers", render.FilterSystem},
{"both", "Both", render.FilterNoop},
{"system", "System containers", render.IsSystem},
{"application", "Application containers", render.IsApplication},
{"both", "Both", render.Noop},
},
},
{
ID: "stopped",
Default: "running",
Options: []APITopologyOption{
{"stopped", "Stopped containers", render.FilterRunning},
{"running", "Running containers", render.FilterStopped},
{"both", "Both", render.FilterNoop},
{"stopped", "Stopped containers", render.IsStopped},
{"running", "Running containers", render.IsRunning},
{"both", "Both", render.Noop},
},
},
}
@@ -73,7 +73,7 @@ func init() {
Options: []APITopologyOption{
// Show the user why there are filtered nodes in this view.
// Don't give them the option to show those nodes.
{"hide", "Unconnected nodes hidden", render.FilterNoop},
{"hide", "Unconnected nodes hidden", render.Noop},
},
},
}
@@ -181,7 +181,7 @@ type APITopologyOption struct {
Value string `json:"value"`
Label string `json:"label"`
decorator func(render.Renderer) render.Renderer
filter render.FilterFunc
}
type topologyStats struct {
@@ -247,31 +247,31 @@ func (r *registry) makeTopologyList(rep Reporter) CtxHandlerFunc {
func (r *registry) renderTopologies(rpt report.Report, req *http.Request) []APITopologyDesc {
topologies := []APITopologyDesc{}
r.walk(func(desc APITopologyDesc) {
renderer := renderedForRequest(req, desc)
desc.Stats = decorateWithStats(rpt, renderer)
renderer, decorator := renderedForRequest(req, desc)
desc.Stats = decorateWithStats(rpt, renderer, decorator)
for i := range desc.SubTopologies {
renderer := renderedForRequest(req, desc.SubTopologies[i])
desc.SubTopologies[i].Stats = decorateWithStats(rpt, renderer)
renderer, decorator := renderedForRequest(req, desc.SubTopologies[i])
desc.SubTopologies[i].Stats = decorateWithStats(rpt, renderer, decorator)
}
topologies = append(topologies, desc)
})
return topologies
}
func decorateWithStats(rpt report.Report, renderer render.Renderer) topologyStats {
func decorateWithStats(rpt report.Report, renderer render.Renderer, decorator render.Decorator) topologyStats {
var (
nodes int
realNodes int
edges int
)
for _, n := range renderer.Render(rpt) {
for _, n := range renderer.Render(rpt, decorator) {
nodes++
if n.Topology != render.Pseudo {
realNodes++
}
edges += len(n.Adjacency)
}
renderStats := renderer.Stats(rpt)
renderStats := renderer.Stats(rpt, decorator)
return topologyStats{
NodeCount: nodes,
NonpseudoNodeCount: realNodes,
@@ -280,20 +280,26 @@ func decorateWithStats(rpt report.Report, renderer render.Renderer) topologyStat
}
}
func renderedForRequest(r *http.Request, topology APITopologyDesc) render.Renderer {
renderer := topology.renderer
func renderedForRequest(r *http.Request, topology APITopologyDesc) (render.Renderer, render.Decorator) {
var filters []render.FilterFunc
for _, group := range topology.Options {
value := r.FormValue(group.ID)
for _, opt := range group.Options {
if (value == "" && group.Default == opt.Value) || (opt.Value != "" && opt.Value == value) {
renderer = opt.decorator(renderer)
filters = append(filters, opt.filter)
}
}
}
return renderer
return topology.renderer, func(renderer render.Renderer) render.Renderer {
return render.MakeFilter(render.ComposeFilterFuncs(filters...), renderer)
}
}
type reportRenderHandler func(context.Context, Reporter, render.Renderer, http.ResponseWriter, *http.Request)
type reportRenderHandler func(
context.Context,
Reporter, render.Renderer, render.Decorator,
http.ResponseWriter, *http.Request,
)
func (r *registry) captureRenderer(rep Reporter, f reportRenderHandler) CtxHandlerFunc {
return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
@@ -302,18 +308,7 @@ func (r *registry) captureRenderer(rep Reporter, f reportRenderHandler) CtxHandl
http.NotFound(w, req)
return
}
renderer := renderedForRequest(req, topology)
f(ctx, rep, renderer, w, req)
}
}
func (r *registry) captureRendererWithoutFilters(rep Reporter, f reportRenderHandler) CtxHandlerFunc {
return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
topology, ok := r.get(mux.Vars(req)["topology"])
if !ok {
http.NotFound(w, req)
return
}
f(ctx, rep, topology.renderer, w, req)
renderer, decorator := renderedForRequest(req, topology)
f(ctx, rep, renderer, decorator, w, req)
}
}
+20 -7
View File
@@ -28,19 +28,27 @@ type APINode struct {
}
// Full topology.
func handleTopology(ctx context.Context, rep Reporter, renderer render.Renderer, w http.ResponseWriter, r *http.Request) {
func handleTopology(
ctx context.Context,
rep Reporter, renderer render.Renderer, decorator render.Decorator,
w http.ResponseWriter, r *http.Request,
) {
report, err := rep.Report(ctx)
if err != nil {
respondWith(w, http.StatusInternalServerError, err.Error())
return
}
respondWith(w, http.StatusOK, APITopology{
Nodes: detailed.Summaries(report, renderer.Render(report)),
Nodes: detailed.Summaries(report, renderer.Render(report, decorator)),
})
}
// Websocket for the full topology. This route overlaps with the next.
func handleWs(ctx context.Context, rep Reporter, renderer render.Renderer, w http.ResponseWriter, r *http.Request) {
func handleWs(
ctx context.Context,
rep Reporter, renderer render.Renderer, decorator render.Decorator,
w http.ResponseWriter, r *http.Request,
) {
if err := r.ParseForm(); err != nil {
respondWith(w, http.StatusInternalServerError, err.Error())
return
@@ -53,17 +61,21 @@ func handleWs(ctx context.Context, rep Reporter, renderer render.Renderer, w htt
return
}
}
handleWebsocket(ctx, w, r, rep, renderer, loop)
handleWebsocket(ctx, w, r, rep, renderer, decorator, loop)
}
// Individual nodes.
func handleNode(ctx context.Context, rep Reporter, renderer render.Renderer, w http.ResponseWriter, r *http.Request) {
func handleNode(
ctx context.Context,
rep Reporter, renderer render.Renderer, _ render.Decorator,
w http.ResponseWriter, r *http.Request,
) {
var (
vars = mux.Vars(r)
topologyID = vars["topology"]
nodeID = vars["id"]
report, err = rep.Report(ctx)
rendered = renderer.Render(report)
rendered = renderer.Render(report, render.FilterNoop)
node, ok = rendered[nodeID]
)
if err != nil {
@@ -83,6 +95,7 @@ func handleWebsocket(
r *http.Request,
rep Reporter,
renderer render.Renderer,
decorator render.Decorator,
loop time.Duration,
) {
conn, err := xfer.Upgrade(w, r, nil)
@@ -119,7 +132,7 @@ func handleWebsocket(
log.Errorf("Error generating report: %v", err)
return
}
newTopo := detailed.Summaries(report, renderer.Render(report))
newTopo := detailed.Summaries(report, renderer.Render(report, decorator))
diff := detailed.TopoDiff(previousTopo, newTopo)
previousTopo = newTopo
+1 -1
View File
@@ -93,7 +93,7 @@ func RegisterTopologyRoutes(router *mux.Router, r Reporter) {
get.HandleFunc("/api/topology/{topology}/ws",
requestContextDecorator(topologyRegistry.captureRenderer(r, handleWs))) // NB not gzip!
get.MatcherFunc(URLMatcher("/api/topology/{topology}/{id}")).HandlerFunc(
gzipHandler(requestContextDecorator(topologyRegistry.captureRendererWithoutFilters(r, handleNode))))
gzipHandler(requestContextDecorator(topologyRegistry.captureRenderer(r, handleNode))))
get.HandleFunc("/api/report",
gzipHandler(requestContextDecorator(makeRawReportHandler(r))))
get.HandleFunc("/api/probes",