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
2 changes: 1 addition & 1 deletion app/api_topologies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 6 additions & 4 deletions app/api_topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@ 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, grouped),
Nodes: topo(rep.Report()).RenderBy(mapping, pseudo, grouped),
})
})

Expand All @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -114,6 +115,7 @@ func handleWebsocket(
rep Reporter,
topo topologySelecter,
mapping report.MapFunc,
psuedo report.PseudoFunc,
grouped bool,
loop time.Duration,
) {
Expand All @@ -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

Expand Down
9 changes: 6 additions & 3 deletions app/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func Router(c Reporter) *mux.Router {
c,
def.topologySelecter,
def.MapFunc,
def.PseudoFunc,
false, // not grouped
get,
"/api/topology/"+name,
Expand All @@ -28,6 +29,7 @@ func Router(c Reporter) *mux.Router {
c,
def.topologySelecter,
def.MapFunc,
def.PseudoFunc,
true, // grouped
get,
"/api/topology/"+name+"grouped",
Expand All @@ -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},
}
50 changes: 50 additions & 0 deletions report/mapping_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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], ""
}
67 changes: 12 additions & 55 deletions report/topology.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package report

import (
"net"
"reflect"
"strings"
)
Expand Down Expand Up @@ -70,15 +69,15 @@ 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
// addressID to nodeID lookup map. Multiple addressIDs can map to the same
// 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
}
Expand Down Expand Up @@ -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?
}
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
69 changes: 32 additions & 37 deletions report/topology_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand All @@ -231,32 +221,37 @@ 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{
KeyBytesIngress: 150,
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))
}
Expand Down Expand Up @@ -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))
}
Expand Down