mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 01:31:17 +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:
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