summary.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. package detailed
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. opentracing "github.com/opentracing/opentracing-go"
  7. "github.com/weaveworks/scope/probe/awsecs"
  8. "github.com/weaveworks/scope/probe/docker"
  9. "github.com/weaveworks/scope/probe/kubernetes"
  10. "github.com/weaveworks/scope/probe/overlay"
  11. "github.com/weaveworks/scope/probe/process"
  12. "github.com/weaveworks/scope/render"
  13. "github.com/weaveworks/scope/report"
  14. )
  15. // Shapes that are allowed
  16. const (
  17. ImageNameNone = "<none>"
  18. // Keys we use to render container names
  19. AmazonECSContainerNameLabel = "com.amazonaws.ecs.container-name"
  20. KubernetesContainerNameLabel = "io.kubernetes.container.name"
  21. MarathonAppIDEnv = "MARATHON_APP_ID"
  22. )
  23. // NodeSummaryGroup is a topology-typed group of children for a Node.
  24. type NodeSummaryGroup struct {
  25. ID string `json:"id"`
  26. Label string `json:"label"`
  27. Nodes []NodeSummary `json:"nodes"`
  28. TopologyID string `json:"topologyId"`
  29. Columns []Column `json:"columns"`
  30. }
  31. // Column provides special json serialization for column ids, so they include
  32. // their label for the frontend.
  33. type Column struct {
  34. ID string `json:"id"`
  35. Label string `json:"label"`
  36. DefaultSort bool `json:"defaultSort"`
  37. Datatype string `json:"dataType"`
  38. }
  39. // BasicNodeSummary is basic summary information about a Node,
  40. // sufficient for rendering links to the node.
  41. type BasicNodeSummary struct {
  42. ID string `json:"id"`
  43. Label string `json:"label"`
  44. LabelMinor string `json:"labelMinor"`
  45. Rank string `json:"rank"`
  46. Shape string `json:"shape,omitempty"`
  47. Tag string `json:"tag,omitempty"`
  48. Stack bool `json:"stack,omitempty"`
  49. Pseudo bool `json:"pseudo,omitempty"`
  50. }
  51. // NodeSummary is summary information about a Node.
  52. type NodeSummary struct {
  53. BasicNodeSummary
  54. Metadata []report.MetadataRow `json:"metadata,omitempty"`
  55. Parents []Parent `json:"parents,omitempty"`
  56. Metrics []report.MetricRow `json:"metrics,omitempty"`
  57. Tables []report.Table `json:"tables,omitempty"`
  58. Adjacency report.IDList `json:"adjacency,omitempty"`
  59. }
  60. var renderers = map[string]func(BasicNodeSummary, report.Node) BasicNodeSummary{
  61. render.Pseudo: pseudoNodeSummary,
  62. report.Process: processNodeSummary,
  63. report.Container: containerNodeSummary,
  64. report.ContainerImage: containerImageNodeSummary,
  65. report.Pod: podNodeSummary,
  66. report.Service: podGroupNodeSummary,
  67. report.Deployment: podGroupNodeSummary,
  68. report.DaemonSet: podGroupNodeSummary,
  69. report.StatefulSet: podGroupNodeSummary,
  70. report.CronJob: podGroupNodeSummary,
  71. report.Job: podGroupNodeSummary,
  72. report.ECSTask: ecsTaskNodeSummary,
  73. report.ECSService: ecsServiceNodeSummary,
  74. report.SwarmService: swarmServiceNodeSummary,
  75. report.Host: hostNodeSummary,
  76. report.Overlay: weaveNodeSummary,
  77. report.Endpoint: nil, // Do not render
  78. report.PersistentVolume: persistentVolumeNodeSummary,
  79. report.PersistentVolumeClaim: persistentVolumeClaimNodeSummary,
  80. report.StorageClass: storageClassNodeSummary,
  81. report.VolumeSnapshot: volumeSnapshotNodeSummary,
  82. report.VolumeSnapshotData: volumeSnapshotDataNodeSummary,
  83. }
  84. // For each report.Topology, map to a 'primary' API topology. This can then be used in a variety of places.
  85. var primaryAPITopology = map[string]string{
  86. report.Process: "processes",
  87. report.Container: "containers",
  88. report.ContainerImage: "containers-by-image",
  89. report.Pod: "pods",
  90. report.Deployment: "kube-controllers",
  91. report.DaemonSet: "kube-controllers",
  92. report.StatefulSet: "kube-controllers",
  93. report.CronJob: "kube-controllers",
  94. report.Job: "kube-controllers",
  95. report.Service: "services",
  96. report.ECSTask: "ecs-tasks",
  97. report.ECSService: "ecs-services",
  98. report.SwarmService: "swarm-services",
  99. report.Host: "hosts",
  100. report.PersistentVolume: "pods",
  101. report.PersistentVolumeClaim: "pods",
  102. report.StorageClass: "pods",
  103. report.VolumeSnapshot: "pods",
  104. report.VolumeSnapshotData: "pods",
  105. }
  106. // MakeBasicNodeSummary returns a basic summary of a node, if
  107. // possible. This summary is sufficient for rendering links to the node.
  108. func MakeBasicNodeSummary(r report.Report, n report.Node) (BasicNodeSummary, bool) {
  109. summary := BasicNodeSummary{ // This is unlikely to look very good, but is a reasonable fallback
  110. ID: n.ID,
  111. Label: n.ID,
  112. Shape: report.Triangle,
  113. }
  114. if t, ok := r.Topology(n.Topology); ok {
  115. summary.Shape = t.GetShape()
  116. summary.Tag = t.Tag
  117. }
  118. // Do we have a renderer for the topology?
  119. if renderer, ok := renderers[n.Topology]; ok {
  120. if renderer == nil { // we don't want to render this
  121. return summary, false
  122. }
  123. return renderer(summary, n), true
  124. }
  125. // Is it a group topology?
  126. if strings.HasPrefix(n.Topology, "group:") {
  127. return groupNodeSummary(summary, r, n), true
  128. }
  129. // Is it any known topology?
  130. if _, ok := r.Topology(n.Topology); ok {
  131. // We should never get here, since all known topologies are in
  132. // 'renderers'.
  133. return summary, true
  134. }
  135. // We have no idea how to render this.
  136. return summary, false
  137. }
  138. // MakeNodeSummary summarizes a node, if possible.
  139. func MakeNodeSummary(rc RenderContext, n report.Node) (NodeSummary, bool) {
  140. base, ok := MakeBasicNodeSummary(rc.Report, n)
  141. if !ok {
  142. return NodeSummary{}, false
  143. }
  144. summary := NodeSummary{
  145. BasicNodeSummary: base,
  146. Parents: Parents(rc.Report, n),
  147. Adjacency: n.Adjacency,
  148. }
  149. // Only include metadata, metrics, tables when it's not a group node
  150. if _, ok := n.LookupCounter(n.Topology); !ok {
  151. if topology, ok := rc.Topology(n.Topology); ok {
  152. summary.Metadata = topology.MetadataTemplates.MetadataRows(n)
  153. summary.Metrics = topology.MetricTemplates.MetricRows(n)
  154. summary.Tables = topology.TableTemplates.Tables(n)
  155. }
  156. }
  157. return RenderMetricURLs(summary, n, rc.Report, rc.MetricsGraphURL), true
  158. }
  159. // SummarizeMetrics returns a copy of the NodeSummary where the metrics are
  160. // replaced with their summaries
  161. func (n NodeSummary) SummarizeMetrics() NodeSummary {
  162. summarizedMetrics := make([]report.MetricRow, len(n.Metrics))
  163. for i, m := range n.Metrics {
  164. summarizedMetrics[i] = m.Summary()
  165. }
  166. n.Metrics = summarizedMetrics
  167. return n
  168. }
  169. func pseudoNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  170. pseudoID, _ := render.ParsePseudoNodeID(n.ID)
  171. base.Pseudo = true
  172. base.Rank = pseudoID
  173. switch {
  174. case n.ID == render.IncomingInternetID:
  175. // render as an internet node
  176. base.Label = render.InboundMajor
  177. base.LabelMinor = render.InboundMinor
  178. base.Shape = report.Cloud
  179. case n.ID == render.OutgoingInternetID:
  180. // render as an internet node
  181. base.Label = render.OutboundMajor
  182. base.LabelMinor = render.OutboundMinor
  183. base.Shape = report.Cloud
  184. case strings.HasPrefix(n.ID, render.ServiceNodeIDPrefix):
  185. // render as a known service node
  186. base.Label = n.ID[len(render.ServiceNodeIDPrefix):]
  187. base.LabelMinor = ""
  188. base.Shape = report.Cloud
  189. case strings.HasPrefix(n.ID, render.UncontainedIDPrefix):
  190. // render as an uncontained node
  191. base.Label = render.UncontainedMajor
  192. base.LabelMinor = n.ID[len(render.UncontainedIDPrefix):]
  193. base.Shape = report.Square
  194. base.Stack = true
  195. case strings.HasPrefix(n.ID, render.UnmanagedIDPrefix):
  196. // render as an unmanaged node
  197. base.Label = render.UnmanagedMajor
  198. base.LabelMinor = n.ID[len(render.UnmanagedIDPrefix):]
  199. base.Shape = report.Square
  200. base.Stack = true
  201. default:
  202. // try rendering it as an endpoint
  203. if _, addr, _, ok := report.ParseEndpointNodeID(n.ID); ok {
  204. base.Label = addr
  205. base.Shape = report.Circle
  206. } else {
  207. // last resort
  208. base.Label = pseudoID
  209. }
  210. }
  211. return base
  212. }
  213. func processNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  214. var (
  215. hostID, pid, _ = report.ParseProcessNodeID(n.ID)
  216. processName, _ = n.Latest.Lookup(process.Name)
  217. containerName, _ = n.Latest.Lookup(docker.ContainerName)
  218. )
  219. switch {
  220. case processName != "" && containerName != "":
  221. base.Label = processName
  222. base.LabelMinor = fmt.Sprintf("%s (%s:%s)", hostID, containerName, pid)
  223. base.Rank = processName
  224. case processName != "":
  225. base.Label = processName
  226. base.LabelMinor = fmt.Sprintf("%s (%s)", hostID, pid)
  227. base.Rank = processName
  228. case containerName != "":
  229. base.Label = pid
  230. base.LabelMinor = fmt.Sprintf("%s (%s)", hostID, containerName)
  231. base.Rank = hostID
  232. default:
  233. base.Label = pid
  234. base.LabelMinor = hostID
  235. base.Rank = hostID
  236. }
  237. return base
  238. }
  239. func containerNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  240. var (
  241. containerName = getRenderableContainerName(n)
  242. hostName = report.ExtractHostID(n)
  243. imageName, _ = n.Latest.Lookup(docker.ImageName)
  244. )
  245. base.Label = containerName
  246. base.LabelMinor = hostName
  247. if imageName != "" {
  248. base.Rank = docker.ImageNameWithoutTag(imageName)
  249. } else if hostName != "" {
  250. base.Rank = hostName
  251. } else {
  252. base.Rank = base.Label
  253. }
  254. return base
  255. }
  256. func containerImageNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  257. var (
  258. imageName, _ = n.Latest.Lookup(docker.ImageName)
  259. imageNameWithoutTag = docker.ImageNameWithoutTag(imageName)
  260. )
  261. switch {
  262. case imageNameWithoutTag != "" && imageNameWithoutTag != ImageNameNone:
  263. base.Label = imageNameWithoutTag
  264. case imageName != "" && imageName != ImageNameNone:
  265. base.Label = imageName
  266. default:
  267. // The id can be an image id or an image name. Ideally we'd
  268. // truncate the former but not the latter, but short of
  269. // heuristic regexp match we cannot tell the difference.
  270. base.Label, _ = report.ParseContainerImageNodeID(n.ID)
  271. }
  272. base.LabelMinor = pluralize(n, report.Container, "container", "containers")
  273. base.Rank = base.Label
  274. base.Stack = true
  275. return base
  276. }
  277. func addKubernetesLabelAndRank(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  278. var (
  279. name, _ = n.Latest.Lookup(kubernetes.Name)
  280. namespace, _ = n.Latest.Lookup(kubernetes.Namespace)
  281. )
  282. if name != "" {
  283. base.Label = name
  284. } else {
  285. base.Label, _, _ = report.ParseNodeID(n.ID)
  286. }
  287. base.Rank = namespace + "/" + base.Label
  288. return base
  289. }
  290. func podNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  291. base = addKubernetesLabelAndRank(base, n)
  292. base.LabelMinor = pluralize(n, report.Container, "container", "containers")
  293. return base
  294. }
  295. var podGroupNodeTypeName = map[string]string{
  296. report.Deployment: "Deployment",
  297. report.DaemonSet: "DaemonSet",
  298. report.StatefulSet: "StatefulSet",
  299. report.CronJob: "CronJob",
  300. report.Job: "Job",
  301. }
  302. func podGroupNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  303. base = addKubernetesLabelAndRank(base, n)
  304. base.Stack = true
  305. // NB: pods are the highest aggregation level for which we display
  306. // counts.
  307. count := pluralize(n, report.Pod, "pod", "pods")
  308. if typeName, ok := podGroupNodeTypeName[n.Topology]; ok {
  309. base.LabelMinor = fmt.Sprintf("%s of %s", typeName, count)
  310. } else {
  311. base.LabelMinor = count
  312. }
  313. return base
  314. }
  315. func ecsTaskNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  316. base.Label, _ = n.Latest.Lookup(awsecs.TaskFamily)
  317. if base.Label == "" {
  318. base.Label, _ = report.ParseECSTaskNodeID(n.ID)
  319. }
  320. return base
  321. }
  322. func ecsServiceNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  323. _, base.Label, _ = report.ParseECSServiceNodeID(n.ID)
  324. base.Stack = true
  325. return base
  326. }
  327. func swarmServiceNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  328. base.Label, _ = n.Latest.Lookup(docker.ServiceName)
  329. if base.Label == "" {
  330. base.Label, _ = report.ParseSwarmServiceNodeID(n.ID)
  331. }
  332. return base
  333. }
  334. func hostNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  335. var (
  336. hostname, _ = report.ParseHostNodeID(n.ID)
  337. parts = strings.SplitN(hostname, ".", 2)
  338. )
  339. if len(parts) == 2 {
  340. base.Label, base.LabelMinor, base.Rank = parts[0], parts[1], parts[1]
  341. } else {
  342. base.Label = hostname
  343. }
  344. return base
  345. }
  346. func weaveNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  347. var (
  348. nickname, _ = n.Latest.Lookup(overlay.WeavePeerNickName)
  349. _, peerName = report.ParseOverlayNodeID(n.ID)
  350. )
  351. if nickname != "" {
  352. base.Label = nickname
  353. } else {
  354. base.Label = peerName
  355. }
  356. base.LabelMinor = peerName
  357. return base
  358. }
  359. func persistentVolumeNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  360. base = addKubernetesLabelAndRank(base, n)
  361. return base
  362. }
  363. func persistentVolumeClaimNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  364. base = addKubernetesLabelAndRank(base, n)
  365. return base
  366. }
  367. func storageClassNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  368. base = addKubernetesLabelAndRank(base, n)
  369. return base
  370. }
  371. func volumeSnapshotNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  372. base = addKubernetesLabelAndRank(base, n)
  373. return base
  374. }
  375. func volumeSnapshotDataNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary {
  376. base = addKubernetesLabelAndRank(base, n)
  377. return base
  378. }
  379. // groupNodeSummary renders the summary for a group node. n.Topology is
  380. // expected to be of the form: group:container:hostname
  381. func groupNodeSummary(base BasicNodeSummary, r report.Report, n report.Node) BasicNodeSummary {
  382. base.Label, base.Rank = n.ID, n.ID
  383. if topology, _, ok := render.ParseGroupNodeTopology(n.Topology); ok {
  384. if t, ok := r.Topology(topology); ok {
  385. base.Shape = t.GetShape()
  386. base.Tag = t.Tag
  387. if t.Label != "" {
  388. base.LabelMinor = pluralize(n, topology, t.Label, t.LabelPlural)
  389. }
  390. }
  391. }
  392. base.Stack = true
  393. return base
  394. }
  395. func pluralize(n report.Node, key, singular, plural string) string {
  396. c, ok := n.LookupCounter(key)
  397. if !ok {
  398. c = 0
  399. }
  400. if c == 1 {
  401. return fmt.Sprintf("%d %s", c, singular)
  402. }
  403. return fmt.Sprintf("%d %s", c, plural)
  404. }
  405. type nodeSummariesByID []NodeSummary
  406. func (s nodeSummariesByID) Len() int { return len(s) }
  407. func (s nodeSummariesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
  408. func (s nodeSummariesByID) Less(i, j int) bool { return s[i].ID < s[j].ID }
  409. // NodeSummaries is a set of NodeSummaries indexed by ID.
  410. type NodeSummaries map[string]NodeSummary
  411. // Summaries converts RenderableNodes into a set of NodeSummaries
  412. func Summaries(ctx context.Context, rc RenderContext, rns report.Nodes) NodeSummaries {
  413. span, ctx := opentracing.StartSpanFromContext(ctx, "detailed.Summaries")
  414. defer span.Finish()
  415. result := NodeSummaries{}
  416. for id, node := range rns {
  417. if summary, ok := MakeNodeSummary(rc, node); ok {
  418. for i, m := range summary.Metrics {
  419. summary.Metrics[i] = m.Summary()
  420. }
  421. result[id] = summary
  422. }
  423. }
  424. return result
  425. }
  426. // getRenderableContainerName obtains a user-friendly container name, to render in the UI
  427. func getRenderableContainerName(nmd report.Node) string {
  428. for _, key := range []string{
  429. // Amazon's ecs-agent produces huge Docker container names, destructively
  430. // derived from mangling Container Definition names in Task
  431. // Definitions.
  432. //
  433. // However, the ecs-agent provides a label containing the original Container
  434. // Definition name.
  435. docker.LabelPrefix + AmazonECSContainerNameLabel,
  436. // Kubernetes also mangles its Docker container names and provides a
  437. // label with the original container name. However, note that this label
  438. // is only provided by Kubernetes versions >= 1.2 (see
  439. // https://github.com/kubernetes/kubernetes/pull/17234/ )
  440. docker.LabelPrefix + KubernetesContainerNameLabel,
  441. // Marathon doesn't set any Docker labels and this is the only meaningful
  442. // attribute we can find to make Scope useful without Mesos plugin
  443. docker.EnvPrefix + MarathonAppIDEnv,
  444. docker.ContainerName,
  445. docker.ContainerHostname,
  446. } {
  447. if label, ok := nmd.Latest.Lookup(key); ok {
  448. return label
  449. }
  450. }
  451. containerID, _ := report.ParseContainerNodeID(nmd.ID)
  452. if len(containerID) > 12 {
  453. containerID = containerID[:12]
  454. }
  455. return containerID
  456. }