statsummary.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. package scrape
  2. import (
  3. "github.com/kubecost/events"
  4. "github.com/opencost/opencost/core/pkg/clustercache"
  5. "github.com/opencost/opencost/core/pkg/log"
  6. "github.com/opencost/opencost/core/pkg/nodestats"
  7. "github.com/opencost/opencost/core/pkg/source"
  8. "github.com/opencost/opencost/modules/collector-source/pkg/event"
  9. "github.com/opencost/opencost/modules/collector-source/pkg/metric"
  10. stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
  11. )
  12. type StatSummaryScraper struct {
  13. client nodestats.StatSummaryClient
  14. clusterCache clustercache.ClusterCache
  15. nodeIndex *persistedIndex[string]
  16. pvcIndex *persistedIndex[pvcKey]
  17. }
  18. func newStatSummaryScraper(client nodestats.StatSummaryClient, clusterCache clustercache.ClusterCache) Scraper {
  19. return &StatSummaryScraper{
  20. client: client,
  21. clusterCache: clusterCache,
  22. nodeIndex: newPersistedIndex[string]("node"),
  23. pvcIndex: newPersistedIndex[pvcKey]("pvc"),
  24. }
  25. }
  26. func (s *StatSummaryScraper) Scrape() []metric.Update {
  27. nodeNameToUID := s.nodeIndex.update(buildNodeIndex(s.clusterCache.GetAllNodes()))
  28. pvcNameToUID := s.pvcIndex.update(buildPVCIndex(s.clusterCache.GetAllPersistentVolumeClaims()))
  29. var scrapeResults []metric.Update
  30. nodeStats, err := s.client.GetNodeData()
  31. // record errors but process successfully retrieved nodes
  32. errs := make([]error, 0)
  33. if err != nil {
  34. if multiErr, ok := err.(interface{ Unwrap() []error }); ok {
  35. errs = multiErr.Unwrap()
  36. } else {
  37. errs = []error{err}
  38. }
  39. log.Errorf("error retrieving node stat data: %s", err.Error())
  40. }
  41. // track if a pvc has already been seen when updating KubeletVolumeStatsUsedBytes
  42. seenPVC := map[stats.PVCReference]struct{}{}
  43. for _, stat := range nodeStats {
  44. nodeName := stat.Node.NodeName
  45. nodeUID := string(nodeNameToUID[nodeName])
  46. if stat.Node.CPU != nil && stat.Node.CPU.UsageCoreNanoSeconds != nil {
  47. scrapeResults = append(scrapeResults, metric.Update{
  48. Name: metric.NodeCPUSecondsTotal,
  49. Labels: map[string]string{
  50. source.KubernetesNodeLabel: nodeName,
  51. source.UIDLabel: nodeUID,
  52. source.ModeLabel: "", // TODO
  53. },
  54. Value: float64(*stat.Node.CPU.UsageCoreNanoSeconds) * 1e-9,
  55. })
  56. }
  57. if stat.Node.Fs != nil && stat.Node.Fs.CapacityBytes != nil {
  58. scrapeResults = append(scrapeResults, metric.Update{
  59. Name: metric.NodeFSCapacityBytes,
  60. Labels: map[string]string{
  61. source.InstanceLabel: nodeName,
  62. source.UIDLabel: nodeUID,
  63. source.DeviceLabel: "local", // This value has to be populated but isn't important here
  64. },
  65. Value: float64(*stat.Node.Fs.CapacityBytes),
  66. })
  67. }
  68. for _, pod := range stat.Pods {
  69. podName := pod.PodRef.Name
  70. namespace := pod.PodRef.Namespace
  71. podUID := pod.PodRef.UID
  72. if pod.Network != nil {
  73. networkLabels := map[string]string{
  74. source.UIDLabel: podUID,
  75. source.NodeUIDLabel: nodeUID,
  76. source.PodLabel: podName,
  77. source.NamespaceLabel: namespace,
  78. }
  79. // The network may contain a list of stats or itself be a single stat, if the list is not present
  80. // scrape the object itself
  81. if pod.Network.Interfaces != nil {
  82. for _, networkStat := range pod.Network.Interfaces {
  83. scrapeNetworkStats(&scrapeResults, networkLabels, networkStat)
  84. }
  85. } else {
  86. scrapeNetworkStats(&scrapeResults, networkLabels, pod.Network.InterfaceStats)
  87. }
  88. }
  89. for _, volumeStats := range pod.VolumeStats {
  90. if volumeStats.PVCRef == nil || volumeStats.UsedBytes == nil {
  91. continue
  92. }
  93. if _, ok := seenPVC[*volumeStats.PVCRef]; ok {
  94. continue
  95. }
  96. pvcUID := string(pvcNameToUID[pvcKey{name: volumeStats.PVCRef.Name, namespace: volumeStats.PVCRef.Namespace}])
  97. scrapeResults = append(scrapeResults, metric.Update{
  98. Name: metric.KubeletVolumeStatsUsedBytes,
  99. Labels: map[string]string{
  100. source.PVCLabel: volumeStats.PVCRef.Name,
  101. source.NamespaceLabel: volumeStats.PVCRef.Namespace,
  102. source.UIDLabel: podUID,
  103. source.NodeUIDLabel: nodeUID,
  104. source.PVCUIDLabel: pvcUID,
  105. },
  106. Value: float64(*volumeStats.UsedBytes),
  107. })
  108. seenPVC[*volumeStats.PVCRef] = struct{}{}
  109. }
  110. for _, container := range pod.Containers {
  111. if container.CPU != nil && container.CPU.UsageCoreNanoSeconds != nil {
  112. scrapeResults = append(scrapeResults, metric.Update{
  113. Name: metric.ContainerCPUUsageSecondsTotal,
  114. Labels: map[string]string{
  115. source.ContainerLabel: container.Name,
  116. source.PodLabel: podName,
  117. source.NamespaceLabel: namespace,
  118. source.NodeLabel: nodeName,
  119. source.InstanceLabel: nodeName,
  120. source.UIDLabel: podUID,
  121. source.NodeUIDLabel: nodeUID,
  122. },
  123. Value: float64(*container.CPU.UsageCoreNanoSeconds) * 1e-9,
  124. })
  125. }
  126. if container.Memory != nil && container.Memory.WorkingSetBytes != nil {
  127. scrapeResults = append(scrapeResults, metric.Update{
  128. Name: metric.ContainerMemoryWorkingSetBytes,
  129. Labels: map[string]string{
  130. source.ContainerLabel: container.Name,
  131. source.PodLabel: podName,
  132. source.NamespaceLabel: namespace,
  133. source.NodeLabel: nodeName,
  134. source.InstanceLabel: nodeName,
  135. source.UIDLabel: podUID,
  136. source.NodeUIDLabel: nodeUID,
  137. },
  138. Value: float64(*container.Memory.WorkingSetBytes),
  139. })
  140. }
  141. if container.Rootfs != nil && container.Rootfs.UsedBytes != nil {
  142. scrapeResults = append(scrapeResults, metric.Update{
  143. Name: metric.ContainerFSUsageBytes,
  144. Labels: map[string]string{
  145. source.InstanceLabel: nodeName,
  146. source.DeviceLabel: "local",
  147. source.UIDLabel: podUID,
  148. source.NodeUIDLabel: nodeUID,
  149. source.ContainerLabel: container.Name,
  150. },
  151. Value: float64(*container.Rootfs.UsedBytes),
  152. })
  153. }
  154. }
  155. }
  156. }
  157. events.Dispatch(event.ScrapeEvent{
  158. ScraperName: event.NodeStatsScraperName,
  159. Targets: len(nodeStats) + len(errs),
  160. Errors: errs,
  161. })
  162. return scrapeResults
  163. }
  164. func scrapeNetworkStats(scrapeResults *[]metric.Update, labels map[string]string, networkStats stats.InterfaceStats) {
  165. // Skip stats for cni0 which tracks internal cluster traffic
  166. if networkStats.Name == "cni0" {
  167. return
  168. }
  169. if networkStats.RxBytes != nil {
  170. *scrapeResults = append(*scrapeResults, metric.Update{
  171. Name: metric.ContainerNetworkReceiveBytesTotal,
  172. Labels: labels,
  173. Value: float64(*networkStats.RxBytes),
  174. })
  175. }
  176. if networkStats.TxBytes != nil {
  177. *scrapeResults = append(*scrapeResults, metric.Update{
  178. Name: metric.ContainerNetworkTransmitBytesTotal,
  179. Labels: labels,
  180. Value: float64(*networkStats.TxBytes),
  181. })
  182. }
  183. }