Files
weave-scope/render/detailed/links.go
2017-08-11 16:45:13 +01:00

166 lines
5.3 KiB
Go

package detailed
import (
"bytes"
"net/url"
"strings"
"text/template"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/report"
"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
const urlQueryVarName = ":query"
var (
// 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: {
// `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.
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.
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.CPUTotalUsage: parsedTemplate(`sum(rate(container_cpu_usage_seconds_total{pod_name=~"^{{.Label}}-[^-]+$"}[1m]))`),
},
report.Service: {
// These recording rules must be defined in the prometheus config.
// 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.CPUTotalUsage: parsedTemplate(`namespace_label_name:container_cpu_usage_seconds_total:sum_rate{label_name="{{.Label}}}`),
},
}
)
// 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 {
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(), metricsGraphURL)
links = append(links, link)
}
summary.MetricLinks = links
return summary
}
// 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)
}
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}}}}
buf := &bytes.Buffer{}
encoder := codec.NewEncoder(buf, &codec.JsonHandle{})
if err := encoder.Encode(params); err != nil {
return "", err
}
return buf.String(), nil
}
// parsedTemplate initializes unnamed text templates.
func parsedTemplate(query string) *template.Template {
tpl, err := template.New("").Parse(query)
if err != nil {
panic(err)
}
return tpl
}