statsummary.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package scrape
  2. import (
  3. "github.com/opencost/opencost/core/pkg/log"
  4. "github.com/opencost/opencost/core/pkg/nodestats"
  5. "github.com/opencost/opencost/core/pkg/source"
  6. "github.com/opencost/opencost/modules/collector-source/pkg/metric"
  7. stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
  8. )
  9. type StatSummaryScraper struct {
  10. client nodestats.StatSummaryClient
  11. }
  12. func newStatSummaryScraper(client nodestats.StatSummaryClient) Scraper {
  13. return &StatSummaryScraper{
  14. client: client,
  15. }
  16. }
  17. func (s *StatSummaryScraper) Scrape() []metric.Update {
  18. var scrapeResults []metric.Update
  19. nodeStats, err := s.client.GetNodeData()
  20. if err != nil {
  21. log.Errorf("error retrieving node stat data: %s", err.Error())
  22. return scrapeResults
  23. }
  24. // track if a pvc has already been seen when updating KubeletVolumeStatsUsedBytes
  25. seenPVC := map[stats.PVCReference]struct{}{}
  26. for _, stat := range nodeStats {
  27. nodeName := stat.Node.NodeName
  28. if stat.Node.CPU != nil && stat.Node.CPU.UsageCoreNanoSeconds != nil {
  29. scrapeResults = append(scrapeResults, metric.Update{
  30. Name: metric.NodeCPUSecondsTotal,
  31. Labels: map[string]string{
  32. source.KubernetesNodeLabel: nodeName,
  33. source.ModeLabel: "", // TODO
  34. },
  35. Value: float64(*stat.Node.CPU.UsageCoreNanoSeconds) * 1e-9,
  36. })
  37. }
  38. if stat.Node.Fs != nil && stat.Node.Fs.CapacityBytes != nil {
  39. scrapeResults = append(scrapeResults, metric.Update{
  40. Name: metric.NodeFSCapacityBytes,
  41. Labels: map[string]string{
  42. source.InstanceLabel: nodeName,
  43. source.DeviceLabel: "local", // This value has to be populated but isn't important here
  44. },
  45. Value: float64(*stat.Node.Fs.CapacityBytes),
  46. })
  47. }
  48. for _, pod := range stat.Pods {
  49. podName := pod.PodRef.Name
  50. namespace := pod.PodRef.Namespace
  51. podUID := pod.PodRef.UID
  52. if pod.Network != nil {
  53. networkLabels := map[string]string{
  54. source.UIDLabel: podUID,
  55. source.PodLabel: podName,
  56. source.NamespaceLabel: namespace,
  57. }
  58. // The network may contain a list of stats or itself be a single stat, if the list is not present
  59. // scrape the object itself
  60. if pod.Network.Interfaces != nil {
  61. for _, networkStat := range pod.Network.Interfaces {
  62. scrapeNetworkStats(&scrapeResults, networkLabels, networkStat)
  63. }
  64. } else {
  65. scrapeNetworkStats(&scrapeResults, networkLabels, pod.Network.InterfaceStats)
  66. }
  67. }
  68. for _, volumeStats := range pod.VolumeStats {
  69. if volumeStats.PVCRef == nil || volumeStats.UsedBytes == nil {
  70. continue
  71. }
  72. if _, ok := seenPVC[*volumeStats.PVCRef]; ok {
  73. continue
  74. }
  75. scrapeResults = append(scrapeResults, metric.Update{
  76. Name: metric.KubeletVolumeStatsUsedBytes,
  77. Labels: map[string]string{
  78. source.PVCLabel: volumeStats.PVCRef.Name,
  79. source.NamespaceLabel: volumeStats.PVCRef.Namespace,
  80. },
  81. Value: float64(*volumeStats.UsedBytes),
  82. })
  83. seenPVC[*volumeStats.PVCRef] = struct{}{}
  84. }
  85. for _, container := range pod.Containers {
  86. if container.CPU != nil && container.CPU.UsageCoreNanoSeconds != nil {
  87. scrapeResults = append(scrapeResults, metric.Update{
  88. Name: metric.ContainerCPUUsageSecondsTotal,
  89. Labels: map[string]string{
  90. source.ContainerLabel: container.Name,
  91. source.PodLabel: podName,
  92. source.NamespaceLabel: namespace,
  93. source.NodeLabel: nodeName,
  94. source.InstanceLabel: nodeName,
  95. },
  96. Value: float64(*container.CPU.UsageCoreNanoSeconds) * 1e-9,
  97. })
  98. }
  99. if container.Memory != nil && container.Memory.WorkingSetBytes != nil {
  100. scrapeResults = append(scrapeResults, metric.Update{
  101. Name: metric.ContainerMemoryWorkingSetBytes,
  102. Labels: map[string]string{
  103. source.ContainerLabel: container.Name,
  104. source.PodLabel: podName,
  105. source.NamespaceLabel: namespace,
  106. source.NodeLabel: nodeName,
  107. source.InstanceLabel: nodeName,
  108. },
  109. Value: float64(*container.Memory.WorkingSetBytes),
  110. })
  111. }
  112. if container.Rootfs != nil && container.Rootfs.UsedBytes != nil {
  113. scrapeResults = append(scrapeResults, metric.Update{
  114. Name: metric.ContainerFSUsageBytes,
  115. Labels: map[string]string{
  116. source.InstanceLabel: nodeName,
  117. source.DeviceLabel: "local",
  118. },
  119. Value: float64(*container.Rootfs.UsedBytes),
  120. })
  121. }
  122. }
  123. }
  124. }
  125. return scrapeResults
  126. }
  127. func scrapeNetworkStats(scrapeResults *[]metric.Update, labels map[string]string, networkStats stats.InterfaceStats) {
  128. // Skip stats for cni0 which tracks internal cluster traffic
  129. if networkStats.Name == "cni0" {
  130. return
  131. }
  132. if networkStats.RxBytes != nil {
  133. *scrapeResults = append(*scrapeResults, metric.Update{
  134. Name: metric.ContainerNetworkReceiveBytesTotal,
  135. Labels: labels,
  136. Value: float64(*networkStats.RxBytes),
  137. })
  138. }
  139. if networkStats.TxBytes != nil {
  140. *scrapeResults = append(*scrapeResults, metric.Update{
  141. Name: metric.ContainerNetworkTransmitBytesTotal,
  142. Labels: labels,
  143. Value: float64(*networkStats.TxBytes),
  144. })
  145. }
  146. }