links.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package detailed
  2. import (
  3. "bytes"
  4. "fmt"
  5. "net/url"
  6. "strings"
  7. "github.com/weaveworks/scope/probe/docker"
  8. "github.com/weaveworks/scope/probe/kubernetes"
  9. "github.com/weaveworks/scope/report"
  10. "github.com/ugorji/go/codec"
  11. )
  12. const (
  13. // Replacement variable name for the query in the metrics graph url
  14. urlQueryVarName = ":query"
  15. idReceiveBytes = "receive_bytes"
  16. idTransmitBytes = "transmit_bytes"
  17. )
  18. var (
  19. // Metadata for shown queries
  20. shownQueries = []struct {
  21. ID string
  22. Label string
  23. }{
  24. {
  25. ID: docker.CPUTotalUsage,
  26. Label: "CPU",
  27. },
  28. {
  29. ID: docker.MemoryUsage,
  30. Label: "Memory",
  31. },
  32. {
  33. ID: idReceiveBytes,
  34. Label: "Rx/s",
  35. },
  36. {
  37. ID: idTransmitBytes,
  38. Label: "Tx/s",
  39. },
  40. }
  41. // Queries on pod names of the format `name-<id>-<hash>`
  42. // See also: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#pod-template-hash-label
  43. podIDHashQueries = formatMetricQueries(`pod_name=~"^{{label}}-[^-]+-[^-]+$",namespace="{{namespace}}"`, []string{docker.MemoryUsage, docker.CPUTotalUsage})
  44. // Prometheus queries for topologies
  45. topologyQueries = map[string]map[string]string{
  46. // Containers
  47. report.Container: formatMetricQueries(`name="{{containerName}}"`, []string{docker.MemoryUsage, docker.CPUTotalUsage}),
  48. report.ContainerImage: formatMetricQueries(`image="{{label}}"`, []string{docker.MemoryUsage, docker.CPUTotalUsage}),
  49. // Kubernetes topologies
  50. report.Pod: formatMetricQueries(
  51. `pod_name="{{label}}",namespace="{{namespace}}"`,
  52. []string{docker.MemoryUsage, docker.CPUTotalUsage},
  53. ),
  54. report.DaemonSet: formatMetricQueries(`pod_name=~"^{{label}}-[^-]+$",namespace="{{namespace}}"`, []string{docker.MemoryUsage, docker.CPUTotalUsage}),
  55. report.Deployment: podIDHashQueries,
  56. report.StatefulSet: podIDHashQueries,
  57. report.CronJob: podIDHashQueries,
  58. report.Service: {
  59. docker.CPUTotalUsage: `sum(rate(container_cpu_usage_seconds_total{image!="",namespace="{{namespace}}",_weave_pod_name="{{label}}",job="cadvisor",container_name!="POD"}[5m]))`,
  60. docker.MemoryUsage: `sum(rate(container_memory_usage_bytes{image!="",namespace="{{namespace}}",_weave_pod_name="{{label}}",job="cadvisor",container_name!="POD"}[5m]))`,
  61. },
  62. }
  63. )
  64. func formatMetricQueries(filter string, ids []string) map[string]string {
  65. queries := make(map[string]string)
  66. for _, id := range ids {
  67. // All `container_*`metrics are provided by cAdvisor in Kubelets
  68. switch id {
  69. case docker.MemoryUsage:
  70. queries[id] = fmt.Sprintf("sum(container_memory_usage_bytes{%s})", filter)
  71. case docker.CPUTotalUsage:
  72. queries[id] = fmt.Sprintf(
  73. "sum(rate(container_cpu_usage_seconds_total{%s}[1m]))/count(container_cpu_usage_seconds_total{%s})*100",
  74. filter,
  75. filter,
  76. )
  77. case idReceiveBytes:
  78. queries[id] = fmt.Sprintf("sum(rate(container_network_receive_bytes_total{%s}[5m]))", filter)
  79. case idTransmitBytes:
  80. queries[id] = fmt.Sprintf("sum(rate(container_network_transmit_bytes_total{%s}[5m]))", filter)
  81. }
  82. }
  83. return queries
  84. }
  85. // RenderMetricURLs sets respective URLs for metrics in a node summary. Missing metrics
  86. // where we have a query for will be appended as an empty metric (no values or samples).
  87. func RenderMetricURLs(summary NodeSummary, n report.Node, r report.Report, metricsGraphURL string) NodeSummary {
  88. if metricsGraphURL == "" {
  89. return summary
  90. }
  91. var maxprio float64
  92. var ms []report.MetricRow
  93. found := make(map[string]struct{})
  94. // Set URL on existing metrics
  95. for _, metric := range summary.Metrics {
  96. if metric.Priority > maxprio {
  97. maxprio = metric.Priority
  98. }
  99. query := metricQuery(summary, n, r, metric.ID)
  100. ms = append(ms, metric)
  101. if query != "" {
  102. ms[len(ms)-1].URL = metricURL(query, metricsGraphURL)
  103. }
  104. found[metric.ID] = struct{}{}
  105. }
  106. // Append empty metrics for unattached queries
  107. for _, metadata := range shownQueries {
  108. if _, ok := found[metadata.ID]; ok {
  109. continue
  110. }
  111. query := metricQuery(summary, n, r, metadata.ID)
  112. if query == "" {
  113. continue
  114. }
  115. maxprio++
  116. ms = append(ms, report.MetricRow{
  117. ID: metadata.ID,
  118. Label: metadata.Label,
  119. URL: metricURL(query, metricsGraphURL),
  120. Metric: &report.Metric{},
  121. Priority: maxprio,
  122. ValueEmpty: true,
  123. })
  124. }
  125. summary.Metrics = ms
  126. return summary
  127. }
  128. // metricQuery returns the query for the given node and metric.
  129. func metricQuery(summary NodeSummary, n report.Node, r report.Report, metricID string) string {
  130. queries := topologyQueries[n.Topology]
  131. if len(queries) == 0 {
  132. return ""
  133. }
  134. namespace, _ := n.Latest.Lookup(kubernetes.Namespace)
  135. name, _ := n.Latest.Lookup(docker.ContainerName)
  136. replacer := strings.NewReplacer(
  137. "{{label}}", metricLabel(summary, n, r),
  138. "{{namespace}}", namespace,
  139. "{{containerName}}", name,
  140. )
  141. return replacer.Replace(queries[metricID])
  142. }
  143. func metricLabel(summary NodeSummary, n report.Node, r report.Report) string {
  144. label := summary.Label
  145. if n.Topology == report.Service {
  146. deploymentTopology, ok := r.Topology(report.Deployment)
  147. if ok {
  148. deploymentNames := []string{}
  149. for _, pod := range r.Pod.Nodes {
  150. serviceParents, serviceOk := pod.Parents.Lookup(report.Service)
  151. deploymentParents, deploymentOk := pod.Parents.Lookup(report.Deployment)
  152. if serviceOk && deploymentOk && serviceParents.Contains(n.ID) {
  153. for _, id := range deploymentParents {
  154. deploymentNode, ok := deploymentTopology.Nodes[id]
  155. if !ok {
  156. continue
  157. }
  158. if name, ok := deploymentNode.Latest.Lookup(report.KubernetesName); ok {
  159. deploymentNames = append(deploymentNames, name)
  160. }
  161. }
  162. break
  163. }
  164. }
  165. if len(deploymentNames) == 1 {
  166. label = deploymentNames[0]
  167. }
  168. }
  169. }
  170. return label
  171. }
  172. // metricURL builds the URL by embedding it into the configured `metricsGraphURL`.
  173. func metricURL(query, metricsGraphURL string) string {
  174. if strings.Contains(metricsGraphURL, urlQueryVarName) {
  175. return strings.Replace(metricsGraphURL, urlQueryVarName, queryEscape(query), -1)
  176. }
  177. params, err := queryParamsAsJSON(query)
  178. if err != nil {
  179. return ""
  180. }
  181. if metricsGraphURL[len(metricsGraphURL)-1] != '/' {
  182. metricsGraphURL += "/"
  183. }
  184. return metricsGraphURL + queryEscape(params)
  185. }
  186. // queryParamsAsJSON packs the query into a JSON of the format `{"cells":[{"queries":[$query]}]}`.
  187. func queryParamsAsJSON(query string) (string, error) {
  188. type cell struct {
  189. Queries []string `json:"queries"`
  190. }
  191. type queryParams struct {
  192. Cells []cell `json:"cells"`
  193. }
  194. params := &queryParams{[]cell{{[]string{query}}}}
  195. buf := &bytes.Buffer{}
  196. encoder := codec.NewEncoder(buf, &codec.JsonHandle{})
  197. if err := encoder.Encode(params); err != nil {
  198. return "", err
  199. }
  200. return buf.String(), nil
  201. }
  202. // queryEscape uses `%20` instead of `+` to encode whitespaces. Both are
  203. // valid but react-router does not decode `+` properly.
  204. func queryEscape(query string) string {
  205. return url.QueryEscape(strings.Replace(query, " ", "%20", -1))
  206. }