Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/api_topologies.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ func makeTopologyList(rep Reporter) func(w http.ResponseWriter, r *http.Request)

url := "/api/topology/" + name
var groupedURL string
if def.hasGrouped {
groupedURL = url + "grouped"
if def.groupedTopology != "" {
groupedURL = "/api/topology/" + def.groupedTopology
}

a = append(a, APITopologyDesc{
Name: def.human,
URL: url,
GroupedURL: groupedURL,
Stats: stats(def.topologySelecter(rpt).RenderBy(def.MapFunc, def.PseudoFunc, false)),
Stats: stats(def.selector(rpt).RenderBy(def.mapper, def.pseudo)),
})
}
respondWith(w, http.StatusOK, a)
Expand Down
108 changes: 47 additions & 61 deletions app/api_topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,68 +41,57 @@ func selectNetwork(r report.Report) report.Topology {
return r.Network
}

// makeTopologyHandlers make /api/topology/* handlers.
func makeTopologyHandlers(
rep Reporter,
topo topologySelecter,
mapping report.MapFunc,
pseudo report.PseudoFunc,
grouped bool,
get *mux.Router,
base string,
) {
// Full topology.
get.HandleFunc(base, func(w http.ResponseWriter, r *http.Request) {
respondWith(w, http.StatusOK, APITopology{
Nodes: topo(rep.Report()).RenderBy(mapping, pseudo, grouped),
})
// Full topology.
func handleTopology(rep Reporter, t topologyView, w http.ResponseWriter, r *http.Request) {
respondWith(w, http.StatusOK, APITopology{
Nodes: t.selector(rep.Report()).RenderBy(t.mapper, t.pseudo),
})
}

// Websocket for the full topology. This route overlaps with the next.
get.HandleFunc(base+"/ws", func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
respondWith(w, http.StatusInternalServerError, err.Error())
// Websocket for the full topology. This route overlaps with the next.
func handleWs(rep Reporter, t topologyView, w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
respondWith(w, http.StatusInternalServerError, err.Error())
return
}
loop := websocketLoop
if t := r.Form.Get("t"); t != "" {
var err error
if loop, err = time.ParseDuration(t); err != nil {
respondWith(w, http.StatusBadRequest, t)
return
}
loop := websocketLoop
if t := r.Form.Get("t"); t != "" {
var err error
if loop, err = time.ParseDuration(t); err != nil {
respondWith(w, http.StatusBadRequest, t)
return
}
}
handleWebsocket(w, r, rep, topo, mapping, pseudo, grouped, loop)
})
}
handleWebsocket(w, r, rep, t, loop)
}

// Individual nodes.
get.HandleFunc(base+"/{id}", func(w http.ResponseWriter, r *http.Request) {
var (
vars = mux.Vars(r)
nodeID = vars["id"]
rpt = rep.Report()
node, ok = topo(rpt).RenderBy(mapping, pseudo, grouped)[nodeID]
)
if !ok {
http.NotFound(w, r)
return
}
originHostFunc := func(id string) (OriginHost, bool) { return getOriginHost(rpt.HostMetadatas, id) }
originNodeFunc := func(id string) (OriginNode, bool) { return getOriginNode(topo(rpt), id) }
respondWith(w, http.StatusOK, APINode{Node: makeDetailed(node, originHostFunc, originNodeFunc)})
})
// Individual nodes.
func handleNode(rep Reporter, t topologyView, w http.ResponseWriter, r *http.Request) {
var (
vars = mux.Vars(r)
nodeID = vars["id"]
rpt = rep.Report()
node, ok = t.selector(rpt).RenderBy(t.mapper, t.pseudo)[nodeID]
)
if !ok {
http.NotFound(w, r)
return
}
originHostFunc := func(id string) (OriginHost, bool) { return getOriginHost(rpt.HostMetadatas, id) }
originNodeFunc := func(id string) (OriginNode, bool) { return getOriginNode(t.selector(rpt), id) }
respondWith(w, http.StatusOK, APINode{Node: makeDetailed(node, originHostFunc, originNodeFunc)})
}

// Individual edges.
get.HandleFunc(base+"/{local}/{remote}", func(w http.ResponseWriter, r *http.Request) {
var (
vars = mux.Vars(r)
localID = vars["local"]
remoteID = vars["remote"]
rpt = rep.Report()
metadata = topo(rpt).EdgeMetadata(mapping, grouped, localID, remoteID).Transform()
)
respondWith(w, http.StatusOK, APIEdge{Metadata: metadata})
})
// Individual edges.
func handleEdge(rep Reporter, t topologyView, w http.ResponseWriter, r *http.Request) {
var (
vars = mux.Vars(r)
localID = vars["local"]
remoteID = vars["remote"]
rpt = rep.Report()
metadata = t.selector(rpt).EdgeMetadata(t.mapper, localID, remoteID).Transform()
)
respondWith(w, http.StatusOK, APIEdge{Metadata: metadata})
}

var upgrader = websocket.Upgrader{
Expand All @@ -113,10 +102,7 @@ func handleWebsocket(
w http.ResponseWriter,
r *http.Request,
rep Reporter,
topo topologySelecter,
mapping report.MapFunc,
psuedo report.PseudoFunc,
grouped bool,
t topologyView,
loop time.Duration,
) {
conn, err := upgrader.Upgrade(w, r, nil)
Expand All @@ -141,7 +127,7 @@ func handleWebsocket(
tick = time.Tick(loop)
)
for {
newTopo := topo(rep.Report()).RenderBy(mapping, psuedo, grouped)
newTopo := t.selector(rep.Report()).RenderBy(t.mapper, t.pseudo)
diff := report.TopoDiff(previousTopo, newTopo)
previousTopo = newTopo

Expand Down
61 changes: 29 additions & 32 deletions app/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,39 @@ func Router(c Reporter) *mux.Router {
router := mux.NewRouter()
get := router.Methods("GET").Subrouter()
get.HandleFunc("/api/topology", makeTopologyList(c))
for name, def := range topologyRegistry {
makeTopologyHandlers(
c,
def.topologySelecter,
def.MapFunc,
def.PseudoFunc,
false, // not grouped
get,
"/api/topology/"+name,
)
if def.hasGrouped {
makeTopologyHandlers(
c,
def.topologySelecter,
def.MapFunc,
def.PseudoFunc,
true, // grouped
get,
"/api/topology/"+name+"grouped",
)
}
}
get.HandleFunc("/api/topology/{topology}", captureTopology(c, handleTopology))
get.HandleFunc("/api/topology/{topology}/ws", captureTopology(c, handleWs))
get.HandleFunc("/api/topology/{topology}/{id}", captureTopology(c, handleNode))
get.HandleFunc("/api/topology/{topology}/{local}/{remote}", captureTopology(c, handleEdge))
get.HandleFunc("/api/origin/host/{id}", makeOriginHostHandler(c))
get.HandleFunc("/api/report", makeRawReportHandler(c))
get.PathPrefix("/").Handler(http.FileServer(FS(false))) // everything else is static
return router
}

var topologyRegistry = map[string]struct {
human string
topologySelecter
report.MapFunc
report.PseudoFunc
hasGrouped bool
}{
"applications": {"Applications", selectProcess, report.ProcessPID, report.GenericPseudoNode, true},
"containers": {"Containers", selectProcess, report.ProcessContainer, report.NoPseudoNode, true},
"hosts": {"Hosts", selectNetwork, report.NetworkHostname, report.GenericPseudoNode, false},
func captureTopology(rep Reporter, f func(Reporter, topologyView, http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
topology, ok := topologyRegistry[mux.Vars(r)["topology"]]
if !ok {
http.NotFound(w, r)

This comment was marked as abuse.

return
}
f(rep, topology, w, r)
}
}

type topologyView struct {
human string
selector topologySelecter
mapper report.MapFunc
pseudo report.PseudoFunc
groupedTopology string
}

var topologyRegistry = map[string]topologyView{
"applications": {"Applications", selectProcess, report.ProcessPID, report.GenericPseudoNode, "applications-grouped"},
"applications-grouped": {"Applications", selectProcess, report.ProcessName, report.GenericGroupedPseudoNode, ""},
"containers": {"Containers", selectProcess, report.ProcessContainer, report.NoPseudoNode, "containers-grouped"},
"containers-grouped": {"Containers", selectProcess, report.ProcessContainerImage, report.NoPseudoNode, ""},
"hosts": {"Hosts", selectNetwork, report.NetworkHostname, report.GenericPseudoNode, ""},
}
94 changes: 63 additions & 31 deletions report/mapping_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,24 @@ type MappedNode struct {
//
// If the final output parameter is false, the node shall be omitted from the
// rendered topology.
type MapFunc func(string, NodeMetadata, bool) (MappedNode, bool)
type MapFunc func(string, NodeMetadata) (MappedNode, bool)

// PseudoFunc creates MappedNode 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 RenderableNode, dstNodeID string, grouped bool) (MappedNode, bool)
type PseudoFunc func(srcNodeID string, srcNode RenderableNode, dstNodeID string) (MappedNode, bool)

// ProcessPID takes a node NodeMetadata from a Process topology, and returns a
// representation with the ID based on the process PID and the labels based
// on the process name.
func ProcessPID(_ string, m NodeMetadata, grouped bool) (MappedNode, bool) {
func ProcessPID(_ string, m NodeMetadata) (MappedNode, bool) {
var (
identifier = fmt.Sprintf("%s:%s:%s", "pid", m["domain"], m["pid"])
minor = fmt.Sprintf("%s (%s)", m["domain"], m["pid"])
show = m["pid"] != "" && m["name"] != ""
)

if grouped {
identifier = m["name"] // flatten
minor = "" // nothing meaningful to put here?
}

return MappedNode{
ID: identifier,
Major: m["name"],
Expand All @@ -54,27 +49,47 @@ func ProcessPID(_ string, m NodeMetadata, grouped bool) (MappedNode, bool) {
}, show
}

// ProcessName takes a node NodeMetadata from a Process topology, and returns a
// representation with the ID based on the process name (grouping all processes with
// the same name together).
func ProcessName(_ string, m NodeMetadata) (MappedNode, bool) {
show := m["pid"] != "" && m["name"] != ""
return MappedNode{
ID: m["name"],
Major: m["name"],
Minor: "",
Rank: m["name"],
}, show
}

// ProcessContainer maps Process topology nodes to the containers they run 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. If grouped is true, nodes with the
// same container image ID are merged together.
func ProcessContainer(_ string, m NodeMetadata, grouped bool) (MappedNode, bool) {
var (
containerID = m["docker_id"]
containerName = m["docker_name"]
imageID = m["docker_image_id"]
imageName = m["docker_image_name"]
domain = m["domain"]
)
// are grouped into the Uncontained node.
func ProcessContainer(_ string, m NodeMetadata) (MappedNode, bool) {
var id, major, minor, rank string
if m["docker_id"] == "" {
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
} else {
id, major, minor, rank = m["docker_id"], m["docker_name"], m["domain"], m["docker_image_id"]
}

return MappedNode{
ID: id,
Major: major,
Minor: minor,
Rank: rank,
}, true
}

// ProcessContainerImage maps Process topology nodes to the container images they run on.
// If no container metadata is found, nodes are grouped into the Uncontained node.
func ProcessContainerImage(_ string, m NodeMetadata) (MappedNode, bool) {
var id, major, minor, rank string
if containerID == "" {
if m["docker_image_id"] == "" {
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
} else if grouped {
id, major, minor, rank = imageID, imageName, "", imageID
} else {
id, major, minor, rank = containerID, containerName, domain, imageID
id, major, minor, rank = m["docker_image_id"], m["docker_image_name"], "", m["docker_image_id"]
}

return MappedNode{
Expand All @@ -88,7 +103,7 @@ func ProcessContainer(_ string, m NodeMetadata, grouped bool) (MappedNode, bool)
// NetworkHostname takes a node NodeMetadata from a Network topology, and
// returns a representation based on the hostname. Major label is the
// hostname, the minor label is the domain, if any.
func NetworkHostname(_ string, m NodeMetadata, _ bool) (MappedNode, bool) {
func NetworkHostname(_ string, m NodeMetadata) (MappedNode, bool) {
var (
name = m["name"]
domain = ""
Expand All @@ -109,18 +124,12 @@ func NetworkHostname(_ string, m NodeMetadata, _ bool) (MappedNode, bool) {

// GenericPseudoNode contains heuristics for building sensible pseudo nodes.
// It should go away.
func GenericPseudoNode(src string, srcMapped RenderableNode, dst string, grouped bool) (MappedNode, bool) {
func GenericPseudoNode(src string, srcMapped RenderableNode, dst string) (MappedNode, bool) {
var maj, min, outputID string

if dst == TheInternet {
outputID = dst
maj, min = "the Internet", ""
} else if grouped {
// When grouping, emit one pseudo node per (srcNodeAddress, dstNodeAddr)
dstNodeAddr, _ := trySplitAddr(dst)

outputID = strings.Join([]string{"pseudo:", dstNodeAddr, srcMapped.ID}, ScopeDelim)
maj, min = dstNodeAddr, ""
} else {
// Rule for non-internet psuedo nodes; emit 1 new node for each
// dstNodeAddr, srcNodeAddr, srcNodePort.
Expand All @@ -138,8 +147,31 @@ func GenericPseudoNode(src string, srcMapped RenderableNode, dst string, grouped
}, true
}

// GenericGroupedPseudoNode contains heuristics for building sensible pseudo nodes.
// It should go away.
func GenericGroupedPseudoNode(src string, srcMapped RenderableNode, dst string) (MappedNode, bool) {
var maj, min, outputID string

if dst == TheInternet {
outputID = dst
maj, min = "the Internet", ""
} else {
// When grouping, emit one pseudo node per (srcNodeAddress, dstNodeAddr)
dstNodeAddr, _ := trySplitAddr(dst)

outputID = strings.Join([]string{"pseudo:", dstNodeAddr, srcMapped.ID}, ScopeDelim)
maj, min = dstNodeAddr, ""
}

return MappedNode{
ID: outputID,
Major: maj,
Minor: min,
}, true
}

// NoPseudoNode never creates a pseudo node.
func NoPseudoNode(string, RenderableNode, string, bool) (MappedNode, bool) {
func NoPseudoNode(string, RenderableNode, string) (MappedNode, bool) {
return MappedNode{}, false
}

Expand Down
2 changes: 1 addition & 1 deletion report/mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestUngroupedMapping(t *testing.T) {
} {
identity := fmt.Sprintf("(%d %s %v)", i, c.id, c.meta)

m, haveOK := c.f(c.id, c.meta, false)
m, haveOK := c.f(c.id, c.meta)
if want, have := c.wantOK, haveOK; want != have {
t.Errorf("%s: map OK error: want %v, have %v", identity, want, have)
}
Expand Down
Loading