mirror of
https://github.com/weaveworks/scope.git
synced 2026-04-19 08:56:53 +00:00
Produce the container topology by way of the process topology.
This commit is contained in:
@@ -58,12 +58,9 @@ var topologyRegistry = map[string]topologyView{
|
||||
renderer: render.LeafMap{Selector: report.SelectEndpoint, Mapper: render.ProcessName, Pseudo: render.GenericGroupedPseudoNode},
|
||||
},
|
||||
"containers": {
|
||||
human: "Containers",
|
||||
parent: "",
|
||||
renderer: render.Reduce([]render.Renderer{
|
||||
render.LeafMap{Selector: report.SelectEndpoint, Mapper: render.MapEndpoint2Container, Pseudo: render.InternetOnlyPseudoNode},
|
||||
render.LeafMap{Selector: report.SelectContainer, Mapper: render.MapContainerIdentity, Pseudo: render.InternetOnlyPseudoNode},
|
||||
}),
|
||||
human: "Containers",
|
||||
parent: "",
|
||||
renderer: render.ContainerRenderer,
|
||||
},
|
||||
"containers-by-image": {
|
||||
human: "by image",
|
||||
|
||||
@@ -76,7 +76,11 @@ func addConnection(
|
||||
if _, ok := r.Endpoint.NodeMetadatas[scopedLocal]; !ok {
|
||||
// First hit establishes NodeMetadata for scoped local address + port
|
||||
md := report.NodeMetadata{
|
||||
"pid": fmt.Sprintf("%d", c.Proc.PID),
|
||||
"addr": c.LocalAddress.String(),
|
||||
"port": strconv.Itoa(int(c.LocalPort)),
|
||||
"pid": fmt.Sprintf("%d", c.Proc.PID),
|
||||
|
||||
// TODO: These can go away once we derives process graph from process topology
|
||||
"name": c.Proc.Name,
|
||||
"domain": hostID,
|
||||
}
|
||||
|
||||
@@ -7,30 +7,12 @@ import (
|
||||
"github.com/weaveworks/scope/report"
|
||||
)
|
||||
|
||||
const humanTheInternet = "the Internet"
|
||||
const (
|
||||
uncontainedID = "uncontained"
|
||||
uncontainedMajor = "Uncontained"
|
||||
|
||||
// NewRenderableNode makes a new RenderableNode
|
||||
func NewRenderableNode(id, major, minor, rank string) RenderableNode {
|
||||
return RenderableNode{
|
||||
ID: id,
|
||||
LabelMajor: major,
|
||||
LabelMinor: minor,
|
||||
Rank: rank,
|
||||
Pseudo: false,
|
||||
Metadata: report.AggregateMetadata{},
|
||||
}
|
||||
}
|
||||
|
||||
func newPseudoNode(id, major, minor string) RenderableNode {
|
||||
return RenderableNode{
|
||||
ID: id,
|
||||
LabelMajor: major,
|
||||
LabelMinor: minor,
|
||||
Rank: "",
|
||||
Pseudo: true,
|
||||
Metadata: report.AggregateMetadata{},
|
||||
}
|
||||
}
|
||||
humanTheInternet = "the Internet"
|
||||
)
|
||||
|
||||
// LeafMapFunc is anything which can take an arbitrary NodeMetadata, which is
|
||||
// always one-to-one with nodes in a topology, and return a specific
|
||||
@@ -54,6 +36,84 @@ type PseudoFunc func(srcNodeID string, srcNode RenderableNode, dstNodeID string)
|
||||
// return another RenderableNode.
|
||||
type MapFunc func(RenderableNode) (RenderableNode, bool)
|
||||
|
||||
// MapEndpointIdentity maps a endpoint topology node to endpoint RenderableNode node.
|
||||
// As it is only ever run on endpoint topology nodes, we can safely assume the
|
||||
// presences of certain keys.
|
||||
func MapEndpointIdentity(m report.NodeMetadata) (RenderableNode, bool) {
|
||||
var (
|
||||
id = fmt.Sprintf("endpoint:%s:%s:%s", m[report.HostNodeID], m["addr"], m["port"])
|
||||
major = fmt.Sprintf("%s:%s", m["addr"], m["port"])
|
||||
minor = fmt.Sprintf("%s (%s)", m[report.HostNodeID], m["pid"])
|
||||
rank = m["pid"]
|
||||
)
|
||||
return NewRenderableNode(id, major, minor, rank, m), true
|
||||
}
|
||||
|
||||
// MapProcessIdentity maps a process topology node to process RenderableNode node.
|
||||
// As it is only ever run on process topology nodes, we can safely assume the
|
||||
// presences of certain keys.
|
||||
func MapProcessIdentity(m report.NodeMetadata) (RenderableNode, bool) {
|
||||
var (
|
||||
id = fmt.Sprintf("pid:%s:%s", m[report.HostNodeID], m["pid"])
|
||||
major = m["comm"]
|
||||
minor = fmt.Sprintf("%s (%s)", m[report.HostNodeID], m["pid"])
|
||||
rank = m["pid"]
|
||||
)
|
||||
|
||||
return NewRenderableNode(id, major, minor, rank, m), true
|
||||
}
|
||||
|
||||
// MapContainerIdentity maps a container topology node to container RenderableNode node.
|
||||
// As it is only ever run on container topology nodes, we can safely assume the
|
||||
// presences of certain keys.
|
||||
func MapContainerIdentity(m report.NodeMetadata) (RenderableNode, bool) {
|
||||
var (
|
||||
id = m["docker_container_id"]
|
||||
major = m["docker_container_name"]
|
||||
minor = m[report.HostNodeID]
|
||||
rank = m["docker_image_id"]
|
||||
)
|
||||
|
||||
return NewRenderableNode(id, major, minor, rank, m), true
|
||||
}
|
||||
|
||||
// MapEndpoint2Process maps endpoint RenderableNodes to process RenderableNodes.
|
||||
//
|
||||
// If this function is given a pseudo node, then it will just return it;
|
||||
// Pseudo nodes will never have pids in them, and therefore will never
|
||||
// be able to be turned into a Process node.
|
||||
//
|
||||
// Otherwise, this function will produce a node with the correct ID
|
||||
// format for a process, but without any Major or Minor labels.
|
||||
// It does not have enough info to do that, and the resulting graph
|
||||
// must be merged with a process graph to get that info.
|
||||
func MapEndpoint2Process(n RenderableNode) (RenderableNode, bool) {
|
||||
if n.Pseudo {
|
||||
return n, true
|
||||
}
|
||||
|
||||
id := fmt.Sprintf("pid:%s:%s", n.NodeMetadata[report.HostNodeID], n.NodeMetadata["pid"])
|
||||
return newDerivedNode(id, n), true
|
||||
}
|
||||
|
||||
// MapProcess2Container maps process RenderableNodes to container RenderableNodes.
|
||||
//
|
||||
// If this function is given a node without a docker_container_id (including other
|
||||
// psueod nodes), it will produce an "Uncontained" pseudo node.
|
||||
//
|
||||
// Otherwise, this function will produce a node with the correct ID
|
||||
// format for a container, but without any Major or Minor labels.
|
||||
// It does not have enough info to do that, and the resulting graph
|
||||
// must be merged with a container graph to get that info.
|
||||
func MapProcess2Container(n RenderableNode) (RenderableNode, bool) {
|
||||
id, ok := n.NodeMetadata["docker_container_id"]
|
||||
if !ok {
|
||||
return newPseudoNode(uncontainedID, uncontainedMajor, ""), true
|
||||
}
|
||||
|
||||
return newDerivedNode(id, n), true
|
||||
}
|
||||
|
||||
// ProcessPID takes a node NodeMetadata from topology, and returns a
|
||||
// representation with the ID based on the process PID and the labels based on
|
||||
// the process name.
|
||||
@@ -64,7 +124,7 @@ func ProcessPID(m report.NodeMetadata) (RenderableNode, bool) {
|
||||
show = m["pid"] != "" && m["name"] != ""
|
||||
)
|
||||
|
||||
return NewRenderableNode(identifier, m["name"], minor, m["pid"]), show
|
||||
return NewRenderableNode(identifier, m["name"], minor, m["pid"], m), show
|
||||
}
|
||||
|
||||
// ProcessName takes a node NodeMetadata from a topology, and returns a
|
||||
@@ -72,34 +132,7 @@ func ProcessPID(m report.NodeMetadata) (RenderableNode, bool) {
|
||||
// processes with the same name together).
|
||||
func ProcessName(m report.NodeMetadata) (RenderableNode, bool) {
|
||||
show := m["pid"] != "" && m["name"] != ""
|
||||
return NewRenderableNode(m["name"], m["name"], "", m["name"]), show
|
||||
}
|
||||
|
||||
// MapEndpoint2Container maps endpoint 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.
|
||||
func MapEndpoint2Container(m report.NodeMetadata) (RenderableNode, bool) {
|
||||
var id, major, minor, rank string
|
||||
if m["docker_container_id"] == "" {
|
||||
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
|
||||
} else {
|
||||
id, major, minor, rank = m["docker_container_id"], "", m["domain"], ""
|
||||
}
|
||||
|
||||
return NewRenderableNode(id, major, minor, rank), true
|
||||
}
|
||||
|
||||
// MapContainerIdentity maps container topology node to container mapped nodes.
|
||||
func MapContainerIdentity(m report.NodeMetadata) (RenderableNode, bool) {
|
||||
var id, major, minor, rank string
|
||||
if m["docker_container_id"] == "" {
|
||||
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
|
||||
} else {
|
||||
id, major, minor, rank = m["docker_container_id"], m["docker_container_name"], m["domain"], m["docker_image_id"]
|
||||
}
|
||||
|
||||
return NewRenderableNode(id, major, minor, rank), true
|
||||
return NewRenderableNode(m["name"], m["name"], "", m["name"], m), show
|
||||
}
|
||||
|
||||
// ProcessContainerImage maps topology nodes to the container images they run
|
||||
@@ -113,7 +146,7 @@ func ProcessContainerImage(m report.NodeMetadata) (RenderableNode, bool) {
|
||||
id, major, minor, rank = m["docker_image_id"], m["docker_image_name"], "", m["docker_image_id"]
|
||||
}
|
||||
|
||||
return NewRenderableNode(id, major, minor, rank), true
|
||||
return NewRenderableNode(id, major, minor, rank, m), true
|
||||
}
|
||||
|
||||
// NetworkHostname takes a node NodeMetadata and returns a representation
|
||||
@@ -130,7 +163,7 @@ func NetworkHostname(m report.NodeMetadata) (RenderableNode, bool) {
|
||||
domain = parts[1]
|
||||
}
|
||||
|
||||
return NewRenderableNode(fmt.Sprintf("host:%s", name), parts[0], domain, parts[0]), name != ""
|
||||
return NewRenderableNode(fmt.Sprintf("host:%s", name), parts[0], domain, parts[0], m), name != ""
|
||||
}
|
||||
|
||||
// GenericPseudoNode contains heuristics for building sensible pseudo nodes.
|
||||
|
||||
@@ -54,38 +54,6 @@ func TestUngroupedMapping(t *testing.T) {
|
||||
wantMinor: "hosta (42)",
|
||||
wantRank: "42",
|
||||
},
|
||||
{
|
||||
f: render.MapEndpoint2Container,
|
||||
id: "foo-id",
|
||||
meta: report.NodeMetadata{
|
||||
"pid": "42",
|
||||
"name": "curl",
|
||||
"domain": "hosta",
|
||||
},
|
||||
wantOK: true,
|
||||
wantID: "uncontained",
|
||||
wantMajor: "Uncontained",
|
||||
wantMinor: "",
|
||||
wantRank: "uncontained",
|
||||
},
|
||||
{
|
||||
f: render.MapEndpoint2Container,
|
||||
id: "bar-id",
|
||||
meta: report.NodeMetadata{
|
||||
"pid": "42",
|
||||
"name": "curl",
|
||||
"domain": "hosta",
|
||||
"docker_container_id": "d321fe0",
|
||||
"docker_container_name": "walking_sparrow",
|
||||
"docker_image_id": "1101fff",
|
||||
"docker_image_name": "org/app:latest",
|
||||
},
|
||||
wantOK: true,
|
||||
wantID: "d321fe0",
|
||||
wantMajor: "",
|
||||
wantMinor: "hosta",
|
||||
wantRank: "",
|
||||
},
|
||||
} {
|
||||
identity := fmt.Sprintf("(%d %s %v)", i, c.id, c.meta)
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ type LeafMap struct {
|
||||
Pseudo PseudoFunc
|
||||
}
|
||||
|
||||
// MakeReduce is the only sane way to produce a Reduce Renderer
|
||||
func MakeReduce(renderers ...Renderer) Renderer {
|
||||
return Reduce(renderers)
|
||||
}
|
||||
|
||||
// Render produces a set of RenderableNodes given a Report
|
||||
func (r Reduce) Render(rpt report.Report) RenderableNodes {
|
||||
result := RenderableNodes{}
|
||||
|
||||
@@ -134,7 +134,7 @@ func TestMapEdge(t *testing.T) {
|
||||
}
|
||||
|
||||
identity := func(nmd report.NodeMetadata) (render.RenderableNode, bool) {
|
||||
return render.NewRenderableNode(nmd["id"], "", "", ""), true
|
||||
return render.NewRenderableNode(nmd["id"], "", "", "", nmd), true
|
||||
}
|
||||
|
||||
mapper := render.Map{
|
||||
@@ -331,6 +331,15 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func trimNodeMetadata(rns render.RenderableNodes) render.RenderableNodes {
|
||||
result := render.RenderableNodes{}
|
||||
for id, rn := range rns {
|
||||
rn.NodeMetadata = nil
|
||||
result[id] = rn
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestRenderByEndpointPID(t *testing.T) {
|
||||
want := render.RenderableNodes{
|
||||
"pid:client-54001-domain:10001": {
|
||||
@@ -392,9 +401,10 @@ func TestRenderByEndpointPID(t *testing.T) {
|
||||
}
|
||||
have := render.LeafMap{
|
||||
Selector: report.SelectEndpoint,
|
||||
Mapper: render.ProcessPID,
|
||||
Pseudo: render.GenericPseudoNode,
|
||||
Mapper: render.ProcessPID,
|
||||
Pseudo: render.GenericPseudoNode,
|
||||
}.Render(rpt)
|
||||
have = trimNodeMetadata(have)
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Error("\n" + diff(want, have))
|
||||
}
|
||||
@@ -450,9 +460,10 @@ func TestRenderByEndpointPIDGrouped(t *testing.T) {
|
||||
}
|
||||
have := render.LeafMap{
|
||||
Selector: report.SelectEndpoint,
|
||||
Mapper: render.ProcessName,
|
||||
Pseudo: render.GenericGroupedPseudoNode,
|
||||
Mapper: render.ProcessName,
|
||||
Pseudo: render.GenericGroupedPseudoNode,
|
||||
}.Render(rpt)
|
||||
have = trimNodeMetadata(have)
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Error("\n" + diff(want, have))
|
||||
}
|
||||
@@ -509,9 +520,10 @@ func TestRenderByNetworkHostname(t *testing.T) {
|
||||
}
|
||||
have := render.LeafMap{
|
||||
Selector: report.SelectAddress,
|
||||
Mapper: render.NetworkHostname,
|
||||
Pseudo: render.GenericPseudoNode,
|
||||
Mapper: render.NetworkHostname,
|
||||
Pseudo: render.GenericPseudoNode,
|
||||
}.Render(rpt)
|
||||
have = trimNodeMetadata(have)
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Error("\n" + diff(want, have))
|
||||
}
|
||||
|
||||
@@ -8,14 +8,15 @@ import (
|
||||
// an element of a topology. It should contain information that's relevant
|
||||
// to rendering a node when there are many nodes visible at once.
|
||||
type RenderableNode struct {
|
||||
ID string `json:"id"` //
|
||||
LabelMajor string `json:"label_major"` // e.g. "process", human-readable
|
||||
LabelMinor string `json:"label_minor,omitempty"` // e.g. "hostname", human-readable, optional
|
||||
Rank string `json:"rank"` // to help the layout engine
|
||||
Pseudo bool `json:"pseudo,omitempty"` // sort-of a placeholder node, for rendering purposes
|
||||
Adjacency report.IDList `json:"adjacency,omitempty"` // Node IDs (in the same topology domain)
|
||||
Origins report.IDList `json:"origins,omitempty"` // Core node IDs that contributed information
|
||||
Metadata report.AggregateMetadata `json:"metadata"` // Numeric sums
|
||||
ID string `json:"id"` //
|
||||
LabelMajor string `json:"label_major"` // e.g. "process", human-readable
|
||||
LabelMinor string `json:"label_minor,omitempty"` // e.g. "hostname", human-readable, optional
|
||||
Rank string `json:"rank"` // to help the layout engine
|
||||
Pseudo bool `json:"pseudo,omitempty"` // sort-of a placeholder node, for rendering purposes
|
||||
Adjacency report.IDList `json:"adjacency,omitempty"` // Node IDs (in the same topology domain)
|
||||
Origins report.IDList `json:"origins,omitempty"` // Core node IDs that contributed information
|
||||
Metadata report.AggregateMetadata `json:"metadata"` // Numeric sums
|
||||
NodeMetadata report.NodeMetadata `json:"-"` // merged NodeMetadata of the nodes used to build this
|
||||
}
|
||||
|
||||
// RenderableNodes is a set of RenderableNodes
|
||||
@@ -55,4 +56,43 @@ func (rn *RenderableNode) Merge(other RenderableNode) {
|
||||
rn.Origins = rn.Origins.Add(other.Origins...)
|
||||
|
||||
rn.Metadata.Merge(other.Metadata)
|
||||
rn.NodeMetadata.Merge(other.NodeMetadata)
|
||||
}
|
||||
|
||||
// NewRenderableNode makes a new RenderableNode
|
||||
func NewRenderableNode(id, major, minor, rank string, nmd report.NodeMetadata) RenderableNode {
|
||||
return RenderableNode{
|
||||
ID: id,
|
||||
LabelMajor: major,
|
||||
LabelMinor: minor,
|
||||
Rank: rank,
|
||||
Pseudo: false,
|
||||
Metadata: report.AggregateMetadata{},
|
||||
NodeMetadata: nmd,
|
||||
}
|
||||
}
|
||||
|
||||
func newDerivedNode(id string, node RenderableNode) RenderableNode {
|
||||
return RenderableNode{
|
||||
ID: id,
|
||||
LabelMajor: "",
|
||||
LabelMinor: "",
|
||||
Rank: "",
|
||||
Pseudo: node.Pseudo,
|
||||
Metadata: node.Metadata,
|
||||
Origins: node.Origins,
|
||||
NodeMetadata: node.NodeMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
func newPseudoNode(id, major, minor string) RenderableNode {
|
||||
return RenderableNode{
|
||||
ID: id,
|
||||
LabelMajor: major,
|
||||
LabelMinor: minor,
|
||||
Rank: "",
|
||||
Pseudo: true,
|
||||
Metadata: report.AggregateMetadata{},
|
||||
NodeMetadata: report.NodeMetadata{},
|
||||
}
|
||||
}
|
||||
|
||||
40
render/topologies.go
Normal file
40
render/topologies.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"github.com/weaveworks/scope/report"
|
||||
)
|
||||
|
||||
// EndpointRenderer is a Renderer which produces a renderable endpoint graph.
|
||||
var EndpointRenderer = LeafMap{
|
||||
Selector: report.SelectEndpoint,
|
||||
Mapper: MapEndpointIdentity,
|
||||
Pseudo: GenericPseudoNode,
|
||||
}
|
||||
|
||||
// ProcessRenderer is a Renderer which produces a renderable process
|
||||
// graph by merging the endpoint graph and the process topology.
|
||||
var ProcessRenderer = MakeReduce(
|
||||
Map{
|
||||
MapFunc: MapEndpoint2Process,
|
||||
Renderer: EndpointRenderer,
|
||||
},
|
||||
LeafMap{
|
||||
Selector: report.SelectProcess,
|
||||
Mapper: MapProcessIdentity,
|
||||
Pseudo: GenericPseudoNode,
|
||||
},
|
||||
)
|
||||
|
||||
// ContainerRenderer is a Renderer which produces a renderable container
|
||||
// graph by merging the process graph and the container topology.
|
||||
var ContainerRenderer = MakeReduce(
|
||||
Map{
|
||||
MapFunc: MapProcess2Container,
|
||||
Renderer: ProcessRenderer,
|
||||
},
|
||||
LeafMap{
|
||||
Selector: report.SelectContainer,
|
||||
Mapper: MapContainerIdentity,
|
||||
Pseudo: GenericPseudoNode,
|
||||
},
|
||||
)
|
||||
@@ -53,6 +53,11 @@ func SelectEndpoint(r Report) Topology {
|
||||
return r.Endpoint
|
||||
}
|
||||
|
||||
// SelectProcess selects the process topology.
|
||||
func SelectProcess(r Report) Topology {
|
||||
return r.Process
|
||||
}
|
||||
|
||||
// SelectAddress selects the address topology.
|
||||
func SelectAddress(r Report) Topology {
|
||||
return r.Address
|
||||
|
||||
Reference in New Issue
Block a user