Adding support for plugins, with basic example of iowait, and ebpf

Squash of:
* Include plugins in the report
* show plugin list in the UI
* moving metric and metadata templates into the probe reports
* update js for prime -> priority
* added retry to plugin handshake
* added iowait plugin
* review feedback
* plugin documentation
This commit is contained in:
Paul Bellamy
2016-04-12 17:22:14 +01:00
parent f899f4451b
commit 7632e0b3c5
50 changed files with 2168 additions and 537 deletions
+9 -9
View File
@@ -65,7 +65,7 @@ func (row connection) ID() string {
return fmt.Sprintf("%s:%s-%s:%s-%s", row.remoteNode.ID, row.remoteAddr, row.localNode.ID, row.localAddr, row.port)
}
func incomingConnectionsSummary(topologyID string, n report.Node, ns report.Nodes) ConnectionsSummary {
func incomingConnectionsSummary(topologyID string, r report.Report, n report.Node, ns report.Nodes) ConnectionsSummary {
localEndpointIDs := endpointChildIDsOf(n)
// For each node which has an edge TO me
@@ -110,11 +110,11 @@ func incomingConnectionsSummary(topologyID string, n report.Node, ns report.Node
TopologyID: topologyID,
Label: "Inbound",
Columns: columnHeaders,
Connections: connectionRows(counts, isInternetNode(n)),
Connections: connectionRows(r, counts, isInternetNode(n)),
}
}
func outgoingConnectionsSummary(topologyID string, n report.Node, ns report.Nodes) ConnectionsSummary {
func outgoingConnectionsSummary(topologyID string, r report.Report, n report.Node, ns report.Nodes) ConnectionsSummary {
localEndpoints := endpointChildrenOf(n)
// For each node which has an edge FROM me
@@ -160,7 +160,7 @@ func outgoingConnectionsSummary(topologyID string, n report.Node, ns report.Node
TopologyID: topologyID,
Label: "Outbound",
Columns: columnHeaders,
Connections: connectionRows(counts, isInternetNode(n)),
Connections: connectionRows(r, counts, isInternetNode(n)),
}
}
@@ -188,13 +188,13 @@ func isInternetNode(n report.Node) bool {
return n.ID == render.IncomingInternetID || n.ID == render.OutgoingInternetID
}
func connectionRows(in map[connection]int, includeLocal bool) []Connection {
func connectionRows(r report.Report, in map[connection]int, includeLocal bool) []Connection {
output := []Connection{}
for row, count := range in {
// Use MakeNodeSummary to render the id and label of this node
// TODO(paulbellamy): Would be cleaner if we hade just a
// MakeNodeID(*row.remoteode). As we don't need the whole summary.
summary, ok := MakeNodeSummary(*row.remoteNode)
summary, ok := MakeNodeSummary(r, *row.remoteNode)
connection := Connection{
ID: row.ID(),
NodeID: summary.ID,
@@ -207,19 +207,19 @@ func connectionRows(in map[connection]int, includeLocal bool) []Connection {
}
if includeLocal {
connection.Metadata = append(connection.Metadata,
MetadataRow{
report.MetadataRow{
ID: "foo",
Value: row.localAddr,
Datatype: number,
})
}
connection.Metadata = append(connection.Metadata,
MetadataRow{
report.MetadataRow{
ID: portKey,
Value: row.port,
Datatype: number,
},
MetadataRow{
report.MetadataRow{
ID: countKey,
Value: strconv.Itoa(count),
Datatype: number,
+3 -3
View File
@@ -10,7 +10,7 @@ import (
// NodeDockerLabels produces a table (to be consumed directly by the UI) based
// on an origin ID, which is (optimistically) a node ID in one of our
// topologies.
func NodeDockerLabels(nmd report.Node) []MetadataRow {
func NodeDockerLabels(nmd report.Node) []report.MetadataRow {
if _, ok := nmd.Counters.Lookup(nmd.Topology); ok {
// This is a group of nodes, so no docker labels!
return nil
@@ -20,7 +20,7 @@ func NodeDockerLabels(nmd report.Node) []MetadataRow {
return nil
}
var rows []MetadataRow
var rows []report.MetadataRow
// Add labels in alphabetical order
labels := docker.ExtractLabels(nmd)
labelKeys := make([]string, 0, len(labels))
@@ -29,7 +29,7 @@ func NodeDockerLabels(nmd report.Node) []MetadataRow {
}
sort.Strings(labelKeys)
for _, labelKey := range labelKeys {
rows = append(rows, MetadataRow{ID: "label_" + labelKey, Value: labels[labelKey]})
rows = append(rows, report.MetadataRow{ID: "label_" + labelKey, Value: labels[labelKey]})
}
return rows
}
+2 -2
View File
@@ -15,7 +15,7 @@ func TestNodeDockerLabels(t *testing.T) {
inputs := []struct {
name string
node report.Node
want []detailed.MetadataRow
want []report.MetadataRow
}{
{
name: "container",
@@ -26,7 +26,7 @@ func TestNodeDockerLabels(t *testing.T) {
}).WithTopology(report.Container).WithSets(report.EmptySets.
Add(docker.ContainerIPs, report.MakeStringSet("10.10.10.0/24", "10.10.10.1/24")),
),
want: []detailed.MetadataRow{
want: []report.MetadataRow{
{
ID: "label_label1",
Value: "label1value",
+4 -175
View File
@@ -1,190 +1,19 @@
package detailed
import (
"strconv"
"strings"
"github.com/ugorji/go/codec"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/probe/overlay"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
)
var (
processNodeMetadata = []MetadataRowTemplate{
Latest{ID: process.PID, Prime: true, Datatype: number},
Latest{ID: process.Cmdline, Prime: true},
Latest{ID: process.PPID, Prime: true},
Latest{ID: process.Threads, Prime: true},
}
containerNodeMetadata = []MetadataRowTemplate{
Latest{ID: docker.ContainerID, Truncate: 12, Prime: true},
Latest{ID: docker.ContainerStateHuman, Prime: true},
Latest{ID: docker.ContainerCommand, Prime: true},
Latest{ID: docker.ImageID, Truncate: 12},
Latest{ID: docker.ContainerUptime},
Latest{ID: docker.ContainerRestartCount},
Set{ID: docker.ContainerIPs},
Set{ID: docker.ContainerPorts},
Latest{ID: docker.ContainerCreated},
Latest{ID: overlay.WeaveMACAddress},
Latest{ID: overlay.WeaveDNSHostname},
}
containerImageNodeMetadata = []MetadataRowTemplate{
Latest{ID: docker.ImageID, Truncate: 12, Prime: true},
Counter{ID: report.Container, Prime: true},
}
podNodeMetadata = []MetadataRowTemplate{
Latest{ID: kubernetes.PodID, Prime: true},
Latest{ID: kubernetes.Namespace, Prime: true},
Latest{ID: kubernetes.PodCreated, Prime: true},
}
hostNodeMetadata = []MetadataRowTemplate{
Latest{ID: host.KernelVersion, Prime: true},
Latest{ID: host.Uptime, Prime: true},
Latest{ID: host.HostName},
Latest{ID: host.OS},
Set{ID: host.LocalNetworks},
}
)
// MetadataRowTemplate extracts some metadata rows from a node
type MetadataRowTemplate interface {
MetadataRows(report.Node) []MetadataRow
}
// Latest extracts some metadata rows from a node's Latest
type Latest struct {
ID string
Truncate int // If > 0, truncate the value to this length.
Prime bool // Whether the row should be shown by default
Datatype string
}
// MetadataRows implements MetadataRowTemplate
func (l Latest) MetadataRows(n report.Node) []MetadataRow {
if val, ok := n.Latest.Lookup(l.ID); ok {
if l.Truncate > 0 && len(val) > l.Truncate {
val = val[:l.Truncate]
}
return []MetadataRow{{ID: l.ID, Value: val, Prime: l.Prime, Datatype: l.Datatype}}
}
return nil
}
// Set extracts some metadata rows from a node's Sets
type Set struct {
ID string
}
// MetadataRows implements MetadataRowTemplate
func (s Set) MetadataRows(n report.Node) []MetadataRow {
if val, ok := n.Sets.Lookup(s.ID); ok && len(val) > 0 {
return []MetadataRow{{ID: s.ID, Value: strings.Join(val, ", ")}}
}
return nil
}
// Counter extracts some metadata rows from a node's Counters
type Counter struct {
ID string
Prime bool
}
// MetadataRows implements MetadataRowTemplate
func (c Counter) MetadataRows(n report.Node) []MetadataRow {
if val, ok := n.Counters.Lookup(c.ID); ok {
return []MetadataRow{{
ID: c.ID,
Value: strconv.Itoa(val),
Prime: c.Prime,
Datatype: number,
}}
}
return nil
}
// MetadataRow is a row for the metadata table.
type MetadataRow struct {
ID string
Value string
Prime bool
Datatype string
}
// Copy returns a value copy of a metadata row.
func (m MetadataRow) Copy() MetadataRow {
return m
}
// MarshalJSON shouldn't be used, use CodecEncodeSelf instead
func (MetadataRow) MarshalJSON() ([]byte, error) {
panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead")
}
// UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead
func (*MetadataRow) UnmarshalJSON(b []byte) error {
panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead")
}
type labelledMetadataRow struct {
ID string `json:"id"`
Label string `json:"label"`
Value string `json:"value"`
Prime bool `json:"prime,omitempty"`
Datatype string `json:"dataType,omitempty"`
}
// CodecEncodeSelf marshals this MetadataRow. It adds a label before
// rendering.
func (m *MetadataRow) CodecEncodeSelf(encoder *codec.Encoder) {
in := labelledMetadataRow{
ID: m.ID,
Label: Label(m.ID),
Value: m.Value,
Prime: m.Prime,
Datatype: m.Datatype,
}
encoder.Encode(in)
}
// CodecDecodeSelf implements codec.Selfer
func (m *MetadataRow) CodecDecodeSelf(decoder *codec.Decoder) {
var in labelledMetadataRow
decoder.Decode(&in)
*m = MetadataRow{
ID: in.ID,
Value: in.Value,
Prime: in.Prime,
Datatype: in.Datatype,
}
}
// NodeMetadata produces a table (to be consumed directly by the UI) based on
// an origin ID, which is (optimistically) a node ID in one of our topologies.
func NodeMetadata(n report.Node) []MetadataRow {
// an a report.Node, which is (hopefully) a node in one of our topologies.
func NodeMetadata(r report.Report, n report.Node) []report.MetadataRow {
if _, ok := n.Counters.Lookup(n.Topology); ok {
// This is a group of nodes, so no metadata!
return nil
}
renderers := map[string][]MetadataRowTemplate{
report.Process: processNodeMetadata,
report.Container: containerNodeMetadata,
report.ContainerImage: containerImageNodeMetadata,
report.Pod: podNodeMetadata,
report.Host: hostNodeMetadata,
}
if templates, ok := renderers[n.Topology]; ok {
rows := []MetadataRow{}
for _, template := range templates {
rows = append(rows, template.MetadataRows(n)...)
}
return rows
if topology, ok := r.Topology(n.Topology); ok {
return topology.MetadataTemplates.MetadataRows(n)
}
return nil
}
+10 -10
View File
@@ -15,7 +15,7 @@ func TestNodeMetadata(t *testing.T) {
inputs := []struct {
name string
node report.Node
want []detailed.MetadataRow
want []report.MetadataRow
}{
{
name: "container",
@@ -26,10 +26,10 @@ func TestNodeMetadata(t *testing.T) {
}).WithTopology(report.Container).WithSets(report.EmptySets.
Add(docker.ContainerIPs, report.MakeStringSet("10.10.10.0/24", "10.10.10.1/24")),
),
want: []detailed.MetadataRow{
{ID: docker.ContainerID, Value: fixture.ClientContainerID, Prime: true},
{ID: docker.ContainerStateHuman, Value: "running", Prime: true},
{ID: docker.ContainerIPs, Value: "10.10.10.0/24, 10.10.10.1/24"},
want: []report.MetadataRow{
{ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 1},
{ID: docker.ContainerStateHuman, Label: "State", Value: "running", Priority: 2},
{ID: docker.ContainerIPs, Label: "IPs", Value: "10.10.10.0/24, 10.10.10.1/24", Priority: 14},
},
},
{
@@ -41,7 +41,7 @@ func TestNodeMetadata(t *testing.T) {
},
}
for _, input := range inputs {
have := detailed.NodeMetadata(input.node)
have := detailed.NodeMetadata(fixture.Report, input.node)
if !reflect.DeepEqual(input.want, have) {
t.Errorf("%s: %s", input.name, test.Diff(input.want, have))
}
@@ -50,10 +50,10 @@ func TestNodeMetadata(t *testing.T) {
func TestMetadataRowCopy(t *testing.T) {
var (
row = detailed.MetadataRow{
row = report.MetadataRow{
ID: "id",
Value: "value",
Prime: true,
Priority: 1,
Datatype: "datatype",
}
cp = row.Copy()
@@ -67,9 +67,9 @@ func TestMetadataRowCopy(t *testing.T) {
// changing the copy should not change the original
cp.ID = ""
cp.Value = ""
cp.Prime = false
cp.Priority = 2
cp.Datatype = ""
if row.ID != "id" || row.Value != "value" || row.Prime != true || row.Datatype != "datatype" {
if row.ID != "id" || row.Value != "value" || row.Priority != 1 || row.Datatype != "datatype" {
t.Errorf("Expected changing the copy not to modify the original")
}
}
+5 -162
View File
@@ -1,177 +1,20 @@
package detailed
import (
"math"
"github.com/ugorji/go/codec"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
)
const (
defaultFormat = ""
filesizeFormat = "filesize"
integerFormat = "integer"
percentFormat = "percent"
)
var (
processNodeMetrics = []MetricRow{
{ID: process.CPUUsage, Format: percentFormat},
{ID: process.MemoryUsage, Format: filesizeFormat},
{ID: process.OpenFilesCount, Format: integerFormat},
}
containerNodeMetrics = []MetricRow{
{ID: docker.CPUTotalUsage, Format: percentFormat},
{ID: docker.MemoryUsage, Format: filesizeFormat},
}
hostNodeMetrics = []MetricRow{
{ID: host.CPUUsage, Format: percentFormat},
{ID: host.MemoryUsage, Format: filesizeFormat},
{ID: host.Load1, Format: defaultFormat, Group: "load"},
}
)
// MetricRow is a tuple of data used to render a metric as a sparkline and
// accoutrements.
type MetricRow struct {
ID string
Format string
Group string
Value float64
Metric *report.Metric
}
// Summary returns a copy of the MetricRow, without the samples, just the value if there is one.
func (m MetricRow) Summary() MetricRow {
row := MetricRow{
ID: m.ID,
Format: m.Format,
Group: m.Group,
Value: m.Value,
}
if m.Metric != nil {
var metric = m.Metric.Copy()
metric.Samples = nil
row.Metric = &metric
}
return row
}
// Copy returns a value copy of the MetricRow
func (m MetricRow) Copy() MetricRow {
row := MetricRow{
ID: m.ID,
Format: m.Format,
Group: m.Group,
Value: m.Value,
}
if m.Metric != nil {
var metric = m.Metric.Copy()
row.Metric = &metric
}
return row
}
// MarshalJSON shouldn't be used, use CodecEncodeSelf instead
func (MetricRow) MarshalJSON() ([]byte, error) {
panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead")
}
// UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead
func (*MetricRow) UnmarshalJSON(b []byte) error {
panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead")
}
type wiredMetricRow struct {
ID string `json:"id"`
Label string `json:"label"`
Format string `json:"format,omitempty"`
Group string `json:"group,omitempty"`
Value float64 `json:"value"`
Samples []report.Sample `json:"samples"`
Min float64 `json:"min"`
Max float64 `json:"max"`
First string `json:"first,omitempty"`
Last string `json:"last,omitempty"`
}
// CodecEncodeSelf marshals this MetricRow. It takes the basic Metric
// rendering, then adds some row-specific fields.
func (m *MetricRow) CodecEncodeSelf(encoder *codec.Encoder) {
in := m.Metric.ToIntermediate()
encoder.Encode(wiredMetricRow{
ID: m.ID,
Label: Label(m.ID),
Format: m.Format,
Group: m.Group,
Value: m.Value,
Samples: in.Samples,
Min: in.Min,
Max: in.Max,
First: in.First,
Last: in.Last,
})
}
// CodecDecodeSelf implements codec.Selfer
func (m *MetricRow) CodecDecodeSelf(decoder *codec.Decoder) {
var in wiredMetricRow
decoder.Decode(&in)
w := report.WireMetrics{
Samples: in.Samples,
Min: in.Min,
Max: in.Max,
First: in.First,
Last: in.Last,
}
metric := w.FromIntermediate()
*m = MetricRow{
ID: in.ID,
Format: in.Format,
Group: in.Group,
Value: in.Value,
Metric: &metric,
}
}
// NodeMetrics produces a table (to be consumed directly by the UI) based on
// an a report.Node, which is (hopefully) a node in one of our topologies.
func NodeMetrics(n report.Node) []MetricRow {
func NodeMetrics(r report.Report, n report.Node) []report.MetricRow {
if _, ok := n.Counters.Lookup(n.Topology); ok {
// This is a group of nodes, so no metrics!
return nil
}
renderers := map[string][]MetricRow{
report.Process: processNodeMetrics,
report.Container: containerNodeMetrics,
report.Host: hostNodeMetrics,
topology, ok := r.Topology(n.Topology)
if !ok {
return nil
}
if templates, ok := renderers[n.Topology]; ok {
rows := []MetricRow{}
for _, template := range templates {
metric, ok := n.Metrics[template.ID]
if !ok {
continue
}
t := template.Copy()
if s := metric.LastSample(); s != nil {
t.Value = toFixed(s.Value, 2)
}
t.Metric = &metric
rows = append(rows, t)
}
return rows
}
return nil
}
// toFixed truncates decimals of float64 down to specified precision
func toFixed(num float64, precision int) float64 {
output := math.Pow(10, float64(precision))
return float64(int64(num*output)) / output
return topology.MetricTemplates.MetricRows(n)
}
+66 -49
View File
@@ -18,71 +18,85 @@ func TestNodeMetrics(t *testing.T) {
inputs := []struct {
name string
node report.Node
want []detailed.MetricRow
want []report.MetricRow
}{
{
name: "process",
node: fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
want: []detailed.MetricRow{
want: []report.MetricRow{
{
ID: process.CPUUsage,
Format: "percent",
Group: "",
Value: 0.01,
Metric: &fixture.ClientProcess1CPUMetric,
ID: process.CPUUsage,
Label: "CPU",
Format: "percent",
Group: "",
Value: 0.01,
Priority: 1,
Metric: &fixture.ClientProcess1CPUMetric,
},
{
ID: process.MemoryUsage,
Format: "filesize",
Group: "",
Value: 0.02,
Metric: &fixture.ClientProcess1MemoryMetric,
ID: process.MemoryUsage,
Label: "Memory",
Format: "filesize",
Group: "",
Value: 0.02,
Priority: 2,
Metric: &fixture.ClientProcess1MemoryMetric,
},
},
},
{
name: "container",
node: fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
want: []detailed.MetricRow{
want: []report.MetricRow{
{
ID: docker.CPUTotalUsage,
Format: "percent",
Group: "",
Value: 0.03,
Metric: &fixture.ClientContainerCPUMetric,
ID: docker.CPUTotalUsage,
Label: "CPU",
Format: "percent",
Group: "",
Value: 0.03,
Priority: 1,
Metric: &fixture.ClientContainerCPUMetric,
},
{
ID: docker.MemoryUsage,
Format: "filesize",
Group: "",
Value: 0.04,
Metric: &fixture.ClientContainerMemoryMetric,
ID: docker.MemoryUsage,
Label: "Memory",
Format: "filesize",
Group: "",
Value: 0.04,
Priority: 2,
Metric: &fixture.ClientContainerMemoryMetric,
},
},
},
{
name: "host",
node: fixture.Report.Host.Nodes[fixture.ClientHostNodeID],
want: []detailed.MetricRow{
want: []report.MetricRow{
{
ID: host.CPUUsage,
Format: "percent",
Group: "",
Value: 0.07,
Metric: &fixture.ClientHostCPUMetric,
ID: host.CPUUsage,
Label: "CPU",
Format: "percent",
Group: "",
Value: 0.07,
Priority: 1,
Metric: &fixture.ClientHostCPUMetric,
},
{
ID: host.MemoryUsage,
Format: "filesize",
Group: "",
Value: 0.08,
Metric: &fixture.ClientHostMemoryMetric,
ID: host.MemoryUsage,
Label: "Memory",
Format: "filesize",
Group: "",
Value: 0.08,
Priority: 2,
Metric: &fixture.ClientHostMemoryMetric,
},
{
ID: host.Load1,
Group: "load",
Value: 0.09,
Metric: &fixture.ClientHostLoad1Metric,
ID: host.Load1,
Label: "Load (1m)",
Group: "load",
Value: 0.09,
Priority: 11,
Metric: &fixture.ClientHostLoad1Metric,
},
},
},
@@ -93,7 +107,7 @@ func TestNodeMetrics(t *testing.T) {
},
}
for _, input := range inputs {
have := detailed.NodeMetrics(input.node)
have := detailed.NodeMetrics(fixture.Report, input.node)
if !reflect.DeepEqual(input.want, have) {
t.Errorf("%s: %s", input.name, test.Diff(input.want, have))
}
@@ -104,19 +118,16 @@ func TestMetricRowSummary(t *testing.T) {
var (
now = time.Now()
metric = report.MakeMetric().Add(now, 1.234)
row = detailed.MetricRow{
ID: "id",
Format: "format",
Group: "group",
Value: 1.234,
Metric: &metric,
row = report.MetricRow{
ID: "id",
Format: "format",
Group: "group",
Value: 1.234,
Priority: 1,
Metric: &metric,
}
summary = row.Summary()
)
// summary should have all the same fields
if row.ID != summary.ID || row.Format != summary.Format || row.Group != summary.Group || row.Value != summary.Value {
t.Errorf("Expected summary to have same fields as original: %#v, but had %#v", row, summary)
}
// summary should not have any samples
if summary.Metric.Len() != 0 {
t.Errorf("Expected summary to have no samples, but had %d", summary.Metric.Len())
@@ -125,4 +136,10 @@ func TestMetricRowSummary(t *testing.T) {
if metric.Len() != 1 {
t.Errorf("Expected original metric to still have it's samples, but had %d", metric.Len())
}
// summary should have all the same fields (minus the metric)
summary.Metric = nil
row.Metric = nil
if !reflect.DeepEqual(summary, row) {
t.Errorf("Expected summary to have same fields as original: %s", test.Diff(summary, row))
}
}
+7 -8
View File
@@ -47,7 +47,7 @@ type wiredControlInstance struct {
Icon string `json:"icon"`
}
// CodecEncodeSelf marshals this MetricRow. It takes the basic Metric
// CodecEncodeSelf marshals this ControlInstance. It takes the basic Metric
// rendering, then adds some row-specific fields.
func (c *ControlInstance) CodecEncodeSelf(encoder *codec.Encoder) {
encoder.Encode(wiredControlInstance{
@@ -77,15 +77,15 @@ func (c *ControlInstance) CodecDecodeSelf(decoder *codec.Decoder) {
// MakeNode transforms a renderable node to a detailed node. It uses
// aggregate metadata, plus the set of origin node IDs, to produce tables.
func MakeNode(topologyID string, r report.Report, ns report.Nodes, n report.Node) Node {
summary, _ := MakeNodeSummary(n)
summary, _ := MakeNodeSummary(r, n)
return Node{
NodeSummary: summary,
Controls: controls(r, n),
Children: children(n),
Children: children(r, n),
Parents: Parents(r, n),
Connections: []ConnectionsSummary{
incomingConnectionsSummary(topologyID, n, ns),
outgoingConnectionsSummary(topologyID, n, ns),
incomingConnectionsSummary(topologyID, r, n, ns),
outgoingConnectionsSummary(topologyID, r, n, ns),
},
}
}
@@ -114,7 +114,6 @@ func controlsFor(topology report.Topology, nodeID string) []ControlInstance {
}
func controls(r report.Report, n report.Node) []ControlInstance {
// TODO(paulbellamy): this ID will have been munged in rendering, so we should stop doing that, so that this matches up.
if t, ok := r.Topology(n.Topology); ok {
return controlsFor(t, n.ID)
}
@@ -178,13 +177,13 @@ var (
}
)
func children(n report.Node) []NodeSummaryGroup {
func children(r report.Report, n report.Node) []NodeSummaryGroup {
summaries := map[string][]NodeSummary{}
n.Children.ForEach(func(child report.Node) {
if child.ID == n.ID {
return
}
summary, ok := MakeNodeSummary(child)
summary, ok := MakeNodeSummary(r, child)
if !ok {
return
}
+54 -38
View File
@@ -16,7 +16,7 @@ import (
)
func child(t *testing.T, r render.Renderer, id string) detailed.NodeSummary {
s, ok := detailed.MakeNodeSummary(r.Render(fixture.Report)[id])
s, ok := detailed.MakeNodeSummary(fixture.Report, r.Render(fixture.Report)[id])
if !ok {
t.Fatalf("Expected node %s to be summarizable, but wasn't", id)
}
@@ -44,38 +44,50 @@ func TestMakeDetailedHostNode(t *testing.T) {
Shape: "circle",
Linkable: true,
Adjacency: report.MakeIDList(fixture.ServerHostNodeID),
Metadata: []detailed.MetadataRow{
Metadata: []report.MetadataRow{
{
ID: "host_name",
Value: "client.hostname.com",
ID: "host_name",
Label: "Hostname",
Value: "client.hostname.com",
Priority: 11,
},
{
ID: "os",
Value: "Linux",
ID: "os",
Label: "OS",
Value: "Linux",
Priority: 12,
},
{
ID: "local_networks",
Value: "10.10.10.0/24",
ID: "local_networks",
Label: "Local Networks",
Value: "10.10.10.0/24",
Priority: 13,
},
},
Metrics: []detailed.MetricRow{
Metrics: []report.MetricRow{
{
ID: host.CPUUsage,
Format: "percent",
Value: 0.07,
Metric: &fixture.ClientHostCPUMetric,
ID: host.CPUUsage,
Label: "CPU",
Format: "percent",
Value: 0.07,
Priority: 1,
Metric: &fixture.ClientHostCPUMetric,
},
{
ID: host.MemoryUsage,
Format: "filesize",
Value: 0.08,
Metric: &fixture.ClientHostMemoryMetric,
ID: host.MemoryUsage,
Label: "Memory",
Format: "filesize",
Value: 0.08,
Priority: 2,
Metric: &fixture.ClientHostMemoryMetric,
},
{
ID: host.Load1,
Group: "load",
Value: 0.09,
Metric: &fixture.ClientHostLoad1Metric,
ID: host.Load1,
Label: "Load (1m)",
Group: "load",
Value: 0.09,
Priority: 11,
Metric: &fixture.ClientHostLoad1Metric,
},
},
},
@@ -121,7 +133,7 @@ func TestMakeDetailedHostNode(t *testing.T) {
NodeID: fixture.ServerHostNodeID,
Label: "server",
Linkable: true,
Metadata: []detailed.MetadataRow{
Metadata: []report.MetadataRow{
{
ID: "port",
Value: "80",
@@ -162,29 +174,33 @@ func TestMakeDetailedContainerNode(t *testing.T) {
Shape: "hexagon",
Linkable: true,
Pseudo: false,
Metadata: []detailed.MetadataRow{
{ID: "docker_container_id", Value: fixture.ServerContainerID, Prime: true},
{ID: "docker_container_state_human", Value: "running", Prime: true},
{ID: "docker_image_id", Value: fixture.ServerContainerImageID},
Metadata: []report.MetadataRow{
{ID: "docker_container_id", Label: "ID", Value: fixture.ServerContainerID, Priority: 1},
{ID: "docker_container_state_human", Label: "State", Value: "running", Priority: 2},
{ID: "docker_image_id", Label: "Image ID", Value: fixture.ServerContainerImageID, Priority: 11},
},
DockerLabels: []detailed.MetadataRow{
DockerLabels: []report.MetadataRow{
{ID: "label_" + detailed.AmazonECSContainerNameLabel, Value: `server`},
{ID: "label_foo1", Value: `bar1`},
{ID: "label_foo2", Value: `bar2`},
{ID: "label_io.kubernetes.pod.name", Value: "ping/pong-b"},
},
Metrics: []detailed.MetricRow{
Metrics: []report.MetricRow{
{
ID: docker.CPUTotalUsage,
Format: "percent",
Value: 0.05,
Metric: &fixture.ServerContainerCPUMetric,
ID: docker.CPUTotalUsage,
Label: "CPU",
Format: "percent",
Value: 0.05,
Priority: 1,
Metric: &fixture.ServerContainerCPUMetric,
},
{
ID: docker.MemoryUsage,
Format: "filesize",
Value: 0.06,
Metric: &fixture.ServerContainerMemoryMetric,
ID: docker.MemoryUsage,
Label: "Memory",
Format: "filesize",
Value: 0.06,
Priority: 2,
Metric: &fixture.ServerContainerMemoryMetric,
},
},
},
@@ -221,7 +237,7 @@ func TestMakeDetailedContainerNode(t *testing.T) {
NodeID: fixture.ClientContainerNodeID,
Label: "client",
Linkable: true,
Metadata: []detailed.MetadataRow{
Metadata: []report.MetadataRow{
{
ID: "port",
Value: "80",
@@ -239,7 +255,7 @@ func TestMakeDetailedContainerNode(t *testing.T) {
NodeID: render.IncomingInternetID,
Label: render.InboundMajor,
Linkable: true,
Metadata: []detailed.MetadataRow{
Metadata: []report.MetadataRow{
{
ID: "port",
Value: "80",
+19 -19
View File
@@ -68,22 +68,22 @@ func MakeColumn(id string) Column {
// NodeSummary is summary information about a child for a Node.
type NodeSummary struct {
ID string `json:"id"`
Label string `json:"label"`
LabelMinor string `json:"label_minor"`
Rank string `json:"rank"`
Shape string `json:"shape,omitempty"`
Stack bool `json:"stack,omitempty"`
Linkable bool `json:"linkable,omitempty"` // Whether this node can be linked-to
Pseudo bool `json:"pseudo,omitempty"`
Metadata []MetadataRow `json:"metadata,omitempty"`
DockerLabels []MetadataRow `json:"docker_labels,omitempty"`
Metrics []MetricRow `json:"metrics,omitempty"`
Adjacency report.IDList `json:"adjacency,omitempty"`
ID string `json:"id"`
Label string `json:"label"`
LabelMinor string `json:"label_minor"`
Rank string `json:"rank"`
Shape string `json:"shape,omitempty"`
Stack bool `json:"stack,omitempty"`
Linkable bool `json:"linkable,omitempty"` // Whether this node can be linked-to
Pseudo bool `json:"pseudo,omitempty"`
Metadata []report.MetadataRow `json:"metadata,omitempty"`
DockerLabels []report.MetadataRow `json:"docker_labels,omitempty"`
Metrics []report.MetricRow `json:"metrics,omitempty"`
Adjacency report.IDList `json:"adjacency,omitempty"`
}
// MakeNodeSummary summarizes a node, if possible.
func MakeNodeSummary(n report.Node) (NodeSummary, bool) {
func MakeNodeSummary(r report.Report, n report.Node) (NodeSummary, bool) {
renderers := map[string]func(NodeSummary, report.Node) (NodeSummary, bool){
render.Pseudo: pseudoNodeSummary,
report.Process: processNodeSummary,
@@ -94,7 +94,7 @@ func MakeNodeSummary(n report.Node) (NodeSummary, bool) {
report.Host: hostNodeSummary,
}
if renderer, ok := renderers[n.Topology]; ok {
return renderer(baseNodeSummary(n), n)
return renderer(baseNodeSummary(r, n), n)
}
return NodeSummary{}, false
}
@@ -133,14 +133,14 @@ func (n NodeSummary) Copy() NodeSummary {
return result
}
func baseNodeSummary(n report.Node) NodeSummary {
func baseNodeSummary(r report.Report, n report.Node) NodeSummary {
return NodeSummary{
ID: n.ID,
Shape: Circle,
Linkable: true,
Metadata: NodeMetadata(n),
Metadata: NodeMetadata(r, n),
DockerLabels: NodeDockerLabels(n),
Metrics: NodeMetrics(n),
Metrics: NodeMetrics(r, n),
Adjacency: n.Adjacency.Copy(),
}
}
@@ -341,10 +341,10 @@ func (s nodeSummariesByID) Less(i, j int) bool { return s[i].ID < s[j].ID }
type NodeSummaries map[string]NodeSummary
// Summaries converts RenderableNodes into a set of NodeSummaries
func Summaries(rns report.Nodes) NodeSummaries {
func Summaries(r report.Report, rns report.Nodes) NodeSummaries {
result := NodeSummaries{}
for id, node := range rns {
if summary, ok := MakeNodeSummary(node); ok {
if summary, ok := MakeNodeSummary(r, node); ok {
for i, m := range summary.Metrics {
summary.Metrics[i] = m.Summary()
}
+19 -20
View File
@@ -21,7 +21,7 @@ import (
func TestSummaries(t *testing.T) {
{
// Just a convenient source of some rendered nodes
have := detailed.Summaries(render.ProcessRenderer.Render(fixture.Report))
have := detailed.Summaries(fixture.Report, render.ProcessRenderer.Render(fixture.Report))
// The ids of the processes rendered above
expectedIDs := []string{
fixture.ClientProcess1NodeID,
@@ -53,14 +53,14 @@ func TestSummaries(t *testing.T) {
input := fixture.Report.Copy()
input.Process.Nodes[fixture.ClientProcess1NodeID] = input.Process.Nodes[fixture.ClientProcess1NodeID].WithMetrics(report.Metrics{process.CPUUsage: metric})
have := detailed.Summaries(render.ProcessRenderer.Render(input))
have := detailed.Summaries(input, render.ProcessRenderer.Render(input))
node, ok := have[fixture.ClientProcess1NodeID]
if !ok {
t.Fatalf("Expected output to have the node we added the metric to")
}
var row detailed.MetricRow
var row report.MetricRow
ok = false
for _, metric := range node.Metrics {
if metric.ID == process.CPUUsage {
@@ -74,10 +74,12 @@ func TestSummaries(t *testing.T) {
}
// Our summarized MetricRow
want := detailed.MetricRow{
ID: process.CPUUsage,
Format: "percent",
Value: 2,
want := report.MetricRow{
ID: process.CPUUsage,
Label: "CPU",
Format: "percent",
Value: 2,
Priority: 1,
Metric: &report.Metric{
Samples: nil,
Min: metric.Min,
@@ -109,10 +111,9 @@ func TestMakeNodeSummary(t *testing.T) {
LabelMinor: "client.hostname.com (10001)",
Rank: fixture.Client1Name,
Shape: "square",
Metadata: []detailed.MetadataRow{
{ID: process.PID, Value: fixture.Client1PID, Prime: true, Datatype: "number"},
Metadata: []report.MetadataRow{
{ID: process.PID, Label: "PID", Value: fixture.Client1PID, Priority: 1, Datatype: "number"},
},
Metrics: []detailed.MetricRow{},
Adjacency: report.MakeIDList(fixture.ServerProcessNodeID),
},
},
@@ -127,10 +128,9 @@ func TestMakeNodeSummary(t *testing.T) {
Rank: fixture.ClientContainerImageName,
Shape: "hexagon",
Linkable: true,
Metadata: []detailed.MetadataRow{
{ID: docker.ContainerID, Value: fixture.ClientContainerID, Prime: true},
Metadata: []report.MetadataRow{
{ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 1},
},
Metrics: []detailed.MetricRow{},
Adjacency: report.MakeIDList(fixture.ServerContainerNodeID),
},
},
@@ -146,9 +146,9 @@ func TestMakeNodeSummary(t *testing.T) {
Shape: "hexagon",
Linkable: true,
Stack: true,
Metadata: []detailed.MetadataRow{
{ID: docker.ImageID, Value: fixture.ClientContainerImageID, Prime: true},
{ID: report.Container, Value: "1", Prime: true, Datatype: "number"},
Metadata: []report.MetadataRow{
{ID: docker.ImageID, Label: "Image ID", Value: fixture.ClientContainerImageID, Priority: 1},
{ID: report.Container, Label: "# Containers", Value: "1", Priority: 2, Datatype: "number"},
},
Adjacency: report.MakeIDList(fixture.ServerContainerImageNodeID),
},
@@ -164,16 +164,15 @@ func TestMakeNodeSummary(t *testing.T) {
Rank: "hostname.com",
Shape: "circle",
Linkable: true,
Metadata: []detailed.MetadataRow{
{ID: host.HostName, Value: fixture.ClientHostName, Prime: false},
Metadata: []report.MetadataRow{
{ID: host.HostName, Label: "Hostname", Value: fixture.ClientHostName, Priority: 11},
},
Metrics: []detailed.MetricRow{},
Adjacency: report.MakeIDList(fixture.ServerHostNodeID),
},
},
}
for _, testcase := range testcases {
have, ok := detailed.MakeNodeSummary(testcase.input)
have, ok := detailed.MakeNodeSummary(fixture.Report, testcase.input)
if ok != testcase.ok {
t.Errorf("%s: MakeNodeSummary failed: expected ok value to be: %v", testcase.name, testcase.ok)
continue
+4 -6
View File
@@ -26,8 +26,6 @@ const (
InboundMinor = "Inbound connections"
OutboundMinor = "Outbound connections"
ipsKey = "ips"
// Topology for pseudo-nodes and IPs so we can differentiate them at the end
Pseudo = "pseudo"
IP = "IP"
@@ -46,7 +44,7 @@ func NewDerivedNode(id string, node report.Node) report.Node {
// NewDerivedPseudoNode makes a new pseudo node with the node as a child
func NewDerivedPseudoNode(id string, node report.Node) report.Node {
return node.WithID(id).WithTopology(Pseudo).WithChildren(report.MakeNodeSet(node)).PruneParents()
return NewDerivedNode(id, node).WithTopology(Pseudo)
}
func theInternetNode(m report.Node) report.Node {
@@ -110,7 +108,7 @@ func MapContainer2IP(m report.Node, _ report.Networks) report.Nodes {
result[id] = NewDerivedNode(id, m).
WithTopology(IP).
WithLatests(map[string]string{docker.ContainerID: containerID}).
WithCounters(map[string]int{ipsKey: 1})
WithCounters(map[string]int{IP: 1})
}
}
@@ -125,7 +123,7 @@ func MapContainer2IP(m report.Node, _ report.Networks) report.Nodes {
result[id] = NewDerivedNode(id, m).
WithTopology(IP).
WithLatests(map[string]string{docker.ContainerID: containerID}).
WithCounters(map[string]int{ipsKey: 1})
WithCounters(map[string]int{IP: 1})
}
}
@@ -139,7 +137,7 @@ func MapContainer2IP(m report.Node, _ report.Networks) report.Nodes {
func MapIP2Container(n report.Node, _ report.Networks) report.Nodes {
// If an IP is shared between multiple containers, we can't
// reliably attribute an connection based on its IP
if count, _ := n.Counters.Lookup(ipsKey); count > 1 {
if count, _ := n.Counters.Lookup(IP); count > 1 {
return report.Nodes{}
}