mirror of
https://github.com/weaveworks/scope.git
synced 2026-02-14 18:09:59 +00:00
297 lines
9.1 KiB
Go
297 lines
9.1 KiB
Go
package report
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/weaveworks/common/mtime"
|
|
)
|
|
|
|
// Table types
|
|
const (
|
|
MulticolumnTableType = "multicolumn-table"
|
|
PropertyListType = "property-list"
|
|
)
|
|
|
|
const (
|
|
tableEntryKeySeparator = "___"
|
|
truncationCountPrefix = "table_truncation_count_"
|
|
)
|
|
|
|
// AddPrefixMulticolumnTable appends arbitrary rows to the Node, returning a new node.
|
|
func (node Node) AddPrefixMulticolumnTable(prefix string, rows []Row) Node {
|
|
now := mtime.Now()
|
|
for _, row := range rows {
|
|
// Add all the row values as separate entries
|
|
for columnID, value := range row.Entries {
|
|
key := strings.Join([]string{row.ID, columnID}, tableEntryKeySeparator)
|
|
node = node.WithLatest(prefix+key, now, value)
|
|
}
|
|
}
|
|
return node
|
|
}
|
|
|
|
// AddPrefixPropertyList appends arbitrary key-value pairs to the Node, returning a new node.
|
|
func (node Node) AddPrefixPropertyList(prefix string, propertyList map[string]string) Node {
|
|
now := mtime.Now()
|
|
for label, value := range propertyList {
|
|
node = node.WithLatest(prefix+label, now, value)
|
|
}
|
|
return node
|
|
}
|
|
|
|
// WithoutPrefix returns the string with trimmed prefix and a
|
|
// boolean information of whether that prefix was really there.
|
|
// NOTE: Consider moving this function to utilities.
|
|
func WithoutPrefix(s string, prefix string) (string, bool) {
|
|
return strings.TrimPrefix(s, prefix), len(prefix) > 0 && strings.HasPrefix(s, prefix)
|
|
}
|
|
|
|
// ExtractMulticolumnTable returns the rows to build a multicolumn table from this node
|
|
func (node Node) ExtractMulticolumnTable(template TableTemplate) (rows []Row) {
|
|
rowsMapByID := map[string]Row{}
|
|
|
|
// Itearate through the whole of our map to extract all the values with the key
|
|
// with the given prefix. Since multicolumn tables don't support fixed rows (yet),
|
|
// all the table values will be stored under the table prefix.
|
|
// NOTE: It would be nice to optimize this part by only iterating through the keys
|
|
// with the given prefix. If it is possible to traverse the keys in the Latest map
|
|
// in a sorted order, then having LowerBoundEntry(key) and UpperBoundEntry(key)
|
|
// methods should be enough to implement ForEachWithPrefix(prefix) straightforwardly.
|
|
node.Latest.ForEach(func(key string, _ time.Time, value string) {
|
|
if keyWithoutPrefix, ok := WithoutPrefix(key, template.Prefix); ok {
|
|
ids := strings.Split(keyWithoutPrefix, tableEntryKeySeparator)
|
|
rowID, columnID := ids[0], ids[1]
|
|
// If the row with the given ID doesn't yet exist, we create an empty one.
|
|
if _, ok := rowsMapByID[rowID]; !ok {
|
|
rowsMapByID[rowID] = Row{
|
|
ID: rowID,
|
|
Entries: map[string]string{},
|
|
}
|
|
}
|
|
// At this point, the row with that ID always exists, so we just update the value.
|
|
rowsMapByID[rowID].Entries[columnID] = value
|
|
}
|
|
})
|
|
|
|
// Gather a list of rows.
|
|
rows = make([]Row, 0, len(rowsMapByID))
|
|
for _, row := range rowsMapByID {
|
|
rows = append(rows, row)
|
|
}
|
|
|
|
// Return the rows sorted by ID.
|
|
sort.Sort(rowsByID(rows))
|
|
return rows
|
|
}
|
|
|
|
// ExtractPropertyList returns the rows to build a property list from this node
|
|
func (node Node) ExtractPropertyList(template TableTemplate) (rows []Row) {
|
|
valuesMapByLabel := map[string]string{}
|
|
|
|
// Itearate through the whole of our map to extract all the values with the key
|
|
// with the given prefix as well as the keys corresponding to the fixed table rows.
|
|
node.Latest.ForEach(func(key string, _ time.Time, value string) {
|
|
if label, ok := template.FixedRows[key]; ok {
|
|
valuesMapByLabel[label] = value
|
|
} else if label, ok := WithoutPrefix(key, template.Prefix); ok {
|
|
valuesMapByLabel[label] = value
|
|
}
|
|
})
|
|
|
|
// Gather a label-value formatted list of rows.
|
|
rows = make([]Row, 0, len(valuesMapByLabel))
|
|
for label, value := range valuesMapByLabel {
|
|
rows = append(rows, Row{
|
|
ID: "label_" + label,
|
|
Entries: map[string]string{
|
|
"label": label,
|
|
"value": value,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Return the rows sorted by ID.
|
|
sort.Sort(rowsByID(rows))
|
|
return rows
|
|
}
|
|
|
|
// ExtractTable returns the rows to build either a property list or a
|
|
// generic table from this node. It also returns the number of rows,
|
|
// if any, that were truncated. The probes used to limit the number of
|
|
// labels, env vars and Weave Net connections they report, but this
|
|
// logic has since been removed. So the code here dealing with
|
|
// truncation is only retained in order to handle legacy reports.
|
|
func (node Node) ExtractTable(template TableTemplate) (rows []Row, truncationCount int) {
|
|
switch template.Type {
|
|
case MulticolumnTableType:
|
|
rows = node.ExtractMulticolumnTable(template)
|
|
default: // By default assume it's a property list (for backward compatibility).
|
|
rows = node.ExtractPropertyList(template)
|
|
}
|
|
|
|
truncationCount = 0
|
|
if str, ok := node.Latest.Lookup(truncationCountPrefix + template.Prefix); ok {
|
|
if n, err := fmt.Sscanf(str, "%d", &truncationCount); n != 1 || err != nil {
|
|
log.Warnf("Unexpected truncation count format %q", str)
|
|
}
|
|
}
|
|
|
|
return rows, truncationCount
|
|
}
|
|
|
|
// Column is the type for multi-column tables in the UI.
|
|
type Column struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
DataType string `json:"dataType"`
|
|
}
|
|
|
|
// Row is the type that holds the table data for the UI. Entries map from column ID to cell value.
|
|
type Row struct {
|
|
ID string `json:"id"`
|
|
Entries map[string]string `json:"entries"`
|
|
}
|
|
|
|
type rowsByID []Row
|
|
|
|
func (t rowsByID) Len() int { return len(t) }
|
|
func (t rowsByID) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
|
func (t rowsByID) Less(i, j int) bool { return t[i].ID < t[j].ID }
|
|
|
|
// Table is the type for a table in the UI.
|
|
type Table struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
Type string `json:"type"`
|
|
Columns []Column `json:"columns"`
|
|
Rows []Row `json:"rows"`
|
|
TruncationCount int `json:"truncationCount,omitempty"`
|
|
}
|
|
|
|
type tablesByID []Table
|
|
|
|
func (t tablesByID) Len() int { return len(t) }
|
|
func (t tablesByID) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
|
func (t tablesByID) Less(i, j int) bool { return t[i].ID < t[j].ID }
|
|
|
|
// TableTemplate describes how to render a table for the UI.
|
|
type TableTemplate struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
Prefix string `json:"prefix"`
|
|
Type string `json:"type"`
|
|
Columns []Column `json:"columns"`
|
|
// FixedRows indicates what predetermined rows to render each entry is
|
|
// indexed by the key to extract the row value is mapped to the row
|
|
// label
|
|
FixedRows map[string]string `json:"fixedRows"`
|
|
}
|
|
|
|
// Copy returns a value-copy of the TableTemplate
|
|
func (t TableTemplate) Copy() TableTemplate {
|
|
fixedRowsCopy := make(map[string]string, len(t.FixedRows))
|
|
for key, value := range t.FixedRows {
|
|
fixedRowsCopy[key] = value
|
|
}
|
|
t.FixedRows = fixedRowsCopy
|
|
return t
|
|
}
|
|
|
|
// Merge other into t, returning a fresh copy. Does fieldwise max -
|
|
// whilst this isn't particularly meaningful, at least it idempotent,
|
|
// commutativite and associative.
|
|
func (t TableTemplate) Merge(other TableTemplate) TableTemplate {
|
|
max := func(s1, s2 string) string {
|
|
if s1 > s2 {
|
|
return s1
|
|
}
|
|
return s2
|
|
}
|
|
|
|
// NOTE: Consider actually merging the columns and fixed rows.
|
|
fixedRows := t.FixedRows
|
|
if len(other.FixedRows) > len(fixedRows) {
|
|
fixedRows = other.FixedRows
|
|
}
|
|
columns := t.Columns
|
|
if len(other.Columns) > len(columns) {
|
|
columns = other.Columns
|
|
}
|
|
|
|
// TODO: Refactor the merging logic, as mixing the types now might result in
|
|
// invalid tables. Maybe we should return an error if the types are different?
|
|
return TableTemplate{
|
|
ID: max(t.ID, other.ID),
|
|
Label: max(t.Label, other.Label),
|
|
Prefix: max(t.Prefix, other.Prefix),
|
|
Type: max(t.Type, other.Type),
|
|
Columns: columns,
|
|
FixedRows: fixedRows,
|
|
}
|
|
}
|
|
|
|
// TableTemplates is a mergeable set of TableTemplate
|
|
type TableTemplates map[string]TableTemplate
|
|
|
|
// Tables renders the TableTemplates for a given node.
|
|
func (t TableTemplates) Tables(node Node) []Table {
|
|
if len(t) == 0 {
|
|
return nil
|
|
}
|
|
result := make([]Table, 0, len(t))
|
|
for _, template := range t {
|
|
rows, truncationCount := node.ExtractTable(template)
|
|
// Extract the type from the template; default to
|
|
// property list for backwards-compatibility.
|
|
tableType := template.Type
|
|
if tableType == "" {
|
|
tableType = PropertyListType
|
|
}
|
|
result = append(result, Table{
|
|
ID: template.ID,
|
|
Label: template.Label,
|
|
Columns: template.Columns,
|
|
Type: tableType,
|
|
Rows: rows,
|
|
TruncationCount: truncationCount,
|
|
})
|
|
}
|
|
sort.Sort(tablesByID(result))
|
|
return result
|
|
}
|
|
|
|
// Copy returns a value copy of the TableTemplates
|
|
func (t TableTemplates) Copy() TableTemplates {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
result := TableTemplates{}
|
|
for k, v := range t {
|
|
result[k] = v.Copy()
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Merge merges two sets of TableTemplates
|
|
func (t TableTemplates) Merge(other TableTemplates) TableTemplates {
|
|
if t == nil && other == nil {
|
|
return nil
|
|
}
|
|
if len(other) > len(t) {
|
|
t, other = other, t
|
|
}
|
|
result := t.Copy()
|
|
for k, v := range other {
|
|
if existing, ok := result[k]; ok {
|
|
result[k] = v.Merge(existing)
|
|
} else {
|
|
result[k] = v
|
|
}
|
|
}
|
|
return result
|
|
}
|