mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-05 03:01:11 +00:00
Merge pull request #571 from weaveworks/561-sort-topologies
Sort the structs returned by /api/topology
This commit is contained in:
@@ -2,26 +2,140 @@ package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/weaveworks/scope/render"
|
||||
"github.com/weaveworks/scope/report"
|
||||
"github.com/weaveworks/scope/xfer"
|
||||
)
|
||||
|
||||
const apiTopologyURL = "/api/topology/"
|
||||
|
||||
var (
|
||||
topologyRegistry = ®istry{
|
||||
items: map[string]APITopologyDesc{},
|
||||
}
|
||||
kubernetesTopologies = []APITopologyDesc{
|
||||
{
|
||||
id: "pods",
|
||||
renderer: render.PodRenderer,
|
||||
Name: "Pods",
|
||||
Options: map[string][]APITopologyOption{"system": {
|
||||
{"show", "System containers shown", false, nop},
|
||||
{"hide", "System containers hidden", true, render.FilterSystem},
|
||||
}},
|
||||
},
|
||||
{
|
||||
id: "pods-by-service",
|
||||
parent: "pods",
|
||||
renderer: render.PodServiceRenderer,
|
||||
Name: "by service",
|
||||
Options: map[string][]APITopologyOption{"system": {
|
||||
{"show", "System containers shown", false, nop},
|
||||
{"hide", "System containers hidden", true, render.FilterSystem},
|
||||
}},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Topology option labels should tell the current state. The first item must
|
||||
// be the verb to get to that state
|
||||
topologyRegistry.add(
|
||||
APITopologyDesc{
|
||||
id: "applications",
|
||||
renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer),
|
||||
Name: "Applications",
|
||||
Options: map[string][]APITopologyOption{"unconnected": {
|
||||
// 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", true, nop},
|
||||
}},
|
||||
},
|
||||
APITopologyDesc{
|
||||
id: "applications-by-name",
|
||||
parent: "applications",
|
||||
renderer: render.FilterUnconnected(render.ProcessNameRenderer),
|
||||
Name: "by name",
|
||||
Options: map[string][]APITopologyOption{"unconnected": {
|
||||
// Ditto above.
|
||||
{"hide", "Unconnected nodes hidden", true, nop},
|
||||
}},
|
||||
},
|
||||
APITopologyDesc{
|
||||
id: "containers",
|
||||
renderer: render.ContainerWithImageNameRenderer,
|
||||
Name: "Containers",
|
||||
Options: map[string][]APITopologyOption{"system": {
|
||||
{"show", "System containers shown", false, nop},
|
||||
{"hide", "System containers hidden", true, render.FilterSystem},
|
||||
}},
|
||||
},
|
||||
APITopologyDesc{
|
||||
id: "containers-by-image",
|
||||
parent: "containers",
|
||||
renderer: render.ContainerImageRenderer,
|
||||
Name: "by image",
|
||||
Options: map[string][]APITopologyOption{"system": {
|
||||
{"show", "System containers shown", false, nop},
|
||||
{"hide", "System containers hidden", true, render.FilterSystem},
|
||||
}},
|
||||
},
|
||||
APITopologyDesc{
|
||||
id: "containers-by-hostname",
|
||||
parent: "containers",
|
||||
renderer: render.ContainerHostnameRenderer,
|
||||
Name: "by hostname",
|
||||
Options: map[string][]APITopologyOption{"system": {
|
||||
{"show", "System containers shown", false, nop},
|
||||
{"hide", "System containers hidden", true, render.FilterSystem},
|
||||
}},
|
||||
},
|
||||
APITopologyDesc{
|
||||
id: "hosts",
|
||||
renderer: render.HostRenderer,
|
||||
Name: "Hosts",
|
||||
Options: map[string][]APITopologyOption{},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// registry is a threadsafe store of the available topologies
|
||||
type registry struct {
|
||||
sync.RWMutex
|
||||
items map[string]APITopologyDesc
|
||||
}
|
||||
|
||||
// APITopologyDesc is returned in a list by the /api/topology handler.
|
||||
type APITopologyDesc struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
SubTopologies []APITopologyDesc `json:"sub_topologies,omitempty"`
|
||||
Options map[string][]APITopologyOption `json:"options"`
|
||||
Stats *topologyStats `json:"stats,omitempty"`
|
||||
id string
|
||||
parent string
|
||||
renderer render.Renderer
|
||||
|
||||
Name string `json:"name"`
|
||||
Options map[string][]APITopologyOption `json:"options"`
|
||||
|
||||
URL string `json:"url"`
|
||||
SubTopologies []APITopologyDesc `json:"sub_topologies,omitempty"`
|
||||
Stats *topologyStats `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
type byName []APITopologyDesc
|
||||
|
||||
func (a byName) Len() int { return len(a) }
|
||||
func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
||||
|
||||
// APITopologyOption describes a ¶m=value to a given topology.
|
||||
type APITopologyOption struct {
|
||||
Value string `json:"value"`
|
||||
Display string `json:"display"`
|
||||
Default bool `json:"default,omitempty"`
|
||||
|
||||
decorator func(render.Renderer) render.Renderer
|
||||
}
|
||||
|
||||
type topologyStats struct {
|
||||
@@ -31,71 +145,113 @@ type topologyStats struct {
|
||||
FilteredNodes int `json:"filtered_nodes"`
|
||||
}
|
||||
|
||||
func (r *registry) add(ts ...APITopologyDesc) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
for _, t := range ts {
|
||||
t.URL = apiTopologyURL + t.id
|
||||
|
||||
if t.parent != "" {
|
||||
parent := r.items[t.parent]
|
||||
parent.SubTopologies = append(parent.SubTopologies, t)
|
||||
sort.Sort(byName(parent.SubTopologies))
|
||||
r.items[t.parent] = parent
|
||||
}
|
||||
|
||||
r.items[t.id] = t
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registry) get(name string) (APITopologyDesc, bool) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
t, ok := r.items[name]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
func (r *registry) walk(f func(APITopologyDesc)) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
descs := []APITopologyDesc{}
|
||||
for _, desc := range r.items {
|
||||
if desc.parent != "" {
|
||||
continue
|
||||
}
|
||||
descs = append(descs, desc)
|
||||
}
|
||||
sort.Sort(byName(descs))
|
||||
for _, desc := range descs {
|
||||
f(desc)
|
||||
}
|
||||
}
|
||||
|
||||
// makeTopologyList returns a handler that yields an APITopologyList.
|
||||
func makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
func (r *registry) makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
var (
|
||||
rpt = rep.Report()
|
||||
topologies = []APITopologyDesc{}
|
||||
)
|
||||
topologyRegistry.walk(func(name string, def topologyView, subDefs map[string]topologyView) {
|
||||
describedSubDefs := []APITopologyDesc{}
|
||||
for subName, subDef := range subDefs {
|
||||
describedSubDefs = append(describedSubDefs, APITopologyDesc{
|
||||
Name: subDef.human,
|
||||
URL: "/api/topology/" + subName,
|
||||
Options: makeTopologyOptions(subDef),
|
||||
Stats: stats(subDef.renderer, rpt),
|
||||
})
|
||||
r.walk(func(desc APITopologyDesc) {
|
||||
decorateTopologyForRequest(req, &desc)
|
||||
decorateWithStats(&desc, rpt)
|
||||
for i := range desc.SubTopologies {
|
||||
decorateTopologyForRequest(req, &desc.SubTopologies[i])
|
||||
decorateWithStats(&desc.SubTopologies[i], rpt)
|
||||
}
|
||||
topologies = append(topologies, APITopologyDesc{
|
||||
Name: def.human,
|
||||
URL: "/api/topology/" + name,
|
||||
SubTopologies: describedSubDefs,
|
||||
Options: makeTopologyOptions(def),
|
||||
Stats: stats(def.renderer, rpt),
|
||||
})
|
||||
topologies = append(topologies, desc)
|
||||
})
|
||||
|
||||
respondWith(w, http.StatusOK, topologies)
|
||||
}
|
||||
}
|
||||
|
||||
func makeTopologyOptions(view topologyView) map[string][]APITopologyOption {
|
||||
options := map[string][]APITopologyOption{}
|
||||
for param, optionVals := range view.options {
|
||||
for _, optionVal := range optionVals {
|
||||
options[param] = append(options[param], APITopologyOption{
|
||||
Value: optionVal.value,
|
||||
Display: optionVal.human,
|
||||
Default: optionVal.def,
|
||||
})
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func stats(renderer render.Renderer, rpt report.Report) *topologyStats {
|
||||
func decorateWithStats(desc *APITopologyDesc, rpt report.Report) {
|
||||
var (
|
||||
nodes int
|
||||
realNodes int
|
||||
edges int
|
||||
)
|
||||
|
||||
for _, n := range renderer.Render(rpt) {
|
||||
for _, n := range desc.renderer.Render(rpt) {
|
||||
nodes++
|
||||
if !n.Pseudo {
|
||||
realNodes++
|
||||
}
|
||||
edges += len(n.Adjacency)
|
||||
}
|
||||
|
||||
renderStats := renderer.Stats(rpt)
|
||||
|
||||
return &topologyStats{
|
||||
renderStats := desc.renderer.Stats(rpt)
|
||||
desc.Stats = &topologyStats{
|
||||
NodeCount: nodes,
|
||||
NonpseudoNodeCount: realNodes,
|
||||
EdgeCount: edges,
|
||||
FilteredNodes: renderStats.FilteredNodes,
|
||||
}
|
||||
}
|
||||
|
||||
func nop(r render.Renderer) render.Renderer { return r }
|
||||
|
||||
func (r *registry) enableKubernetesTopologies() {
|
||||
r.add(kubernetesTopologies...)
|
||||
}
|
||||
|
||||
func decorateTopologyForRequest(r *http.Request, topology *APITopologyDesc) {
|
||||
for param, opts := range topology.Options {
|
||||
value := r.FormValue(param)
|
||||
for _, opt := range opts {
|
||||
if (value == "" && opt.Default) || (opt.Value != "" && opt.Value == value) {
|
||||
topology.renderer = opt.decorator(topology.renderer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registry) captureTopology(rep xfer.Reporter, f func(xfer.Reporter, APITopologyDesc, http.ResponseWriter, *http.Request)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
topology, ok := r.get(mux.Vars(req)["topology"])
|
||||
if !ok {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
decorateTopologyForRequest(req, &topology)
|
||||
f(rep, topology, w, req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,14 +33,14 @@ type APIEdge struct {
|
||||
}
|
||||
|
||||
// Full topology.
|
||||
func handleTopology(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *http.Request) {
|
||||
func handleTopology(rep xfer.Reporter, t APITopologyDesc, w http.ResponseWriter, r *http.Request) {
|
||||
respondWith(w, http.StatusOK, APITopology{
|
||||
Nodes: t.renderer.Render(rep.Report()).Prune(),
|
||||
})
|
||||
}
|
||||
|
||||
// Websocket for the full topology. This route overlaps with the next.
|
||||
func handleWs(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *http.Request) {
|
||||
func handleWs(rep xfer.Reporter, t APITopologyDesc, w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
respondWith(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -57,7 +57,7 @@ func handleWs(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// Individual nodes.
|
||||
func handleNode(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *http.Request) {
|
||||
func handleNode(rep xfer.Reporter, t APITopologyDesc, w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
vars = mux.Vars(r)
|
||||
nodeID = vars["id"]
|
||||
@@ -72,7 +72,7 @@ func handleNode(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
// Individual edges.
|
||||
func handleEdge(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *http.Request) {
|
||||
func handleEdge(rep xfer.Reporter, t APITopologyDesc, w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
vars = mux.Vars(r)
|
||||
localID = vars["local"]
|
||||
@@ -92,7 +92,7 @@ func handleWebsocket(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
rep xfer.Reporter,
|
||||
t topologyView,
|
||||
t APITopologyDesc,
|
||||
loop time.Duration,
|
||||
) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
|
||||
191
app/router.go
191
app/router.go
@@ -6,12 +6,10 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/PuerkitoBio/ghost/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/weaveworks/scope/render"
|
||||
"github.com/weaveworks/scope/report"
|
||||
"github.com/weaveworks/scope/xfer"
|
||||
)
|
||||
@@ -65,12 +63,17 @@ func Router(c collector) *mux.Router {
|
||||
|
||||
get := router.Methods("GET").Subrouter()
|
||||
get.HandleFunc("/api", gzipHandler(apiHandler))
|
||||
get.HandleFunc("/api/topology", gzipHandler(makeTopologyList(c)))
|
||||
get.HandleFunc("/api/topology/{topology}", gzipHandler(captureTopology(c, handleTopology)))
|
||||
get.HandleFunc("/api/topology/{topology}/ws", captureTopology(c, handleWs)) // NB not gzip!
|
||||
get.MatcherFunc(URLMatcher("/api/topology/{topology}/{id}")).HandlerFunc(gzipHandler(captureTopology(c, handleNode)))
|
||||
get.MatcherFunc(URLMatcher("/api/topology/{topology}/{local}/{remote}")).HandlerFunc(gzipHandler(captureTopology(c, handleEdge)))
|
||||
get.MatcherFunc(URLMatcher("/api/origin/host/{id}")).HandlerFunc(gzipHandler(makeOriginHostHandler(c)))
|
||||
get.HandleFunc("/api/topology", gzipHandler(topologyRegistry.makeTopologyList(c)))
|
||||
get.HandleFunc("/api/topology/{topology}",
|
||||
gzipHandler(topologyRegistry.captureTopology(c, handleTopology)))
|
||||
get.HandleFunc("/api/topology/{topology}/ws",
|
||||
topologyRegistry.captureTopology(c, handleWs)) // NB not gzip!
|
||||
get.MatcherFunc(URLMatcher("/api/topology/{topology}/{id}")).HandlerFunc(
|
||||
gzipHandler(topologyRegistry.captureTopology(c, handleNode)))
|
||||
get.MatcherFunc(URLMatcher("/api/topology/{topology}/{local}/{remote}")).HandlerFunc(
|
||||
gzipHandler(topologyRegistry.captureTopology(c, handleEdge)))
|
||||
get.MatcherFunc(URLMatcher("/api/origin/host/{id}")).HandlerFunc(
|
||||
gzipHandler(makeOriginHostHandler(c)))
|
||||
get.HandleFunc("/api/report", gzipHandler(makeRawReportHandler(c)))
|
||||
get.PathPrefix("/").Handler(http.FileServer(FS(false))) // everything else is static
|
||||
|
||||
@@ -98,35 +101,12 @@ func makeReportPostHandler(a xfer.Adder) http.HandlerFunc {
|
||||
}
|
||||
a.Add(rpt)
|
||||
if len(rpt.Pod.Nodes) > 0 {
|
||||
enableKubernetesTopologies()
|
||||
topologyRegistry.enableKubernetesTopologies()
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
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.get(mux.Vars(r)["topology"])
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
decorateTopologyForRequest(r, &topology)
|
||||
f(rep, topology, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// APIDetails are some generic details that can be fetched from /api
|
||||
type APIDetails struct {
|
||||
ID string `json:"id"`
|
||||
@@ -136,150 +116,3 @@ type APIDetails struct {
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
respondWith(w, http.StatusOK, APIDetails{ID: uniqueID, Version: version})
|
||||
}
|
||||
|
||||
// registry is a threadsafe store of the available topologies
|
||||
type registry struct {
|
||||
sync.RWMutex
|
||||
items map[string]topologyView
|
||||
}
|
||||
|
||||
func (r *registry) add(ts map[string]topologyView) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
result := map[string]topologyView{}
|
||||
for name, t := range r.items {
|
||||
result[name] = t
|
||||
}
|
||||
for name, t := range ts {
|
||||
result[name] = t
|
||||
}
|
||||
r.items = result
|
||||
}
|
||||
|
||||
func (r *registry) get(name string) (topologyView, bool) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
t, ok := r.items[name]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
func (r *registry) walk(f func(string, topologyView, map[string]topologyView)) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
for name, def := range r.items {
|
||||
if def.parent != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
subDefs := map[string]topologyView{}
|
||||
for subName, subDef := range r.items {
|
||||
if subDef.parent != name {
|
||||
continue
|
||||
}
|
||||
subDefs[subName] = subDef
|
||||
}
|
||||
|
||||
f(name, def, subDefs)
|
||||
}
|
||||
}
|
||||
|
||||
// Topology option labels should tell the current state. The first item must
|
||||
// be the verb to get to that state
|
||||
var topologyRegistry = ®istry{
|
||||
items: map[string]topologyView{
|
||||
"applications": {
|
||||
human: "Applications",
|
||||
parent: "",
|
||||
renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer),
|
||||
options: optionParams{"unconnected": {
|
||||
// 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", true, nop},
|
||||
}},
|
||||
},
|
||||
"applications-by-name": {
|
||||
human: "by name",
|
||||
parent: "applications",
|
||||
renderer: render.FilterUnconnected(render.ProcessNameRenderer),
|
||||
options: optionParams{"unconnected": {
|
||||
// Ditto above.
|
||||
{"hide", "Unconnected nodes hidden", true, nop},
|
||||
}},
|
||||
},
|
||||
"containers": {
|
||||
human: "Containers",
|
||||
parent: "",
|
||||
renderer: render.ContainerWithImageNameRenderer,
|
||||
options: optionParams{"system": {
|
||||
{"show", "System containers shown", false, nop},
|
||||
{"hide", "System containers hidden", true, render.FilterSystem},
|
||||
}},
|
||||
},
|
||||
"containers-by-image": {
|
||||
human: "by image",
|
||||
parent: "containers",
|
||||
renderer: render.ContainerImageRenderer,
|
||||
options: optionParams{"system": {
|
||||
{"show", "System containers shown", false, nop},
|
||||
{"hide", "System containers hidden", true, render.FilterSystem},
|
||||
}},
|
||||
},
|
||||
"containers-by-hostname": {
|
||||
human: "by hostname",
|
||||
parent: "containers",
|
||||
renderer: render.ContainerHostnameRenderer,
|
||||
options: optionParams{"system": {
|
||||
{"show", "System containers shown", false, nop},
|
||||
{"hide", "System containers hidden", true, render.FilterSystem},
|
||||
}},
|
||||
},
|
||||
"hosts": {
|
||||
human: "Hosts",
|
||||
parent: "",
|
||||
renderer: render.HostRenderer,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type topologyView struct {
|
||||
human string
|
||||
parent string
|
||||
renderer render.Renderer
|
||||
options optionParams
|
||||
}
|
||||
|
||||
type optionParams map[string][]optionValue // param: values
|
||||
|
||||
type optionValue struct {
|
||||
value string // "hide"
|
||||
human string // "Hide system containers"
|
||||
def bool
|
||||
decorator func(render.Renderer) render.Renderer
|
||||
}
|
||||
|
||||
func nop(r render.Renderer) render.Renderer { return r }
|
||||
|
||||
func enableKubernetesTopologies() {
|
||||
topologyRegistry.add(kubernetesTopologies)
|
||||
}
|
||||
|
||||
var kubernetesTopologies = map[string]topologyView{
|
||||
"pods": {
|
||||
human: "Pods",
|
||||
parent: "",
|
||||
renderer: render.PodRenderer,
|
||||
options: optionParams{"system": {
|
||||
{"show", "System containers shown", false, nop},
|
||||
{"hide", "System containers hidden", true, render.FilterSystem},
|
||||
}},
|
||||
},
|
||||
"pods-by-service": {
|
||||
human: "by service",
|
||||
parent: "pods",
|
||||
renderer: render.PodServiceRenderer,
|
||||
options: optionParams{"system": {
|
||||
{"show", "System containers shown", false, nop},
|
||||
{"hide", "System containers hidden", true, render.FilterSystem},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user