mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 18:20:27 +00:00
573 lines
16 KiB
Go
573 lines
16 KiB
Go
package render
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"github.com/weaveworks/scope/probe/docker"
|
|
"github.com/weaveworks/scope/probe/host"
|
|
"github.com/weaveworks/scope/probe/overlay"
|
|
"github.com/weaveworks/scope/probe/process"
|
|
"github.com/weaveworks/scope/report"
|
|
)
|
|
|
|
const (
|
|
containerImageRank = 4
|
|
containerRank = 3
|
|
processRank = 2
|
|
hostRank = 1
|
|
connectionsRank = 0 // keep connections at the bottom until they are expandable in the UI
|
|
)
|
|
|
|
// DetailedNode is the data type that's yielded to the JavaScript layer when
|
|
// we want deep information about an individual node.
|
|
type DetailedNode struct {
|
|
ID string `json:"id"`
|
|
LabelMajor string `json:"label_major"`
|
|
LabelMinor string `json:"label_minor,omitempty"`
|
|
Rank string `json:"rank,omitempty"`
|
|
Pseudo bool `json:"pseudo,omitempty"`
|
|
Tables []Table `json:"tables"`
|
|
Controls []ControlInstance `json:"controls"`
|
|
}
|
|
|
|
// Table is a dataset associated with a node. It will be displayed in the
|
|
// detail panel when a user clicks on a node.
|
|
type Table struct {
|
|
Title string `json:"title"` // e.g. Bandwidth
|
|
Numeric bool `json:"numeric"` // should the major column be right-aligned?
|
|
Rank int `json:"-"` // used to sort tables; not emitted.
|
|
Rows []Row `json:"rows"`
|
|
}
|
|
|
|
// Row is a single entry in a Table dataset.
|
|
type Row struct {
|
|
Key string `json:"key"` // e.g. Ingress
|
|
ValueMajor string `json:"value_major"` // e.g. 25
|
|
ValueMinor string `json:"value_minor,omitempty"` // e.g. KB/s
|
|
Expandable bool `json:"expandable,omitempty"` // Whether it can be expanded (hidden by default)
|
|
ValueType string `json:"value_type,omitempty"` // e.g. sparkline
|
|
Metric *report.Metric `json:"metric,omitempty"` // e.g. sparkline data samples
|
|
}
|
|
|
|
// ControlInstance contains a control description, and all the info
|
|
// needed to execute it.
|
|
type ControlInstance struct {
|
|
ProbeID string `json:"probeId"`
|
|
NodeID string `json:"nodeId"`
|
|
report.Control
|
|
}
|
|
|
|
type sortableRows []Row
|
|
|
|
func (r sortableRows) Len() int { return len(r) }
|
|
func (r sortableRows) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
|
func (r sortableRows) Less(i, j int) bool {
|
|
switch {
|
|
case r[i].Key != r[j].Key:
|
|
return r[i].Key < r[j].Key
|
|
|
|
case r[i].ValueMajor != r[j].ValueMajor:
|
|
return r[i].ValueMajor < r[j].ValueMajor
|
|
|
|
default:
|
|
return r[i].ValueMinor < r[j].ValueMinor
|
|
}
|
|
}
|
|
|
|
type sortableTables []Table
|
|
|
|
func (t sortableTables) Len() int { return len(t) }
|
|
func (t sortableTables) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
|
func (t sortableTables) Less(i, j int) bool { return t[i].Rank > t[j].Rank }
|
|
|
|
// MakeDetailedNode transforms a renderable node to a detailed node. It uses
|
|
// aggregate metadata, plus the set of origin node IDs, to produce tables.
|
|
func MakeDetailedNode(r report.Report, n RenderableNode) DetailedNode {
|
|
tables := sortableTables{}
|
|
|
|
// Figure out if multiple hosts/containers are referenced by the renderableNode
|
|
multiContainer, multiHost := getRenderingContext(r, n)
|
|
|
|
// RenderableNode may be the result of merge operation(s), and so may have
|
|
// multiple origins. The ultimate goal here is to generate tables to view
|
|
// in the UI, so we skip the intermediate representations, but we could
|
|
// add them later.
|
|
connections := []Row{}
|
|
for _, id := range n.Origins {
|
|
if table, ok := OriginTable(r, id, multiHost, multiContainer); ok {
|
|
tables = append(tables, table)
|
|
} else if _, ok := r.Endpoint.Nodes[id]; ok {
|
|
connections = append(connections, connectionDetailsRows(r.Endpoint, id)...)
|
|
} else if _, ok := r.Address.Nodes[id]; ok {
|
|
connections = append(connections, connectionDetailsRows(r.Address, id)...)
|
|
}
|
|
}
|
|
|
|
if table, ok := connectionsTable(connections, r, n); ok {
|
|
tables = append(tables, table)
|
|
}
|
|
|
|
// Sort tables by rank
|
|
sort.Sort(tables)
|
|
|
|
return DetailedNode{
|
|
ID: n.ID,
|
|
LabelMajor: n.LabelMajor,
|
|
LabelMinor: n.LabelMinor,
|
|
Rank: n.Rank,
|
|
Pseudo: n.Pseudo,
|
|
Tables: tables,
|
|
Controls: controls(r, n),
|
|
}
|
|
}
|
|
|
|
func getRenderingContext(r report.Report, n RenderableNode) (multiContainer, multiHost bool) {
|
|
var (
|
|
originHosts = map[string]struct{}{}
|
|
originContainers = map[string]struct{}{}
|
|
)
|
|
for _, id := range n.Origins {
|
|
for _, topology := range r.Topologies() {
|
|
if nmd, ok := topology.Nodes[id]; ok {
|
|
originHosts[report.ExtractHostID(nmd)] = struct{}{}
|
|
if id, ok := nmd.Metadata[docker.ContainerID]; ok {
|
|
originContainers[id] = struct{}{}
|
|
}
|
|
}
|
|
// Return early if possible
|
|
multiHost = len(originHosts) > 1
|
|
multiContainer = len(originContainers) > 1
|
|
if multiHost && multiContainer {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func connectionsTable(connections []Row, r report.Report, n RenderableNode) (Table, bool) {
|
|
sec := r.Window.Seconds()
|
|
rate := func(u *uint64) (float64, bool) {
|
|
if u == nil {
|
|
return 0.0, false
|
|
}
|
|
if sec <= 0 {
|
|
return 0.0, true
|
|
}
|
|
return float64(*u) / sec, true
|
|
}
|
|
shortenByteRate := func(rate float64) (major, minor string) {
|
|
switch {
|
|
case rate > 1024*1024:
|
|
return fmt.Sprintf("%.2f", rate/1024/1024), "MBps"
|
|
case rate > 1024:
|
|
return fmt.Sprintf("%.1f", rate/1024), "KBps"
|
|
default:
|
|
return fmt.Sprintf("%.0f", rate), "Bps"
|
|
}
|
|
}
|
|
|
|
rows := []Row{}
|
|
if n.EdgeMetadata.MaxConnCountTCP != nil {
|
|
rows = append(rows, Row{Key: "TCP connections", ValueMajor: strconv.FormatUint(*n.EdgeMetadata.MaxConnCountTCP, 10)})
|
|
}
|
|
if rate, ok := rate(n.EdgeMetadata.EgressPacketCount); ok {
|
|
rows = append(rows, Row{Key: "Egress packet rate", ValueMajor: fmt.Sprintf("%.0f", rate), ValueMinor: "packets/sec"})
|
|
}
|
|
if rate, ok := rate(n.EdgeMetadata.IngressPacketCount); ok {
|
|
rows = append(rows, Row{Key: "Ingress packet rate", ValueMajor: fmt.Sprintf("%.0f", rate), ValueMinor: "packets/sec"})
|
|
}
|
|
if rate, ok := rate(n.EdgeMetadata.EgressByteCount); ok {
|
|
s, unit := shortenByteRate(rate)
|
|
rows = append(rows, Row{Key: "Egress byte rate", ValueMajor: s, ValueMinor: unit})
|
|
}
|
|
if rate, ok := rate(n.EdgeMetadata.IngressByteCount); ok {
|
|
s, unit := shortenByteRate(rate)
|
|
rows = append(rows, Row{Key: "Ingress byte rate", ValueMajor: s, ValueMinor: unit})
|
|
}
|
|
if len(connections) > 0 {
|
|
sort.Sort(sortableRows(connections))
|
|
rows = append(rows, Row{Key: "Client", ValueMajor: "Server", Expandable: true})
|
|
rows = append(rows, connections...)
|
|
}
|
|
if len(rows) > 0 {
|
|
return Table{
|
|
Title: "Connections",
|
|
Numeric: false,
|
|
Rank: connectionsRank,
|
|
Rows: rows,
|
|
}, true
|
|
}
|
|
return Table{}, false
|
|
}
|
|
|
|
func controlsFor(topology report.Topology, nodeID string) []ControlInstance {
|
|
result := []ControlInstance{}
|
|
node, ok := topology.Nodes[nodeID]
|
|
if !ok {
|
|
return result
|
|
}
|
|
|
|
for _, id := range node.Controls.Controls {
|
|
if control, ok := topology.Controls[id]; ok {
|
|
result = append(result, ControlInstance{
|
|
ProbeID: node.Metadata[report.ProbeID],
|
|
NodeID: nodeID,
|
|
Control: control,
|
|
})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func controls(r report.Report, n RenderableNode) []ControlInstance {
|
|
if _, ok := r.Process.Nodes[n.ControlNode]; ok {
|
|
return controlsFor(r.Process, n.ControlNode)
|
|
} else if _, ok := r.Container.Nodes[n.ControlNode]; ok {
|
|
return controlsFor(r.Container, n.ControlNode)
|
|
} else if _, ok := r.ContainerImage.Nodes[n.ControlNode]; ok {
|
|
return controlsFor(r.ContainerImage, n.ControlNode)
|
|
} else if _, ok := r.Host.Nodes[n.ControlNode]; ok {
|
|
return controlsFor(r.Host, n.ControlNode)
|
|
}
|
|
return []ControlInstance{}
|
|
}
|
|
|
|
// OriginTable 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 OriginTable(r report.Report, originID string, addHostTags bool, addContainerTags bool) (Table, bool) {
|
|
result, show := Table{}, false
|
|
if nmd, ok := r.Process.Nodes[originID]; ok {
|
|
result, show = processOriginTable(nmd, addHostTags, addContainerTags)
|
|
}
|
|
if nmd, ok := r.Container.Nodes[originID]; ok {
|
|
result, show = containerOriginTable(nmd, addHostTags)
|
|
}
|
|
if nmd, ok := r.ContainerImage.Nodes[originID]; ok {
|
|
result, show = containerImageOriginTable(nmd)
|
|
}
|
|
if nmd, ok := r.Host.Nodes[originID]; ok {
|
|
result, show = hostOriginTable(nmd)
|
|
}
|
|
return result, show
|
|
}
|
|
|
|
func connectionDetailsRows(topology report.Topology, originID string) []Row {
|
|
rows := []Row{}
|
|
labeler := func(nodeID string, sets report.Sets) (string, bool) {
|
|
if _, addr, port, ok := report.ParseEndpointNodeID(nodeID); ok {
|
|
if names, ok := sets["name"]; ok {
|
|
return fmt.Sprintf("%s:%s", names[0], port), true
|
|
}
|
|
return fmt.Sprintf("%s:%s", addr, port), true
|
|
}
|
|
if _, addr, ok := report.ParseAddressNodeID(nodeID); ok {
|
|
return addr, true
|
|
}
|
|
return "", false
|
|
}
|
|
local, ok := labeler(originID, topology.Nodes[originID].Sets)
|
|
if !ok {
|
|
return rows
|
|
}
|
|
// Firstly, collection outgoing connections from this node.
|
|
for _, serverNodeID := range topology.Nodes[originID].Adjacency {
|
|
remote, ok := labeler(serverNodeID, topology.Nodes[serverNodeID].Sets)
|
|
if !ok {
|
|
continue
|
|
}
|
|
rows = append(rows, Row{
|
|
Key: local,
|
|
ValueMajor: remote,
|
|
Expandable: true,
|
|
})
|
|
}
|
|
// Next, scan the topology for incoming connections to this node.
|
|
for clientNodeID, clientNode := range topology.Nodes {
|
|
if clientNodeID == originID {
|
|
continue
|
|
}
|
|
serverNodeIDs := clientNode.Adjacency
|
|
if !serverNodeIDs.Contains(originID) {
|
|
continue
|
|
}
|
|
remote, ok := labeler(clientNodeID, clientNode.Sets)
|
|
if !ok {
|
|
continue
|
|
}
|
|
rows = append(rows, Row{
|
|
Key: remote,
|
|
ValueMajor: local,
|
|
ValueMinor: "",
|
|
Expandable: true,
|
|
})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func processOriginTable(nmd report.Node, addHostTag bool, addContainerTag bool) (Table, bool) {
|
|
rows := []Row{}
|
|
for _, tuple := range []struct{ key, human string }{
|
|
{process.PPID, "Parent PID"},
|
|
{process.Cmdline, "Command"},
|
|
{process.Threads, "# Threads"},
|
|
} {
|
|
if val, ok := nmd.Metadata[tuple.key]; ok {
|
|
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
|
|
}
|
|
}
|
|
|
|
if containerID, ok := nmd.Metadata[docker.ContainerID]; ok && addContainerTag {
|
|
rows = append([]Row{{Key: "Container ID", ValueMajor: containerID}}, rows...)
|
|
}
|
|
|
|
if addHostTag {
|
|
rows = append([]Row{{Key: "Host", ValueMajor: report.ExtractHostID(nmd)}}, rows...)
|
|
}
|
|
|
|
for _, tuple := range []struct {
|
|
key, human string
|
|
fmt formatter
|
|
}{
|
|
{process.CPUUsage, "CPU Usage", formatPercent},
|
|
{process.MemoryUsage, "Memory Usage", formatMemory},
|
|
} {
|
|
if val, ok := nmd.Metrics[tuple.key]; ok {
|
|
rows = append(rows, sparklineRow(tuple.human, val, tuple.fmt))
|
|
}
|
|
}
|
|
|
|
var (
|
|
title = "Process"
|
|
name, commFound = nmd.Metadata[process.Name]
|
|
pid, pidFound = nmd.Metadata[process.PID]
|
|
)
|
|
if commFound {
|
|
title += ` "` + name + `"`
|
|
}
|
|
if pidFound {
|
|
title += " (" + pid + ")"
|
|
}
|
|
return Table{
|
|
Title: title,
|
|
Numeric: false,
|
|
Rows: rows,
|
|
Rank: processRank,
|
|
}, len(rows) > 0 || commFound || pidFound
|
|
}
|
|
|
|
type formatter func(report.Metric) (report.Metric, string)
|
|
|
|
func sparklineRow(human string, metric report.Metric, format formatter) Row {
|
|
if format == nil {
|
|
format = formatDefault
|
|
}
|
|
metric, lastStr := format(metric)
|
|
return Row{Key: human, ValueMajor: lastStr, Metric: &metric, ValueType: "sparkline"}
|
|
}
|
|
|
|
func formatDefault(m report.Metric) (report.Metric, string) {
|
|
if s := m.LastSample(); s != nil {
|
|
return m, fmt.Sprintf("%0.2f", s.Value)
|
|
}
|
|
return m, ""
|
|
}
|
|
|
|
func memoryScale(n float64) (string, float64) {
|
|
brackets := []struct {
|
|
human string
|
|
shift uint
|
|
}{
|
|
{"bytes", 0},
|
|
{"KB", 10},
|
|
{"MB", 20},
|
|
{"GB", 30},
|
|
{"TB", 40},
|
|
{"PB", 50},
|
|
}
|
|
for _, bracket := range brackets {
|
|
unit := (1 << bracket.shift)
|
|
if n < float64(unit<<10) {
|
|
return bracket.human, float64(unit)
|
|
}
|
|
}
|
|
return "PB", float64(1 << 50)
|
|
}
|
|
|
|
func formatMemory(m report.Metric) (report.Metric, string) {
|
|
s := m.LastSample()
|
|
if s == nil {
|
|
return m, ""
|
|
}
|
|
human, divisor := memoryScale(s.Value)
|
|
return m.Div(divisor), fmt.Sprintf("%0.2f %s", s.Value/divisor, human)
|
|
}
|
|
|
|
func formatPercent(m report.Metric) (report.Metric, string) {
|
|
if s := m.LastSample(); s != nil {
|
|
return m, fmt.Sprintf("%0.2f%%", s.Value)
|
|
}
|
|
return m, ""
|
|
}
|
|
|
|
func containerOriginTable(nmd report.Node, addHostTag bool) (Table, bool) {
|
|
rows := []Row{}
|
|
for _, tuple := range []struct{ key, human string }{
|
|
{docker.ContainerState, "State"},
|
|
} {
|
|
if val, ok := nmd.Latest.Lookup(tuple.key); ok && val != "" {
|
|
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
|
|
}
|
|
}
|
|
|
|
for _, tuple := range []struct{ key, human string }{
|
|
{docker.ContainerID, "ID"},
|
|
{docker.ImageID, "Image ID"},
|
|
{docker.ContainerPorts, "Ports"},
|
|
{docker.ContainerCreated, "Created"},
|
|
{docker.ContainerCommand, "Command"},
|
|
{overlay.WeaveMACAddress, "Weave MAC"},
|
|
{overlay.WeaveDNSHostname, "Weave DNS Hostname"},
|
|
} {
|
|
if val, ok := nmd.Metadata[tuple.key]; ok && val != "" {
|
|
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
|
|
}
|
|
}
|
|
|
|
for _, ip := range docker.ExtractContainerIPs(nmd) {
|
|
rows = append(rows, Row{Key: "IP Address", ValueMajor: ip, ValueMinor: ""})
|
|
}
|
|
rows = append(rows, getDockerLabelRows(nmd)...)
|
|
|
|
if addHostTag {
|
|
rows = append([]Row{{Key: "Host", ValueMajor: report.ExtractHostID(nmd)}}, rows...)
|
|
}
|
|
|
|
if val, ok := nmd.Metrics[docker.MemoryUsage]; ok {
|
|
rows = append(rows, sparklineRow("Memory Usage", val, formatMemory))
|
|
}
|
|
if val, ok := nmd.Metrics[docker.CPUTotalUsage]; ok {
|
|
rows = append(rows, sparklineRow("CPU Usage", val, formatPercent))
|
|
}
|
|
|
|
var (
|
|
title = "Container"
|
|
name, nameFound = GetRenderableContainerName(nmd)
|
|
)
|
|
if nameFound {
|
|
title += ` "` + name + `"`
|
|
}
|
|
|
|
return Table{
|
|
Title: title,
|
|
Numeric: false,
|
|
Rows: rows,
|
|
Rank: containerRank,
|
|
}, len(rows) > 0 || nameFound
|
|
}
|
|
|
|
func containerImageOriginTable(nmd report.Node) (Table, bool) {
|
|
rows := []Row{}
|
|
for _, tuple := range []struct{ key, human string }{
|
|
{docker.ImageID, "Image ID"},
|
|
} {
|
|
if val, ok := nmd.Metadata[tuple.key]; ok {
|
|
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
|
|
}
|
|
}
|
|
rows = append(rows, getDockerLabelRows(nmd)...)
|
|
title := "Container Image"
|
|
var (
|
|
nameFound bool
|
|
name string
|
|
)
|
|
if name, nameFound = nmd.Metadata[docker.ImageName]; nameFound {
|
|
title += ` "` + name + `"`
|
|
}
|
|
return Table{
|
|
Title: title,
|
|
Numeric: false,
|
|
Rows: rows,
|
|
Rank: containerImageRank,
|
|
}, len(rows) > 0 || nameFound
|
|
}
|
|
|
|
func getDockerLabelRows(nmd report.Node) []Row {
|
|
rows := []Row{}
|
|
// Add labels in alphabetical order
|
|
labels := docker.ExtractLabels(nmd)
|
|
labelKeys := make([]string, 0, len(labels))
|
|
for k := range labels {
|
|
labelKeys = append(labelKeys, k)
|
|
}
|
|
sort.Strings(labelKeys)
|
|
for _, labelKey := range labelKeys {
|
|
rows = append(rows, Row{Key: fmt.Sprintf("Label %q", labelKey), ValueMajor: labels[labelKey]})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func hostOriginTable(nmd report.Node) (Table, bool) {
|
|
// Ensure that all metrics have the same max
|
|
maxLoad := 0.0
|
|
for _, key := range []string{host.Load1, host.Load5, host.Load15} {
|
|
if metric, ok := nmd.Metrics[key]; ok {
|
|
if metric.Len() == 0 {
|
|
continue
|
|
}
|
|
if metric.Max > maxLoad {
|
|
maxLoad = metric.Max
|
|
}
|
|
}
|
|
}
|
|
|
|
rows := []Row{}
|
|
for _, tuple := range []struct{ key, human string }{
|
|
{host.Load1, "Load (1m)"},
|
|
{host.Load5, "Load (5m)"},
|
|
{host.Load15, "Load (15m)"},
|
|
} {
|
|
if val, ok := nmd.Metrics[tuple.key]; ok {
|
|
val.Max = maxLoad
|
|
rows = append(rows, sparklineRow(tuple.human, val, nil))
|
|
}
|
|
}
|
|
for _, tuple := range []struct {
|
|
key, human string
|
|
fmt formatter
|
|
}{
|
|
{host.CPUUsage, "CPU Usage", formatPercent},
|
|
{host.MemUsage, "Memory Usage", formatMemory},
|
|
} {
|
|
if val, ok := nmd.Metrics[tuple.key]; ok {
|
|
rows = append(rows, sparklineRow(tuple.human, val, tuple.fmt))
|
|
}
|
|
}
|
|
for _, tuple := range []struct{ key, human string }{
|
|
{host.OS, "Operating system"},
|
|
{host.KernelVersion, "Kernel version"},
|
|
{host.Uptime, "Uptime"},
|
|
} {
|
|
if val, ok := nmd.Metadata[tuple.key]; ok {
|
|
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
|
|
}
|
|
}
|
|
|
|
title := "Host"
|
|
var (
|
|
name string
|
|
foundName bool
|
|
)
|
|
if name, foundName = nmd.Metadata[host.HostName]; foundName {
|
|
title += ` "` + name + `"`
|
|
}
|
|
return Table{
|
|
Title: title,
|
|
Numeric: false,
|
|
Rows: rows,
|
|
Rank: hostRank,
|
|
}, len(rows) > 0 || foundName
|
|
}
|