Merge pull request #116 from tomwilkie/105-grouping-pseudo

Group pseudo nodes by the node the referred to and their address in grouped topologies.
This commit is contained in:
Tom Wilkie
2015-05-27 11:09:36 +01:00
6 changed files with 107 additions and 100 deletions

View File

@@ -42,7 +42,7 @@ func makeTopologyList(rep Reporter) func(w http.ResponseWriter, r *http.Request)
Name: def.human,
URL: url,
GroupedURL: groupedURL,
Stats: stats(def.topologySelecter(rpt).RenderBy(def.MapFunc, false)),
Stats: stats(def.topologySelecter(rpt).RenderBy(def.MapFunc, def.PseudoFunc, false)),
})
}
respondWith(w, http.StatusOK, a)

View File

@@ -46,6 +46,7 @@ func makeTopologyHandlers(
rep Reporter,
topo topologySelecter,
mapping report.MapFunc,
pseudo report.PseudoFunc,
grouped bool,
get *mux.Router,
base string,
@@ -53,7 +54,7 @@ func makeTopologyHandlers(
// Full topology.
get.HandleFunc(base, func(w http.ResponseWriter, r *http.Request) {
respondWith(w, http.StatusOK, APITopology{
Nodes: topo(rep.Report()).RenderBy(mapping, grouped),
Nodes: topo(rep.Report()).RenderBy(mapping, pseudo, grouped),
})
})
@@ -71,7 +72,7 @@ func makeTopologyHandlers(
return
}
}
handleWebsocket(w, r, rep, topo, mapping, grouped, loop)
handleWebsocket(w, r, rep, topo, mapping, pseudo, grouped, loop)
})
// Individual nodes.
@@ -80,7 +81,7 @@ func makeTopologyHandlers(
vars = mux.Vars(r)
nodeID = vars["id"]
rpt = rep.Report()
node, ok = topo(rpt).RenderBy(mapping, grouped)[nodeID]
node, ok = topo(rpt).RenderBy(mapping, pseudo, grouped)[nodeID]
)
if !ok {
http.NotFound(w, r)
@@ -114,6 +115,7 @@ func handleWebsocket(
rep Reporter,
topo topologySelecter,
mapping report.MapFunc,
psuedo report.PseudoFunc,
grouped bool,
loop time.Duration,
) {
@@ -139,7 +141,7 @@ func handleWebsocket(
tick = time.Tick(loop)
)
for {
newTopo := topo(rep.Report()).RenderBy(mapping, grouped)
newTopo := topo(rep.Report()).RenderBy(mapping, psuedo, grouped)
diff := report.TopoDiff(previousTopo, newTopo)
previousTopo = newTopo

View File

@@ -19,6 +19,7 @@ func Router(c Reporter) *mux.Router {
c,
def.topologySelecter,
def.MapFunc,
def.PseudoFunc,
false, // not grouped
get,
"/api/topology/"+name,
@@ -28,6 +29,7 @@ func Router(c Reporter) *mux.Router {
c,
def.topologySelecter,
def.MapFunc,
def.PseudoFunc,
true, // grouped
get,
"/api/topology/"+name+"grouped",
@@ -44,9 +46,10 @@ var topologyRegistry = map[string]struct {
human string
topologySelecter
report.MapFunc
report.PseudoFunc
hasGrouped bool
}{
"applications": {"Applications", selectProcess, report.ProcessPID, true},
"containers": {"Containers", selectProcess, report.ProcessContainer, true},
"hosts": {"Hosts", selectNetwork, report.NetworkHostname, false},
"applications": {"Applications", selectProcess, report.ProcessPID, report.GenericPseudoNode, true},
"containers": {"Containers", selectProcess, report.ProcessContainer, report.NoPseudoNode, true},
"hosts": {"Hosts", selectNetwork, report.NetworkHostname, report.GenericPseudoNode, false},
}

View File

@@ -25,6 +25,12 @@ type MappedNode struct {
// rendered topology.
type MapFunc func(string, NodeMetadata, bool) (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)
// 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.
@@ -100,3 +106,47 @@ func NetworkHostname(_ string, m NodeMetadata, _ bool) (MappedNode, bool) {
Rank: parts[0],
}, name != ""
}
// GenericPseudoNode contains heuristics for building sensible pseudo nodes.
// It should go away.
func GenericPseudoNode(src string, srcMapped RenderableNode, dst string, grouped bool) (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.
srcNodeAddr, srcNodePort := trySplitAddr(src)
dstNodeAddr, _ := trySplitAddr(dst)
outputID = strings.Join([]string{"pseudo:", dstNodeAddr, srcNodeAddr, srcNodePort}, 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) {
return MappedNode{}, false
}
func trySplitAddr(addr string) (string, string) {
fields := strings.SplitN(addr, ScopeDelim, 3)
if len(fields) == 3 {
return fields[1], fields[2]
}
return fields[1], ""
}

View File

@@ -1,7 +1,6 @@
package report
import (
"net"
"reflect"
"strings"
)
@@ -70,7 +69,7 @@ func NewTopology() Topology {
//
// RenderBy takes a a MapFunc, which defines how to group and label nodes. If
// grouped is true, nodes that belong to the same "class" will be merged.
func (t Topology) RenderBy(f MapFunc, grouped bool) map[string]RenderableNode {
func (t Topology) RenderBy(mapFunc MapFunc, pseudoFunc PseudoFunc, grouped bool) map[string]RenderableNode {
nodes := map[string]RenderableNode{}
// Build a set of RenderableNodes for all non-pseudo probes, and an
@@ -78,7 +77,7 @@ func (t Topology) RenderBy(f MapFunc, grouped bool) map[string]RenderableNode {
// RenderableNodes.
address2mapped := map[string]string{}
for addressID, metadata := range t.NodeMetadatas {
mapped, ok := f(addressID, metadata, grouped)
mapped, ok := mapFunc(addressID, metadata, grouped)
if !ok {
continue
}
@@ -113,29 +112,15 @@ func (t Topology) RenderBy(f MapFunc, grouped bool) map[string]RenderableNode {
for _, dstNodeAddress := range dsts {
dstRenderableID, ok := address2mapped[dstNodeAddress]
if !ok {
// We don't have a node for this target address. So we'll make
// a pseudonode for it, instead.
var maj, min string
if dstNodeAddress == TheInternet {
dstRenderableID = dstNodeAddress
maj, min = formatLabel(dstNodeAddress)
} else if grouped {
dstRenderableID = localUnknown
maj, min = "", ""
} else {
// Rule for non-internet psuedo nodes; emit 1 new node for each
// dstNodeAddr, srcNodeAddr, srcNodePort.
srcNodeAddr, srcNodePort := trySplitAddr(srcNodeAddress)
dstNodeAddr, _ := trySplitAddr(dstNodeAddress)
// We don't care about <s>dst</s> remote port, just <s>src</s> local.
dstRenderableID = strings.Join([]string{"pseudo:", dstNodeAddr, srcNodeAddr, srcNodePort}, ScopeDelim)
maj, min = dstNodeAddr, ""
pseudoNode, ok := pseudoFunc(srcNodeAddress, srcRenderableNode, dstNodeAddress, grouped)
if !ok {
continue
}
dstRenderableID = pseudoNode.ID
nodes[dstRenderableID] = RenderableNode{
ID: dstRenderableID,
LabelMajor: maj,
LabelMinor: min,
ID: pseudoNode.ID,
LabelMajor: pseudoNode.Major,
LabelMinor: pseudoNode.Minor,
Pseudo: true,
Metadata: AggregateMetadata{}, // populated below - or not?
}
@@ -157,30 +142,22 @@ func (t Topology) RenderBy(f MapFunc, grouped bool) map[string]RenderableNode {
return nodes
}
func trySplitAddr(addr string) (string, string) {
fields := strings.SplitN(addr, ScopeDelim, 3)
if len(fields) == 3 {
return fields[1], fields[2]
}
return fields[1], ""
}
// EdgeMetadata gives the metadata of an edge from the perspective of the
// srcRenderableID. Since an edgeID can have multiple edges on the address
// level, it uses the supplied mapping function to translate address IDs to
// renderable node (mapped) IDs.
func (t Topology) EdgeMetadata(f MapFunc, grouped bool, srcRenderableID, dstRenderableID string) EdgeMetadata {
func (t Topology) EdgeMetadata(mapFunc MapFunc, grouped bool, srcRenderableID, dstRenderableID string) EdgeMetadata {
metadata := EdgeMetadata{}
for edgeID, edgeMeta := range t.EdgeMetadatas {
edgeParts := strings.SplitN(edgeID, IDDelim, 2)
src := edgeParts[0]
if src != TheInternet {
mapped, _ := f(src, t.NodeMetadatas[src], grouped)
mapped, _ := mapFunc(src, t.NodeMetadatas[src], grouped)
src = mapped.ID
}
dst := edgeParts[1]
if dst != TheInternet {
mapped, _ := f(dst, t.NodeMetadatas[dst], grouped)
mapped, _ := mapFunc(dst, t.NodeMetadatas[dst], grouped)
dst = mapped.ID
}
if src == srcRenderableID && dst == dstRenderableID {
@@ -190,26 +167,6 @@ func (t Topology) EdgeMetadata(f MapFunc, grouped bool, srcRenderableID, dstRend
return metadata
}
// formatLabel is an opportunistic helper to format any addressID into
// something we can show on screen.
func formatLabel(s string) (major, minor string) {
if s == TheInternet {
return "the Internet", ""
}
// Format is either "scope;ip;port", "scope;ip", or some process id.
parts := strings.SplitN(s, ScopeDelim, 3)
if len(parts) < 2 {
return s, ""
}
if len(parts) == 2 {
return parts[1], ""
}
return net.JoinHostPort(parts[1], parts[2]), ""
}
// Diff is returned by TopoDiff. It represents the changes between two
// RenderableNode maps.
type Diff struct {

View File

@@ -183,29 +183,19 @@ func TestRenderByProcessPID(t *testing.T) {
},
},
"pseudo:;10.10.10.10;192.168.1.1;80": {
ID: "pseudo:;10.10.10.10;192.168.1.1;80",
LabelMajor: "10.10.10.10",
LabelMinor: "",
Rank: "",
Pseudo: true,
Adjacency: nil,
OriginHosts: nil,
OriginNodes: nil,
Metadata: AggregateMetadata{},
ID: "pseudo:;10.10.10.10;192.168.1.1;80",
LabelMajor: "10.10.10.10",
Pseudo: true,
Metadata: AggregateMetadata{},
},
"pseudo:;10.10.10.11;192.168.1.1;80": {
ID: "pseudo:;10.10.10.11;192.168.1.1;80",
LabelMajor: "10.10.10.11",
LabelMinor: "",
Rank: "",
Pseudo: true,
Adjacency: nil,
OriginHosts: nil,
OriginNodes: nil,
Metadata: AggregateMetadata{},
ID: "pseudo:;10.10.10.11;192.168.1.1;80",
LabelMajor: "10.10.10.11",
Pseudo: true,
Metadata: AggregateMetadata{},
},
}
have := report.Process.RenderBy(ProcessPID, false)
have := report.Process.RenderBy(ProcessPID, GenericPseudoNode, false)
if !reflect.DeepEqual(want, have) {
t.Error("\n" + diff(want, have))
}
@@ -231,12 +221,16 @@ func TestRenderByProcessPIDGrouped(t *testing.T) {
},
},
"apache": {
ID: "apache",
LabelMajor: "apache",
LabelMinor: "",
Rank: "215",
Pseudo: false,
Adjacency: NewIDList("curl", "localUnknown"),
ID: "apache",
LabelMajor: "apache",
LabelMinor: "",
Rank: "215",
Pseudo: false,
Adjacency: NewIDList(
"curl",
"pseudo:;10.10.10.10;apache",
"pseudo:;10.10.10.11;apache",
),
OriginHosts: NewIDList("server.hostname.com"),
OriginNodes: NewIDList(";192.168.1.1;80"),
Metadata: AggregateMetadata{
@@ -244,19 +238,20 @@ func TestRenderByProcessPIDGrouped(t *testing.T) {
KeyBytesEgress: 1500,
},
},
"localUnknown": {
ID: "localUnknown",
LabelMajor: "",
LabelMinor: "",
Rank: "",
Pseudo: true,
Adjacency: nil,
OriginHosts: nil,
OriginNodes: nil,
Metadata: AggregateMetadata{},
"pseudo:;10.10.10.10;apache": {
ID: "pseudo:;10.10.10.10;apache",
LabelMajor: "10.10.10.10",
Pseudo: true,
Metadata: AggregateMetadata{},
},
"pseudo:;10.10.10.11;apache": {
ID: "pseudo:;10.10.10.11;apache",
LabelMajor: "10.10.10.11",
Pseudo: true,
Metadata: AggregateMetadata{},
},
}
have := report.Process.RenderBy(ProcessPID, true)
have := report.Process.RenderBy(ProcessPID, GenericPseudoNode, true)
if !reflect.DeepEqual(want, have) {
t.Error("\n" + diff(want, have))
}
@@ -315,7 +310,7 @@ func TestRenderByNetworkHostname(t *testing.T) {
Metadata: AggregateMetadata{},
},
}
have := report.Network.RenderBy(NetworkHostname, false)
have := report.Network.RenderBy(NetworkHostname, GenericPseudoNode, false)
if !reflect.DeepEqual(want, have) {
t.Error("\n" + diff(want, have))
}