mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-27 17:21:34 +00:00
Link scope-ui graphs clickable to prometheus queries
scope-app: - Adds `-app.metrics-graph` cli flag for configuring the base url to use for graph links; supports `:orgID` and `:query` placeholders - Renders `metric_links` in node detail API response scope-ui: - Extends `<CloudFeature />` with option `alwaysShow` and adds boolean `isCloud` property - Links metric graphs in the ui's node details view for all k8s toplogies; or displays placeholder graph if no metrics available
This commit is contained in:
@@ -13,10 +13,16 @@ class CloudFeature extends React.Component {
|
||||
if (process.env.WEAVE_CLOUD) {
|
||||
return React.cloneElement(React.Children.only(this.props.children), {
|
||||
params: this.context.router.params,
|
||||
router: this.context.router
|
||||
router: this.context.router,
|
||||
isCloud: true
|
||||
});
|
||||
}
|
||||
|
||||
// also show if not in weave cloud?
|
||||
if (this.props.alwaysShow) {
|
||||
return React.cloneElement(React.Children.only(this.props.children), {isCloud: false});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,14 @@ class NodeDetails extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
// collect by metric id (id => link)
|
||||
const metricLinks = (details.metric_links || [])
|
||||
.reduce((agg, link) => Object.assign(agg, {[link.id]: link}), {});
|
||||
|
||||
// collect links with no corresponding metric
|
||||
const unattachedLinks = Object.assign({}, metricLinks);
|
||||
(details.metrics || []).forEach(metric => delete unattachedLinks[metric.id]);
|
||||
|
||||
return (
|
||||
<div className="node-details">
|
||||
{tools}
|
||||
@@ -190,9 +198,13 @@ class NodeDetails extends React.Component {
|
||||
</div>}
|
||||
|
||||
<div className="node-details-content">
|
||||
{details.metrics && <div className="node-details-content-section">
|
||||
{Object.keys(metricLinks).length > 0 && <div className="node-details-content-section">
|
||||
<div className="node-details-content-section-header">Status</div>
|
||||
<NodeDetailsHealth metrics={details.metrics} />
|
||||
<NodeDetailsHealth
|
||||
metrics={details.metrics}
|
||||
metricLinks={metricLinks}
|
||||
unattachedLinks={unattachedLinks}
|
||||
/>
|
||||
</div>}
|
||||
{details.metadata && <div className="node-details-content-section">
|
||||
<div className="node-details-content-section-header">Info</div>
|
||||
|
||||
@@ -6,13 +6,17 @@ import { formatMetric } from '../../utils/string-utils';
|
||||
function NodeDetailsHealthItem(props) {
|
||||
return (
|
||||
<div className="node-details-health-item">
|
||||
<div className="node-details-health-item-value">{formatMetric(props.value, props)}</div>
|
||||
<div className="node-details-health-item-sparkline">
|
||||
{props.value !== undefined && <div className="node-details-health-item-value">{formatMetric(props.value, props)}</div>}
|
||||
{props.samples && <div className="node-details-health-item-sparkline">
|
||||
<Sparkline
|
||||
data={props.samples} max={props.max} format={props.format}
|
||||
first={props.first} last={props.last} />
|
||||
</div>}
|
||||
{!props.samples && <div className="node-details-health-item-placeholder"><span className="fa fa-circle-thin" /></div>}
|
||||
<div className="node-details-health-item-label">
|
||||
{props.label}
|
||||
{props.icon && <span className={`fa ${props.icon}`} />}
|
||||
</div>
|
||||
<div className="node-details-health-item-label">{props.label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
import NodeDetailsHealthItem from './node-details-health-item';
|
||||
|
||||
export default class NodeDetailsHealthLinkItem extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.buildHref = this.buildHref.bind(this);
|
||||
}
|
||||
|
||||
handleClick(ev, href) {
|
||||
ev.preventDefault();
|
||||
if (!href) return;
|
||||
|
||||
const {router} = this.props;
|
||||
if (router && href[0] === '/') {
|
||||
router.push(href);
|
||||
} else {
|
||||
location.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
buildHref(url) {
|
||||
if (!url || !this.props.isCloud) return url;
|
||||
return url.replace(/:orgid/gi, encodeURIComponent(this.props.params.orgId));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {links, withoutGraph, ...props} = this.props;
|
||||
const href = this.buildHref(links[props.id] && links[props.id].url);
|
||||
|
||||
if (!href) return <NodeDetailsHealthItem {...props} />;
|
||||
|
||||
return (
|
||||
<a
|
||||
className="node-details-health-link-item"
|
||||
href={href}
|
||||
onClick={e => this.handleClick(e, href)}>
|
||||
{!withoutGraph && <NodeDetailsHealthItem {...props} icon="fa-expand" />}
|
||||
{withoutGraph && <NodeDetailsHealthItem label={props.label} icon="fa-expand" />}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import React from 'react';
|
||||
|
||||
import ShowMore from '../show-more';
|
||||
import NodeDetailsHealthOverflow from './node-details-health-overflow';
|
||||
import NodeDetailsHealthItem from './node-details-health-item';
|
||||
import NodeDetailsHealthLinkItem from './node-details-health-link-item';
|
||||
import CloudFeature from '../cloud-feature';
|
||||
|
||||
export default class NodeDetailsHealth extends React.Component {
|
||||
|
||||
@@ -21,6 +22,9 @@ export default class NodeDetailsHealth extends React.Component {
|
||||
|
||||
render() {
|
||||
const metrics = this.props.metrics || [];
|
||||
const metricLinks = this.props.metricLinks || {};
|
||||
const unattachedLinks = this.props.unattachedLinks || {};
|
||||
const hasUnattached = Object.keys(unattachedLinks).length > 0;
|
||||
const primeCutoff = metrics.length > 3 && !this.state.expanded ? 2 : metrics.length;
|
||||
const primeMetrics = metrics.slice(0, primeCutoff);
|
||||
const overflowMetrics = metrics.slice(primeCutoff);
|
||||
@@ -32,7 +36,12 @@ export default class NodeDetailsHealth extends React.Component {
|
||||
return (
|
||||
<div className="node-details-health" style={{flexWrap, justifyContent}}>
|
||||
<div className="node-details-health-wrapper">
|
||||
{primeMetrics.map(item => <NodeDetailsHealthItem key={item.id} {...item} />)}
|
||||
{primeMetrics.map(item => <CloudFeature alwaysShow key={item.id}>
|
||||
<NodeDetailsHealthLinkItem
|
||||
{...item}
|
||||
links={metricLinks}
|
||||
/>
|
||||
</CloudFeature>)}
|
||||
{showOverflow && <NodeDetailsHealthOverflow
|
||||
items={overflowMetrics}
|
||||
handleClick={this.handleClickMore}
|
||||
@@ -41,6 +50,16 @@ export default class NodeDetailsHealth extends React.Component {
|
||||
<ShowMore
|
||||
handleClick={this.handleClickMore} collection={this.props.metrics}
|
||||
expanded={this.state.expanded} notShown={notShown} hideNumber />
|
||||
|
||||
{hasUnattached && <div className="node-details-health-wrapper">
|
||||
{Object.keys(unattachedLinks).map(id => <CloudFeature alwaysShow key={id}>
|
||||
<NodeDetailsHealthLinkItem
|
||||
withoutGraph
|
||||
{...unattachedLinks[id]}
|
||||
links={unattachedLinks}
|
||||
/>
|
||||
</CloudFeature>)}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -931,12 +931,31 @@
|
||||
color: $text-secondary-color;
|
||||
text-transform: uppercase;
|
||||
font-size: 80%;
|
||||
|
||||
.fa {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&-value {
|
||||
color: $text-secondary-color;
|
||||
font-size: 150%;
|
||||
padding-bottom: 0.5em;
|
||||
&-placeholder {
|
||||
font-size: 200%;
|
||||
opacity: 0.2;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
&-link-item {
|
||||
@extend .btn-opacity;
|
||||
cursor: pointer;
|
||||
opacity: $link-opacity-default;
|
||||
width: 33%;
|
||||
|
||||
.label {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.node-details-health-item {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,17 +18,18 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/tylerb/graceful"
|
||||
"github.com/weaveworks/go-checkpoint"
|
||||
"github.com/weaveworks/scope/common/xfer"
|
||||
"github.com/weaveworks/weave/common"
|
||||
|
||||
billing "github.com/weaveworks/billing-client"
|
||||
"github.com/weaveworks/common/middleware"
|
||||
"github.com/weaveworks/common/network"
|
||||
"github.com/weaveworks/go-checkpoint"
|
||||
"github.com/weaveworks/scope/app"
|
||||
"github.com/weaveworks/scope/app/multitenant"
|
||||
"github.com/weaveworks/scope/common/weave"
|
||||
"github.com/weaveworks/scope/common/xfer"
|
||||
"github.com/weaveworks/scope/probe/docker"
|
||||
"github.com/weaveworks/scope/render/detailed"
|
||||
"github.com/weaveworks/weave/common"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -216,6 +217,7 @@ func appMain(flags appFlags) {
|
||||
setLogLevel(flags.logLevel)
|
||||
setLogFormatter(flags.logPrefix)
|
||||
runtime.SetBlockProfileRate(flags.blockProfileRate)
|
||||
detailed.SetMetricsGraphURL(flags.metricsGraphURL)
|
||||
|
||||
defer log.Info("app exiting")
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
@@ -160,6 +160,7 @@ type appFlags struct {
|
||||
memcachedCompressionLevel int
|
||||
userIDHeader string
|
||||
externalUI bool
|
||||
metricsGraphURL string
|
||||
|
||||
blockProfileRate int
|
||||
|
||||
@@ -355,6 +356,7 @@ func setupFlags(flags *flags) {
|
||||
flag.IntVar(&flags.app.memcachedCompressionLevel, "app.memcached.compression", gzip.DefaultCompression, "How much to compress reports stored in memcached.")
|
||||
flag.StringVar(&flags.app.userIDHeader, "app.userid.header", "", "HTTP header to use as userid")
|
||||
flag.BoolVar(&flags.app.externalUI, "app.externalUI", false, "Point to externally hosted static UI assets")
|
||||
flag.StringVar(&flags.app.metricsGraphURL, "app.metrics-graph", "", "Enable extended metrics graph by providing a templated URL (supports :orgID and :query)")
|
||||
|
||||
flag.IntVar(&flags.app.blockProfileRate, "app.block.profile.rate", 0, "If more than 0, enable block profiling. The profiler aims to sample an average of one blocking event per rate nanoseconds spent blocked.")
|
||||
|
||||
|
||||
@@ -13,11 +13,10 @@ import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/weaveworks/go-checkpoint"
|
||||
"github.com/weaveworks/weave/common"
|
||||
|
||||
"github.com/weaveworks/common/network"
|
||||
"github.com/weaveworks/common/sanitize"
|
||||
"github.com/weaveworks/go-checkpoint"
|
||||
"github.com/weaveworks/scope/common/hostname"
|
||||
"github.com/weaveworks/scope/common/weave"
|
||||
"github.com/weaveworks/scope/common/xfer"
|
||||
@@ -33,6 +32,7 @@ import (
|
||||
"github.com/weaveworks/scope/probe/plugins"
|
||||
"github.com/weaveworks/scope/probe/process"
|
||||
"github.com/weaveworks/scope/report"
|
||||
"github.com/weaveworks/weave/common"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
167
render/detailed/links.go
Normal file
167
render/detailed/links.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package detailed
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/weaveworks/scope/probe/docker"
|
||||
"github.com/weaveworks/scope/report"
|
||||
)
|
||||
|
||||
// MetricLink describes a link referencing a metric.
|
||||
type MetricLink struct {
|
||||
// References the metric id
|
||||
ID string `json:"id,omitempty"`
|
||||
Label string `json:"label"`
|
||||
URL string `json:"url"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
// Variable name for the query within the metrics graph url
|
||||
const urlQueryVarName = ":query"
|
||||
|
||||
var (
|
||||
// As configured by the user
|
||||
metricsGraphURL = ""
|
||||
|
||||
// Available metric links
|
||||
linkTemplates = []MetricLink{
|
||||
{ID: docker.CPUTotalUsage, Label: "CPU", Priority: 1},
|
||||
{ID: docker.MemoryUsage, Label: "Memory", Priority: 2},
|
||||
}
|
||||
|
||||
// Prometheus queries for topologies
|
||||
topologyQueries = map[string]map[string]*template.Template{
|
||||
report.Pod: {
|
||||
docker.MemoryUsage: prepareTemplate(`sum(container_memory_usage_bytes{pod_name="{{.Label}}"})`),
|
||||
docker.CPUTotalUsage: prepareTemplate(`sum(rate(container_cpu_usage_seconds_total{pod_name="{{.Label}}"}[1m]))`),
|
||||
},
|
||||
report.ReplicaSet: {
|
||||
docker.MemoryUsage: prepareTemplate(`sum(container_memory_usage_bytes{pod_name=~"{{.Label}}-.+"})`),
|
||||
docker.CPUTotalUsage: prepareTemplate(`sum(rate(container_cpu_usage_seconds_total{pod_name=~"{{.Label}}-.+"}[1m]))`),
|
||||
},
|
||||
report.Deployment: {
|
||||
docker.MemoryUsage: prepareTemplate(`sum(container_memory_usage_bytes{pod_name=~"{{.Label}}-[0-9]+-[^-]+"})`),
|
||||
docker.CPUTotalUsage: prepareTemplate(`sum(rate(container_cpu_usage_seconds_total{pod_name=~"{{.Label}}-[0-9]+-[^-]+"}[1m]))`),
|
||||
},
|
||||
report.DaemonSet: {
|
||||
docker.MemoryUsage: prepareTemplate(`namespace_name:container_memory_usage_bytes:sum{name="{{.Label}}",monitor=""}`),
|
||||
docker.CPUTotalUsage: prepareTemplate(`namespace_name:container_cpu_usage_seconds_total:sum_rate{name="{{.Label}}"}`),
|
||||
},
|
||||
report.Service: {
|
||||
docker.MemoryUsage: prepareTemplate(`namespace_name:container_memory_usage_bytes:sum{name="{{.Label}}",monitor=""}`),
|
||||
docker.CPUTotalUsage: prepareTemplate(`namespace_name:container_cpu_usage_seconds_total:sum_rate{name="{{.Label}}"}`),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// SetMetricsGraphURL sets the URL we deduce our eventual metric link from.
|
||||
// Supports placeholders such as `:orgID` and `:query`. An empty url disables
|
||||
// this feature. If the `:query` part is missing, a JSON version will be
|
||||
// appended, see `queryParamsAsJSON()` for more info.
|
||||
func SetMetricsGraphURL(url string) {
|
||||
metricsGraphURL = url
|
||||
}
|
||||
|
||||
// NodeLinks 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 {
|
||||
if metricsGraphURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// RenderLinks executes the templated links by supplying the node summary as data.
|
||||
// It returns the modified summary.
|
||||
func RenderMetricLinks(summary NodeSummary, n report.Node) NodeSummary {
|
||||
queries := topologyQueries[n.Topology]
|
||||
if len(queries) == 0 || len(summary.MetricLinks) == 0 {
|
||||
return summary
|
||||
}
|
||||
|
||||
links := []MetricLink{}
|
||||
var bs bytes.Buffer
|
||||
for _, link := range summary.MetricLinks {
|
||||
tpl := queries[link.ID]
|
||||
if tpl == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
bs.Reset()
|
||||
if err := tpl.Execute(&bs, summary); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
link.URL = buildURL(bs.String())
|
||||
links = append(links, link)
|
||||
}
|
||||
summary.MetricLinks = links
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// buildURL puts together the URL by looking at the configured
|
||||
// `metricsGraphURL`.
|
||||
func buildURL(query string) string {
|
||||
if strings.Contains(metricsGraphURL, urlQueryVarName) {
|
||||
return strings.Replace(metricsGraphURL, urlQueryVarName, url.PathEscape(query), -1)
|
||||
}
|
||||
|
||||
params, err := queryParamsAsJSON(query)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if metricsGraphURL[len(metricsGraphURL)-1] != '/' {
|
||||
metricsGraphURL += "/"
|
||||
}
|
||||
|
||||
return metricsGraphURL + url.PathEscape(params)
|
||||
}
|
||||
|
||||
// queryParamsAsJSON packs the query into a JSON of the
|
||||
// format `{"cells":[{"queries":[$query]}]}`.
|
||||
func queryParamsAsJSON(query string) (string, error) {
|
||||
type cell struct {
|
||||
Queries []string `json:"queries"`
|
||||
}
|
||||
type queryParams struct {
|
||||
Cells []cell `json:"cells"`
|
||||
}
|
||||
params := &queryParams{[]cell{{[]string{query}}}}
|
||||
|
||||
bs, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(bs), nil
|
||||
}
|
||||
|
||||
// prepareTemplate initializes unnamed text templates.
|
||||
func prepareTemplate(query string) *template.Template {
|
||||
tpl, err := template.New("").Parse(query)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tpl
|
||||
}
|
||||
76
render/detailed/links_test.go
Normal file
76
render/detailed/links_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package detailed_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/weaveworks/scope/probe/docker"
|
||||
"github.com/weaveworks/scope/render/detailed"
|
||||
"github.com/weaveworks/scope/report"
|
||||
"github.com/weaveworks/scope/test/fixture"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNodeMetricLinks_DefaultDisabled(t *testing.T) {
|
||||
links := detailed.NodeMetricLinks(fixture.Report, fixture.Report.Pod.Nodes[fixture.ClientPodNodeID])
|
||||
assert.Nil(t, links)
|
||||
}
|
||||
|
||||
func TestNodeMetricLinks_UnknownTopology(t *testing.T) {
|
||||
detailed.SetMetricsGraphURL("/foo")
|
||||
node := report.MakeNode("foo").WithTopology("bar")
|
||||
|
||||
links := detailed.NodeMetricLinks(report.Report{}, node)
|
||||
assert.Nil(t, links)
|
||||
}
|
||||
|
||||
func TestNodeMetricLinks(t *testing.T) {
|
||||
detailed.SetMetricsGraphURL("/foo")
|
||||
defer detailed.SetMetricsGraphURL("")
|
||||
node := fixture.Report.Pod.Nodes[fixture.ClientPodNodeID]
|
||||
expected := []detailed.MetricLink{
|
||||
{ID: docker.CPUTotalUsage, Label: "CPU", Priority: 1, URL: ""},
|
||||
{ID: docker.MemoryUsage, Label: "Memory", Priority: 2, URL: ""},
|
||||
}
|
||||
|
||||
links := detailed.NodeMetricLinks(fixture.Report, node)
|
||||
assert.Equal(t, expected, links)
|
||||
}
|
||||
|
||||
func TestRenderMetricLinks_UnknownTopology(t *testing.T) {
|
||||
summary := detailed.NodeSummary{}
|
||||
node := report.MakeNode("foo").WithTopology("bar")
|
||||
|
||||
result := detailed.RenderMetricLinks(summary, node)
|
||||
assert.Equal(t, summary, result)
|
||||
}
|
||||
|
||||
func TestRenderMetricLinks_Pod(t *testing.T) {
|
||||
detailed.SetMetricsGraphURL("/prom/:orgID/notebook/new")
|
||||
defer detailed.SetMetricsGraphURL("")
|
||||
node := fixture.Report.Pod.Nodes[fixture.ClientPodNodeID]
|
||||
summary := detailed.NodeSummary{Label: "woo", MetricLinks: detailed.NodeMetricLinks(fixture.Report, node)}
|
||||
|
||||
result := detailed.RenderMetricLinks(summary, node)
|
||||
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)
|
||||
}
|
||||
|
||||
func TestRenderMetricLinks_QueryReplacement(t *testing.T) {
|
||||
detailed.SetMetricsGraphURL("/foo/:orgID/bar?q=:query")
|
||||
defer detailed.SetMetricsGraphURL("")
|
||||
node := fixture.Report.Pod.Nodes[fixture.ClientPodNodeID]
|
||||
summary := detailed.NodeSummary{Label: "boo", MetricLinks: detailed.NodeMetricLinks(fixture.Report, node)}
|
||||
|
||||
result := detailed.RenderMetricLinks(summary, node)
|
||||
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)
|
||||
}
|
||||
@@ -44,19 +44,20 @@ 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
var renderers = map[string]func(NodeSummary, report.Node) (NodeSummary, bool){
|
||||
@@ -106,7 +107,8 @@ func MakeNodeSummary(r report.Report, n report.Node) (NodeSummary, bool) {
|
||||
if renderer, ok := renderers[n.Topology]; ok {
|
||||
// Skip (and don't fall through to fallback) if renderer maps to nil
|
||||
if renderer != nil {
|
||||
return renderer(baseNodeSummary(r, n), n)
|
||||
summary, b := renderer(baseNodeSummary(r, n), n)
|
||||
return RenderMetricLinks(summary, n), b
|
||||
}
|
||||
} else if _, ok := r.Topology(n.Topology); ok {
|
||||
summary := baseNodeSummary(r, n)
|
||||
@@ -133,14 +135,15 @@ func (n NodeSummary) SummarizeMetrics() NodeSummary {
|
||||
func baseNodeSummary(r report.Report, n report.Node) NodeSummary {
|
||||
t, _ := r.Topology(n.Topology)
|
||||
return NodeSummary{
|
||||
ID: n.ID,
|
||||
Shape: t.GetShape(),
|
||||
Linkable: true,
|
||||
Metadata: NodeMetadata(r, n),
|
||||
Metrics: NodeMetrics(r, n),
|
||||
Parents: Parents(r, n),
|
||||
Tables: NodeTables(r, n),
|
||||
Adjacency: n.Adjacency,
|
||||
ID: n.ID,
|
||||
Shape: t.GetShape(),
|
||||
Linkable: true,
|
||||
Metadata: NodeMetadata(r, n),
|
||||
Metrics: NodeMetrics(r, n),
|
||||
MetricLinks: NodeMetricLinks(r, n),
|
||||
Parents: Parents(r, n),
|
||||
Tables: NodeTables(r, n),
|
||||
Adjacency: n.Adjacency,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/weaveworks/scope/test/fixture"
|
||||
)
|
||||
|
||||
// Exported for testing.
|
||||
var (
|
||||
circle = "circle"
|
||||
square = "square"
|
||||
|
||||
Reference in New Issue
Block a user