123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- package detailed
- import (
- "bytes"
- "fmt"
- "net/url"
- "strings"
- "github.com/weaveworks/scope/probe/docker"
- "github.com/weaveworks/scope/probe/kubernetes"
- "github.com/weaveworks/scope/report"
- "github.com/ugorji/go/codec"
- )
- const (
- // Replacement variable name for the query in the metrics graph url
- urlQueryVarName = ":query"
- idReceiveBytes = "receive_bytes"
- idTransmitBytes = "transmit_bytes"
- )
- var (
- // Metadata for shown queries
- shownQueries = []struct {
- ID string
- Label string
- }{
- {
- ID: docker.CPUTotalUsage,
- Label: "CPU",
- },
- {
- ID: docker.MemoryUsage,
- Label: "Memory",
- },
- {
- ID: idReceiveBytes,
- Label: "Rx/s",
- },
- {
- ID: idTransmitBytes,
- Label: "Tx/s",
- },
- }
- // Queries on pod names of the format `name-<id>-<hash>`
- // See also: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#pod-template-hash-label
- podIDHashQueries = formatMetricQueries(`pod_name=~"^{{label}}-[^-]+-[^-]+$",namespace="{{namespace}}"`, []string{docker.MemoryUsage, docker.CPUTotalUsage})
- // Prometheus queries for topologies
- topologyQueries = map[string]map[string]string{
- // Containers
- report.Container: formatMetricQueries(`name="{{containerName}}"`, []string{docker.MemoryUsage, docker.CPUTotalUsage}),
- report.ContainerImage: formatMetricQueries(`image="{{label}}"`, []string{docker.MemoryUsage, docker.CPUTotalUsage}),
- // Kubernetes topologies
- report.Pod: formatMetricQueries(
- `pod_name="{{label}}",namespace="{{namespace}}"`,
- []string{docker.MemoryUsage, docker.CPUTotalUsage},
- ),
- report.DaemonSet: formatMetricQueries(`pod_name=~"^{{label}}-[^-]+$",namespace="{{namespace}}"`, []string{docker.MemoryUsage, docker.CPUTotalUsage}),
- report.Deployment: podIDHashQueries,
- report.StatefulSet: podIDHashQueries,
- report.CronJob: podIDHashQueries,
- report.Service: {
- docker.CPUTotalUsage: `sum(rate(container_cpu_usage_seconds_total{image!="",namespace="{{namespace}}",_weave_pod_name="{{label}}",job="cadvisor",container_name!="POD"}[5m]))`,
- docker.MemoryUsage: `sum(rate(container_memory_usage_bytes{image!="",namespace="{{namespace}}",_weave_pod_name="{{label}}",job="cadvisor",container_name!="POD"}[5m]))`,
- },
- }
- )
- func formatMetricQueries(filter string, ids []string) map[string]string {
- queries := make(map[string]string)
- for _, id := range ids {
- // All `container_*`metrics are provided by cAdvisor in Kubelets
- switch id {
- case docker.MemoryUsage:
- queries[id] = fmt.Sprintf("sum(container_memory_usage_bytes{%s})", filter)
- case docker.CPUTotalUsage:
- queries[id] = fmt.Sprintf(
- "sum(rate(container_cpu_usage_seconds_total{%s}[1m]))/count(container_cpu_usage_seconds_total{%s})*100",
- filter,
- filter,
- )
- case idReceiveBytes:
- queries[id] = fmt.Sprintf("sum(rate(container_network_receive_bytes_total{%s}[5m]))", filter)
- case idTransmitBytes:
- queries[id] = fmt.Sprintf("sum(rate(container_network_transmit_bytes_total{%s}[5m]))", filter)
- }
- }
- return queries
- }
- // 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, r report.Report, metricsGraphURL string) NodeSummary {
- if metricsGraphURL == "" {
- return summary
- }
- var maxprio float64
- 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
- }
- query := metricQuery(summary, n, r, metric.ID)
- ms = append(ms, metric)
- if query != "" {
- ms[len(ms)-1].URL = metricURL(query, metricsGraphURL)
- }
- found[metric.ID] = struct{}{}
- }
- // Append empty metrics for unattached queries
- for _, metadata := range shownQueries {
- if _, ok := found[metadata.ID]; ok {
- continue
- }
- query := metricQuery(summary, n, r, metadata.ID)
- if query == "" {
- continue
- }
- maxprio++
- ms = append(ms, report.MetricRow{
- ID: metadata.ID,
- Label: metadata.Label,
- URL: metricURL(query, metricsGraphURL),
- Metric: &report.Metric{},
- Priority: maxprio,
- ValueEmpty: true,
- })
- }
- summary.Metrics = ms
- return summary
- }
- // metricQuery returns the query for the given node and metric.
- func metricQuery(summary NodeSummary, n report.Node, r report.Report, metricID string) string {
- queries := topologyQueries[n.Topology]
- if len(queries) == 0 {
- return ""
- }
- namespace, _ := n.Latest.Lookup(kubernetes.Namespace)
- name, _ := n.Latest.Lookup(docker.ContainerName)
- replacer := strings.NewReplacer(
- "{{label}}", metricLabel(summary, n, r),
- "{{namespace}}", namespace,
- "{{containerName}}", name,
- )
- return replacer.Replace(queries[metricID])
- }
- func metricLabel(summary NodeSummary, n report.Node, r report.Report) string {
- label := summary.Label
- if n.Topology == report.Service {
- deploymentTopology, ok := r.Topology(report.Deployment)
- if ok {
- deploymentNames := []string{}
- for _, pod := range r.Pod.Nodes {
- serviceParents, serviceOk := pod.Parents.Lookup(report.Service)
- deploymentParents, deploymentOk := pod.Parents.Lookup(report.Deployment)
- if serviceOk && deploymentOk && serviceParents.Contains(n.ID) {
- for _, id := range deploymentParents {
- deploymentNode, ok := deploymentTopology.Nodes[id]
- if !ok {
- continue
- }
- if name, ok := deploymentNode.Latest.Lookup(report.KubernetesName); ok {
- deploymentNames = append(deploymentNames, name)
- }
- }
- break
- }
- }
- if len(deploymentNames) == 1 {
- label = deploymentNames[0]
- }
- }
- }
- return label
- }
- // metricURL builds the URL by embedding it into the configured `metricsGraphURL`.
- func metricURL(query, metricsGraphURL string) string {
- if strings.Contains(metricsGraphURL, urlQueryVarName) {
- return strings.Replace(metricsGraphURL, urlQueryVarName, queryEscape(query), -1)
- }
- params, err := queryParamsAsJSON(query)
- if err != nil {
- return ""
- }
- if metricsGraphURL[len(metricsGraphURL)-1] != '/' {
- metricsGraphURL += "/"
- }
- return metricsGraphURL + queryEscape(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
- }
- // queryEscape uses `%20` instead of `+` to encode whitespaces. Both are
- // valid but react-router does not decode `+` properly.
- func queryEscape(query string) string {
- return url.QueryEscape(strings.Replace(query, " ", "%20", -1))
- }
|