+
);
}
-
}
Sparkline.propTypes = {
@@ -104,8 +144,10 @@ Sparkline.defaultProps = {
width: 80,
height: 24,
strokeColor: '#7d7da8',
- strokeWidth: '0.5px',
+ strokeWidth: 0.5,
+ hoverColor: '#7d7da8',
curve: curveLinear,
- circleDiameter: 1.75,
+ circleRadius: 1.75,
+ hovered: false,
data: [],
};
diff --git a/client/app/scripts/selectors/node-metric.js b/client/app/scripts/selectors/node-metric.js
index 6ac3f5d44..4058534fd 100644
--- a/client/app/scripts/selectors/node-metric.js
+++ b/client/app/scripts/selectors/node-metric.js
@@ -23,6 +23,7 @@ export const availableMetricsSelector = createSelector(
return nodes
.valueSeq()
.flatMap(n => n.get('metrics', makeList()))
+ .filter(m => !m.get('valueEmpty'))
.map(m => makeMap({ id: m.get('id'), label: m.get('label') }))
.toSet()
.toList()
diff --git a/client/app/scripts/utils/color-utils.js b/client/app/scripts/utils/color-utils.js
index 676d7af04..2f9b08274 100644
--- a/client/app/scripts/utils/color-utils.js
+++ b/client/app/scripts/utils/color-utils.js
@@ -82,3 +82,13 @@ export function brightenColor(c) {
}
return color.toString();
}
+
+export function darkenColor(c) {
+ let color = hsl(c);
+ if (hsl.l < 0.5) {
+ color = color.darker(0.5);
+ } else {
+ color = color.darker(0.8);
+ }
+ return color.toString();
+}
diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js
index 4350bddb6..e14547d62 100644
--- a/client/app/scripts/utils/metric-utils.js
+++ b/client/app/scripts/utils/metric-utils.js
@@ -58,12 +58,12 @@ export function getMetricColor(metric) {
if (/mem/.test(metricId)) {
return 'steelBlue';
} else if (/cpu/.test(metricId)) {
- return colors('cpu');
+ return colors('cpu').toString();
} else if (/files/.test(metricId)) {
// purple
return '#9467bd';
} else if (/load/.test(metricId)) {
- return colors('load');
+ return colors('load').toString();
}
return 'steelBlue';
}
diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss
index 3cacf9200..860989829 100644
--- a/client/app/styles/_base.scss
+++ b/client/app/styles/_base.scss
@@ -12,6 +12,10 @@
// TODO: Remove this line once Service UI CONFIGURE button stops being added to Scope.
.scope-wrapper .setup-nav-button { display: none; }
+a {
+ text-decoration: none;
+}
+
.browsehappy {
margin: 0.2em 0;
background: #ccc;
@@ -928,18 +932,22 @@
width: 33%;
display: flex;
flex-direction: column;
+ flex-grow: 1;
&-label {
color: $text-secondary-color;
text-transform: uppercase;
font-size: 80%;
- margin-top: auto;
.fa {
margin-left: 0.5em;
}
}
+ &-sparkline {
+ margin-top: auto;
+ }
+
&-placeholder {
font-size: 200%;
opacity: 0.2;
@@ -948,10 +956,12 @@
}
&-link-item {
- @extend .btn-opacity;
+ @extend .palable;
cursor: pointer;
opacity: $link-opacity-default;
width: 33%;
+ display: flex;
+ color: inherit;
.label {
text-transform: uppercase;
@@ -1105,6 +1115,14 @@
text-align: right;
}
+ &-metric-link {
+ @extend .btn-opacity;
+ text-decoration: underline;
+ cursor: pointer;
+ opacity: $link-opacity-default;
+ color: $text-color;
+ }
+
&-value-scalar {
// width: 2em;
text-align: right;
diff --git a/client/package.json b/client/package.json
index 5d971a1c9..3245191ef 100644
--- a/client/package.json
+++ b/client/package.json
@@ -23,6 +23,7 @@
"dagre": "0.7.4",
"debug": "2.6.6",
"filesize": "3.5.9",
+ "filter-invalid-dom-props": "^2.0.0",
"font-awesome": "4.7.0",
"immutable": "3.8.1",
"lcp": "1.1.0",
diff --git a/client/yarn.lock b/client/yarn.lock
index 8518dc6ff..a4cc86908 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -2499,6 +2499,12 @@ fill-range@^2.1.0:
repeat-element "^1.1.2"
repeat-string "^1.5.2"
+filter-invalid-dom-props@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/filter-invalid-dom-props/-/filter-invalid-dom-props-2.0.0.tgz#527f1494cb3c4f282a73c43804153eb80c42dc2c"
+ dependencies:
+ html-attributes "1.1.0"
+
finalhandler@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.1.tgz#bcd15d1689c0e5ed729b6f7f541a6df984117db8"
@@ -2889,6 +2895,10 @@ hosted-git-info@^2.1.4:
version "2.3.1"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.3.1.tgz#ac439421605f0beb0ea1349de7d8bb28e50be1dd"
+html-attributes@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/html-attributes/-/html-attributes-1.1.0.tgz#82027a4fac7a6070ea6c18cc3886aea18d6dea09"
+
html-comment-regex@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
diff --git a/render/detailed/links.go b/render/detailed/links.go
index 5a078a545..acb2210f9 100644
--- a/render/detailed/links.go
+++ b/render/detailed/links.go
@@ -12,46 +12,70 @@ import (
"github.com/ugorji/go/codec"
)
-// MetricLink describes a URL referencing a metric.
-type MetricLink struct {
- // References the metric id
- ID string `json:"id"`
- Label string `json:"label"`
- URL string `json:"url"`
- Priority int `json:"priority"`
-}
-
-// Variable name for the query within the metrics graph url
+// Replacement variable name for the query in the metrics graph url
const urlQueryVarName = ":query"
var (
- // Available metric links
- linkTemplates = []MetricLink{
- {ID: docker.CPUTotalUsage, Label: "CPU", Priority: 1},
- {ID: docker.MemoryUsage, Label: "Memory", Priority: 2},
+ // Metadata for shown queries
+ shownQueries = []struct {
+ ID string
+ Label string
+ }{
+ {
+ ID: docker.CPUTotalUsage,
+ Label: "CPU",
+ },
+ {
+ ID: docker.MemoryUsage,
+ Label: "Memory",
+ },
+ {
+ ID: "receive_bytes",
+ Label: "Rx/s",
+ },
+ {
+ ID: "transmit_bytes",
+ Label: "Tx/s",
+ },
}
// Prometheus queries for topologies
+ //
+ // Metrics
+ // - `container_cpu_usage_seconds_total` --> cAdvisor in Kubelets.
+ // - `container_memory_usage_bytes` --> cAdvisor in Kubelets.
topologyQueries = map[string]map[string]*template.Template{
- report.Pod: {
- // `container_memory_usage_bytes` is provided by cAdvisor in Kubelets.
- docker.MemoryUsage: parsedTemplate(`sum(container_memory_usage_bytes{pod_name="{{.Label}}"})`),
- // `container_cpu_usage_seconds_total` is provided by cAdvisor in Kubelets.
+ // Containers
+
+ report.Container: {
+ docker.MemoryUsage: parsedTemplate(`sum(container_memory_usage_bytes{container_name="{{.Label}}"})`),
+ docker.CPUTotalUsage: parsedTemplate(`sum(rate(container_cpu_usage_seconds_total{container_name="{{.Label}}"}[1m]))`),
+ },
+ report.ContainerImage: {
+ docker.MemoryUsage: parsedTemplate(`sum(container_memory_usage_bytes{image="{{.Label}}"})`),
+ docker.CPUTotalUsage: parsedTemplate(`sum(rate(container_cpu_usage_seconds_total{image="{{.Label}}"}[1m]))`),
+ },
+ "group:container:docker_container_hostname": {
+ docker.MemoryUsage: parsedTemplate(`sum(container_memory_usage_bytes{pod_name="{{.Label}}"})`),
docker.CPUTotalUsage: parsedTemplate(`sum(rate(container_cpu_usage_seconds_total{pod_name="{{.Label}}"}[1m]))`),
},
- report.Deployment: {
- // `container_memory_usage_bytes` is provided by cAdvisor in Kubelets.
- // Pod names are automatically generated by k8s using the deployment name:
- // https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#pod-template-hash-label
- docker.MemoryUsage: parsedTemplate(`sum(container_memory_usage_bytes{pod_name=~"^{{.Label}}-[^-]+-[^-]+$"})`),
- // `container_cpu_usage_seconds_total` is provided by cAdvisor in Kubelets.
+
+ // Kubernetes topologies
+
+ report.Pod: {
+ docker.MemoryUsage: parsedTemplate(`sum(container_memory_usage_bytes{pod_name="{{.Label}}"})`),
+ docker.CPUTotalUsage: parsedTemplate(`sum(rate(container_cpu_usage_seconds_total{pod_name="{{.Label}}"}[1m]))`),
+ "receive_bytes": parsedTemplate(`sum(rate(container_network_receive_bytes_total{pod_name="{{.Label}}"}[5m]))`),
+ "transmit_bytes": parsedTemplate(`sum(rate(container_network_transmit_bytes_total{pod_name="{{.Label}}"}[5m]))`),
+ },
+ // Pod naming:
+ // https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#pod-template-hash-label
+ "__k8s_controllers": {
+ docker.MemoryUsage: parsedTemplate(`sum(container_memory_usage_bytes{pod_name=~"^{{.Label}}-[^-]+-[^-]+$"})`),
docker.CPUTotalUsage: parsedTemplate(`sum(rate(container_cpu_usage_seconds_total{pod_name=~"^{{.Label}}-[^-]+-[^-]+$"}[1m]))`),
},
report.DaemonSet: {
- // `container_memory_usage_bytes` is provided by cAdvisor in Kubelets.
- // Pod names are automatically generated by k8s using the DaemonSet name.
- docker.MemoryUsage: parsedTemplate(`sum(container_memory_usage_bytes{pod_name=~"^{{.Label}}-[^-]+$"})`),
- // `container_cpu_usage_seconds_total` is provided by cAdvisor in Kubelets.
+ docker.MemoryUsage: parsedTemplate(`sum(container_memory_usage_bytes{pod_name=~"^{{.Label}}-[^-]+$"})`),
docker.CPUTotalUsage: parsedTemplate(`sum(rate(container_cpu_usage_seconds_total{pod_name=~"^{{.Label}}-[^-]+$"}[1m]))`),
},
report.Service: {
@@ -59,45 +83,40 @@ var (
// NB: Pods need to be labeled and selected by their respective Service name, meaning:
// - The Service's `spec.selector` needs to select on `name`
// - The Service's `metadata.name` needs to be the same value as `spec.selector.name`
- docker.MemoryUsage: parsedTemplate(`namespace_label_name:container_memory_usage_bytes:sum{label_name="{{.Label}}"}`),
+ docker.MemoryUsage: parsedTemplate(`namespace_label_name:container_memory_usage_bytes:sum{label_name="{{.Label}}"}`),
docker.CPUTotalUsage: parsedTemplate(`namespace_label_name:container_cpu_usage_seconds_total:sum_rate{label_name="{{.Label}}}`),
},
}
+ k8sControllers = map[string]struct{}{
+ report.Deployment: {},
+ "stateful_set": {},
+ "cron_job": {},
+ }
)
-// NodeMetricLinks returns the links of a node. The links are collected
-// by a predefined set but filtered depending on whether a query
-// is configured or not for the particular topology.
-func NodeMetricLinks(_ report.Report, n report.Node) []MetricLink {
- queries := topologyQueries[n.Topology]
- if len(queries) == 0 {
- return nil
- }
-
- links := []MetricLink{}
- for _, link := range linkTemplates {
- if _, ok := queries[link.ID]; ok {
- links = append(links, link)
- }
- }
-
- return links
-}
-
-// RenderMetricLinks executes the templated links by supplying the node summary as data.
-// `metricsGraphURL` supports placeholders such as `:orgID` and `:query`. If the `:query`
-// part is missing, a JSON version will be appended, see `queryParamsAsJSON()` for more info.
-// It returns the modified summary.
-func RenderMetricLinks(summary NodeSummary, n report.Node, metricsGraphURL string) NodeSummary {
- queries := topologyQueries[n.Topology]
- if len(queries) == 0 || len(summary.MetricLinks) == 0 {
+// RenderMetricURLs sets respective URLs for metrics in a node summary. Missing metrics
+// where we have a query for will be appended as an empty metric (no values or samples).
+func RenderMetricURLs(summary NodeSummary, n report.Node, metricsGraphURL string) NodeSummary {
+ if metricsGraphURL == "" {
return summary
}
- links := []MetricLink{}
+ queries := getTopologyQueries(n.Topology)
+ if len(queries) == 0 {
+ return summary
+ }
+
+ var maxprio float64
var bs bytes.Buffer
- for _, link := range summary.MetricLinks {
- tpl := queries[link.ID]
+ var ms []report.MetricRow
+ found := make(map[string]struct{})
+
+ // Set URL on existing metrics
+ for _, metric := range summary.Metrics {
+ if metric.Priority > maxprio {
+ maxprio = metric.Priority
+ }
+ tpl := queries[metric.ID]
if tpl == nil {
continue
}
@@ -107,19 +126,47 @@ func RenderMetricLinks(summary NodeSummary, n report.Node, metricsGraphURL strin
continue
}
- link.URL = buildURL(bs.String(), metricsGraphURL)
- links = append(links, link)
+ ms = append(ms, metric)
+ ms[len(ms)-1].URL = buildURL(bs.String(), metricsGraphURL)
+ found[metric.ID] = struct{}{}
}
- summary.MetricLinks = links
+
+ // Append empty metrics for unattached queries
+ for _, metadata := range shownQueries {
+ if _, ok := found[metadata.ID]; ok {
+ continue
+ }
+
+ tpl := queries[metadata.ID]
+ if tpl == nil {
+ continue
+ }
+
+ bs.Reset()
+ if err := tpl.Execute(&bs, summary); err != nil {
+ continue
+ }
+
+ maxprio++
+ ms = append(ms, report.MetricRow{
+ ID: metadata.ID,
+ Label: metadata.Label,
+ URL: buildURL(bs.String(), metricsGraphURL),
+ Metric: &report.Metric{},
+ Priority: maxprio,
+ ValueEmpty: true,
+ })
+ }
+
+ summary.Metrics = ms
return summary
}
-// buildURL puts together the URL by looking at the configured
-// `metricsGraphURL`.
+// buildURL puts together the URL by looking at the configured `metricsGraphURL`.
func buildURL(query, metricsGraphURL string) string {
if strings.Contains(metricsGraphURL, urlQueryVarName) {
- return strings.Replace(metricsGraphURL, urlQueryVarName, url.PathEscape(query), -1)
+ return strings.Replace(metricsGraphURL, urlQueryVarName, url.QueryEscape(query), -1)
}
params, err := queryParamsAsJSON(query)
@@ -131,7 +178,7 @@ func buildURL(query, metricsGraphURL string) string {
metricsGraphURL += "/"
}
- return metricsGraphURL + url.PathEscape(params)
+ return metricsGraphURL + url.QueryEscape(params)
}
// queryParamsAsJSON packs the query into a JSON of the
@@ -163,3 +210,10 @@ func parsedTemplate(query string) *template.Template {
return tpl
}
+
+func getTopologyQueries(t string) map[string]*template.Template {
+ if _, ok := k8sControllers[t]; ok {
+ t = "__k8s_controllers"
+ }
+ return topologyQueries[t]
+}
diff --git a/render/detailed/links_test.go b/render/detailed/links_test.go
index 29328d7d2..811f51fc5 100644
--- a/render/detailed/links_test.go
+++ b/render/detailed/links_test.go
@@ -10,54 +10,82 @@ import (
"github.com/stretchr/testify/assert"
)
-var (
- sampleReport = report.Report{}
- samplePodNode = report.MakeNode("noo").WithTopology(report.Pod)
- sampleUnknownNode = report.MakeNode("???").WithTopology("foo")
+const (
+ sampleMetricsGraphURL = "/prom/:orgID/notebook/new"
)
-func TestNodeMetricLinks_UnknownTopology(t *testing.T) {
- links := detailed.NodeMetricLinks(sampleReport, sampleUnknownNode)
- assert.Nil(t, links)
-}
-
-func TestNodeMetricLinks(t *testing.T) {
- expected := []detailed.MetricLink{
- {ID: docker.CPUTotalUsage, Label: "CPU", Priority: 1, URL: ""},
- {ID: docker.MemoryUsage, Label: "Memory", Priority: 2, URL: ""},
+var (
+ sampleUnknownNode = report.MakeNode("???").WithTopology("foo")
+ samplePodNode = report.MakeNode("noo").WithTopology(report.Pod)
+ sampleMetrics = []report.MetricRow{
+ {ID: docker.MemoryUsage},
+ {ID: docker.CPUTotalUsage},
}
+)
- links := detailed.NodeMetricLinks(sampleReport, samplePodNode)
- assert.Equal(t, expected, links)
+func TestRenderMetricURLs_Disabled(t *testing.T) {
+ s := detailed.NodeSummary{Label: "foo", Metrics: sampleMetrics}
+ result := detailed.RenderMetricURLs(s, samplePodNode, "")
+
+ assert.Empty(t, result.Metrics[0].URL)
+ assert.Empty(t, result.Metrics[1].URL)
}
-func TestRenderMetricLinks_UnknownTopology(t *testing.T) {
- summary := detailed.NodeSummary{}
+func TestRenderMetricURLs_UnknownTopology(t *testing.T) {
+ s := detailed.NodeSummary{Label: "foo", Metrics: sampleMetrics}
+ result := detailed.RenderMetricURLs(s, sampleUnknownNode, sampleMetricsGraphURL)
- result := detailed.RenderMetricLinks(summary, sampleUnknownNode, "")
- assert.Equal(t, summary, result)
+ assert.Empty(t, result.Metrics[0].URL)
+ assert.Empty(t, result.Metrics[1].URL)
}
-func TestRenderMetricLinks_Pod(t *testing.T) {
- summary := detailed.NodeSummary{Label: "woo", MetricLinks: detailed.NodeMetricLinks(sampleReport, samplePodNode)}
+func TestRenderMetricURLs(t *testing.T) {
+ s := detailed.NodeSummary{Label: "foo", Metrics: sampleMetrics}
+ result := detailed.RenderMetricURLs(s, samplePodNode, sampleMetricsGraphURL)
- result := detailed.RenderMetricLinks(summary, samplePodNode, "/prom/:orgID/notebook/new")
- assert.Equal(t,
- "/prom/:orgID/notebook/new/%7B%22cells%22:%5B%7B%22queries%22:%5B%22sum%28rate%28container_cpu_usage_seconds_total%7Bpod_name=%5C%22woo%5C%22%7D%5B1m%5D%29%29%22%5D%7D%5D%7D",
- result.MetricLinks[0].URL)
- assert.Equal(t,
- "/prom/:orgID/notebook/new/%7B%22cells%22:%5B%7B%22queries%22:%5B%22sum%28container_memory_usage_bytes%7Bpod_name=%5C%22woo%5C%22%7D%29%22%5D%7D%5D%7D",
- result.MetricLinks[1].URL)
+ u := "/prom/:orgID/notebook/new/%7B%22cells%22%3A%5B%7B%22queries%22%3A%5B%22sum%28container_memory_usage_bytes%7Bpod_name%3D%5C%22foo%5C%22%7D%29%22%5D%7D%5D%7D"
+ assert.Equal(t, u, result.Metrics[0].URL)
+ u = "/prom/:orgID/notebook/new/%7B%22cells%22%3A%5B%7B%22queries%22%3A%5B%22sum%28rate%28container_cpu_usage_seconds_total%7Bpod_name%3D%5C%22foo%5C%22%7D%5B1m%5D%29%29%22%5D%7D%5D%7D"
+ assert.Equal(t, u, result.Metrics[1].URL)
}
-func TestRenderMetricLinks_QueryReplacement(t *testing.T) {
- summary := detailed.NodeSummary{Label: "boo", MetricLinks: detailed.NodeMetricLinks(sampleReport, samplePodNode)}
+func TestRenderMetricURLs_EmptyMetrics(t *testing.T) {
+ result := detailed.RenderMetricURLs(detailed.NodeSummary{}, samplePodNode, sampleMetricsGraphURL)
- result := detailed.RenderMetricLinks(summary, samplePodNode, "/foo/:orgID/bar?q=:query")
- assert.Equal(t,
- "/foo/:orgID/bar?q=sum%28rate%28container_cpu_usage_seconds_total%7Bpod_name=%22boo%22%7D%5B1m%5D%29%29",
- result.MetricLinks[0].URL)
- assert.Equal(t,
- "/foo/:orgID/bar?q=sum%28container_memory_usage_bytes%7Bpod_name=%22boo%22%7D%29",
- result.MetricLinks[1].URL)
+ m := result.Metrics[0]
+ assert.Equal(t, docker.CPUTotalUsage, m.ID)
+ assert.Equal(t, "CPU", m.Label)
+ assert.NotEmpty(t, m.URL)
+ assert.True(t, m.ValueEmpty)
+ assert.Equal(t, float64(1), m.Priority)
+
+ m = result.Metrics[1]
+ assert.NotEmpty(t, m.URL)
+ assert.True(t, m.ValueEmpty)
+ assert.Equal(t, float64(2), m.Priority)
+}
+
+func TestRenderMetricURLs_CombinedEmptyMetrics(t *testing.T) {
+ s := detailed.NodeSummary{
+ Label: "foo",
+ Metrics: []report.MetricRow{{ID: docker.MemoryUsage, Priority: 1}},
+ }
+ result := detailed.RenderMetricURLs(s, samplePodNode, sampleMetricsGraphURL)
+
+ assert.NotEmpty(t, result.Metrics[0].URL)
+ assert.False(t, result.Metrics[0].ValueEmpty)
+
+ assert.NotEmpty(t, result.Metrics[1].URL)
+ assert.True(t, result.Metrics[1].ValueEmpty)
+ assert.Equal(t, float64(2), result.Metrics[1].Priority) // first empty metric starts at non-empty prio + 1
+}
+
+func TestRenderMetricURLs_QueryReplacement(t *testing.T) {
+ s := detailed.NodeSummary{Label: "foo", Metrics: sampleMetrics}
+ result := detailed.RenderMetricURLs(s, samplePodNode, "http://example.test/?q=:query")
+
+ u := "http://example.test/?q=sum%28container_memory_usage_bytes%7Bpod_name%3D%22foo%22%7D%29"
+ assert.Equal(t, u, result.Metrics[0].URL)
+ u = "http://example.test/?q=sum%28rate%28container_cpu_usage_seconds_total%7Bpod_name%3D%22foo%22%7D%5B1m%5D%29%29"
+ assert.Equal(t, u, result.Metrics[1].URL)
}
diff --git a/render/detailed/summary.go b/render/detailed/summary.go
index 697e7ecdd..d302f6b38 100644
--- a/render/detailed/summary.go
+++ b/render/detailed/summary.go
@@ -44,20 +44,19 @@ type Column struct {
// NodeSummary is summary information about a child for a Node.
type NodeSummary struct {
- ID string `json:"id"`
- Label string `json:"label"`
- LabelMinor string `json:"labelMinor"`
- 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"`
- Parents []Parent `json:"parents,omitempty"`
- Metrics []report.MetricRow `json:"metrics,omitempty"`
- Tables []report.Table `json:"tables,omitempty"`
- Adjacency report.IDList `json:"adjacency,omitempty"`
- MetricLinks []MetricLink `json:"metric_links,omitempty"`
+ ID string `json:"id"`
+ Label string `json:"label"`
+ LabelMinor string `json:"labelMinor"`
+ 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"`
+ Parents []Parent `json:"parents,omitempty"`
+ Metrics []report.MetricRow `json:"metrics,omitempty"`
+ Tables []report.Table `json:"tables,omitempty"`
+ Adjacency report.IDList `json:"adjacency,omitempty"`
}
var renderers = map[string]func(NodeSummary, report.Node) (NodeSummary, bool){
@@ -104,20 +103,20 @@ var primaryAPITopology = map[string]string{
// MakeNodeSummary summarizes a node, if possible.
func MakeNodeSummary(r report.Report, n report.Node, metricsGraphURL string) (NodeSummary, bool) {
- metricLinks := metricsGraphURL != ""
if renderer, ok := renderers[n.Topology]; ok {
// Skip (and don't fall through to fallback) if renderer maps to nil
if renderer != nil {
- summary, b := renderer(baseNodeSummary(r, n, metricLinks), n)
- return RenderMetricLinks(summary, n, metricsGraphURL), b
+ summary, b := renderer(baseNodeSummary(r, n), n)
+ return RenderMetricURLs(summary, n, metricsGraphURL), b
}
} else if _, ok := r.Topology(n.Topology); ok {
- summary := baseNodeSummary(r, n, metricLinks)
+ summary := baseNodeSummary(r, n)
summary.Label = n.ID // This is unlikely to look very good, but is a reasonable fallback
return summary, true
}
if strings.HasPrefix(n.Topology, "group:") {
- return groupNodeSummary(baseNodeSummary(r, n, metricLinks), r, n)
+ summary, b := groupNodeSummary(baseNodeSummary(r, n), r, n)
+ return RenderMetricURLs(summary, n, metricsGraphURL), b
}
return NodeSummary{}, false
}
@@ -133,7 +132,7 @@ func (n NodeSummary) SummarizeMetrics() NodeSummary {
return n
}
-func baseNodeSummary(r report.Report, n report.Node, metricLinks bool) NodeSummary {
+func baseNodeSummary(r report.Report, n report.Node) NodeSummary {
t, _ := r.Topology(n.Topology)
ns := NodeSummary{
ID: n.ID,
@@ -145,9 +144,7 @@ func baseNodeSummary(r report.Report, n report.Node, metricLinks bool) NodeSumma
Tables: NodeTables(r, n),
Adjacency: n.Adjacency,
}
- if metricLinks {
- ns.MetricLinks = NodeMetricLinks(r, n)
- }
+
return ns
}
diff --git a/report/metric_row.go b/report/metric_row.go
index b33b37851..2d61e0218 100644
--- a/report/metric_row.go
+++ b/report/metric_row.go
@@ -16,13 +16,15 @@ const (
// MetricRow is a tuple of data used to render a metric as a sparkline and
// accoutrements.
type MetricRow struct {
- ID string
- Label string
- Format string
- Group string
- Value float64
- Priority float64
- Metric *Metric
+ ID string
+ Label string
+ Format string
+ Group string
+ Value float64
+ ValueEmpty bool
+ Priority float64
+ URL string
+ Metric *Metric
}
// Summary returns a copy of the MetricRow, without the samples, just the value if there is one.
@@ -49,17 +51,19 @@ func (*MetricRow) UnmarshalJSON(b []byte) error {
// Needed to flatten the fields for backwards compatibility with probes
// (time.Time is encoded in binary in MsgPack)
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"`
- Priority float64 `json:"priority,omitempty"`
- Samples []Sample `json:"samples"`
- Min float64 `json:"min"`
- Max float64 `json:"max"`
- First string `json:"first,omitempty"`
- Last string `json:"last,omitempty"`
+ ID string `json:"id"`
+ Label string `json:"label"`
+ Format string `json:"format,omitempty"`
+ Group string `json:"group,omitempty"`
+ Value float64 `json:"value"`
+ ValueEmpty bool `json:"valueEmpty,omitempty"`
+ Priority float64 `json:"priority,omitempty"`
+ Samples []Sample `json:"samples"`
+ Min float64 `json:"min"`
+ Max float64 `json:"max"`
+ First string `json:"first,omitempty"`
+ Last string `json:"last,omitempty"`
+ URL string `json:"url"`
}
// CodecEncodeSelf marshals this MetricRow. It takes the basic Metric
@@ -67,17 +71,19 @@ type wiredMetricRow struct {
func (m *MetricRow) CodecEncodeSelf(encoder *codec.Encoder) {
in := m.Metric.ToIntermediate()
encoder.Encode(wiredMetricRow{
- ID: m.ID,
- Label: m.Label,
- Format: m.Format,
- Group: m.Group,
- Value: m.Value,
- Priority: m.Priority,
- Samples: in.Samples,
- Min: in.Min,
- Max: in.Max,
- First: in.First,
- Last: in.Last,
+ ID: m.ID,
+ Label: m.Label,
+ Format: m.Format,
+ Group: m.Group,
+ Value: m.Value,
+ ValueEmpty: m.ValueEmpty,
+ Priority: m.Priority,
+ URL: m.URL,
+ Samples: in.Samples,
+ Min: in.Min,
+ Max: in.Max,
+ First: in.First,
+ Last: in.Last,
})
}
@@ -94,13 +100,14 @@ func (m *MetricRow) CodecDecodeSelf(decoder *codec.Decoder) {
}
metric := w.FromIntermediate()
*m = MetricRow{
- ID: in.ID,
- Label: in.Label,
- Format: in.Format,
- Group: in.Group,
- Value: in.Value,
- Priority: in.Priority,
- Metric: &metric,
+ ID: in.ID,
+ Label: in.Label,
+ Format: in.Format,
+ Group: in.Group,
+ Value: in.Value,
+ ValueEmpty: in.ValueEmpty,
+ Priority: in.Priority,
+ Metric: &metric,
}
}