metrics.go 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. package costmodel
  2. import (
  3. "maps"
  4. "math"
  5. "strconv"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/opencost/opencost/core/pkg/clustercache"
  10. "github.com/opencost/opencost/core/pkg/clusters"
  11. coreenv "github.com/opencost/opencost/core/pkg/env"
  12. "github.com/opencost/opencost/core/pkg/errors"
  13. "github.com/opencost/opencost/core/pkg/log"
  14. "github.com/opencost/opencost/core/pkg/source"
  15. "github.com/opencost/opencost/core/pkg/util"
  16. "github.com/opencost/opencost/core/pkg/util/atomic"
  17. "github.com/opencost/opencost/core/pkg/util/promutil"
  18. "github.com/opencost/opencost/pkg/cloud/models"
  19. "github.com/opencost/opencost/pkg/env"
  20. "github.com/opencost/opencost/pkg/metrics"
  21. promclient "github.com/prometheus/client_golang/api"
  22. "github.com/prometheus/client_golang/prometheus"
  23. dto "github.com/prometheus/client_model/go"
  24. v1 "k8s.io/api/core/v1"
  25. )
  26. //--------------------------------------------------------------------------
  27. // ClusterInfoCollector
  28. //--------------------------------------------------------------------------
  29. // ClusterInfoCollector is a prometheus collector that generates ClusterInfoMetrics
  30. type ClusterInfoCollector struct {
  31. ClusterInfo clusters.ClusterInfoProvider
  32. metricsConfig metrics.MetricsConfig
  33. }
  34. // Describe sends the super-set of all possible descriptors of metrics
  35. // collected by this Collector.
  36. func (cic ClusterInfoCollector) Describe(ch chan<- *prometheus.Desc) {
  37. disabledMetrics := cic.metricsConfig.GetDisabledMetricsMap()
  38. if _, disabled := disabledMetrics["kubecost_cluster_info"]; disabled {
  39. return
  40. }
  41. ch <- prometheus.NewDesc("kubecost_cluster_info", "Kubecost Cluster Info", []string{}, nil)
  42. }
  43. // Collect is called by the Prometheus registry when collecting metrics.
  44. func (cic ClusterInfoCollector) Collect(ch chan<- prometheus.Metric) {
  45. disabledMetrics := cic.metricsConfig.GetDisabledMetricsMap()
  46. if _, disabled := disabledMetrics["kubecost_cluster_info"]; disabled {
  47. return
  48. }
  49. clusterInfo := cic.ClusterInfo.GetClusterInfo()
  50. labels := promutil.MapToLabels(clusterInfo)
  51. m := newClusterInfoMetric("kubecost_cluster_info", labels)
  52. ch <- m
  53. }
  54. //--------------------------------------------------------------------------
  55. // ClusterInfoMetric
  56. //--------------------------------------------------------------------------
  57. // ClusterInfoMetric is a prometheus.Metric used to encode the local cluster info
  58. type ClusterInfoMetric struct {
  59. fqName string
  60. help string
  61. labels map[string]string
  62. }
  63. // Creates a new ClusterInfoMetric, implementation of prometheus.Metric
  64. func newClusterInfoMetric(fqName string, labels map[string]string) ClusterInfoMetric {
  65. return ClusterInfoMetric{
  66. fqName: fqName,
  67. labels: labels,
  68. help: "kubecost_cluster_info ClusterInfo",
  69. }
  70. }
  71. // Desc returns the descriptor for the Metric. This method idempotently
  72. // returns the same descriptor throughout the lifetime of the Metric.
  73. func (cim ClusterInfoMetric) Desc() *prometheus.Desc {
  74. l := prometheus.Labels{}
  75. return prometheus.NewDesc(cim.fqName, cim.help, promutil.LabelNamesFrom(cim.labels), l)
  76. }
  77. // Write encodes the Metric into a "Metric" Protocol Buffer data
  78. // transmission object.
  79. func (cim ClusterInfoMetric) Write(m *dto.Metric) error {
  80. h := float64(1)
  81. m.Gauge = &dto.Gauge{
  82. Value: &h,
  83. }
  84. var labels []*dto.LabelPair
  85. for k, v := range cim.labels {
  86. labels = append(labels, &dto.LabelPair{
  87. Name: toStringPtr(k),
  88. Value: toStringPtr(v),
  89. })
  90. }
  91. m.Label = labels
  92. return nil
  93. }
  94. // returns a pointer to the string provided
  95. func toStringPtr(s string) *string { return &s }
  96. //--------------------------------------------------------------------------
  97. // Cost Model Metrics Initialization
  98. //--------------------------------------------------------------------------
  99. // Only allow the metrics to be instantiated and registered once
  100. var metricsInit sync.Once
  101. var (
  102. cpuGv *prometheus.GaugeVec
  103. ramGv *prometheus.GaugeVec
  104. gpuGv *prometheus.GaugeVec
  105. gpuCountGv *prometheus.GaugeVec
  106. pvGv *prometheus.GaugeVec
  107. spotGv *prometheus.GaugeVec
  108. totalGv *prometheus.GaugeVec
  109. ramAllocGv *prometheus.GaugeVec
  110. cpuAllocGv *prometheus.GaugeVec
  111. gpuAllocGv *prometheus.GaugeVec
  112. pvAllocGv *prometheus.GaugeVec
  113. networkZoneEgressCostG prometheus.Gauge
  114. networkRegionEgressCostG prometheus.Gauge
  115. networkInternetEgressCostG prometheus.Gauge
  116. networkNatGatewayEgressCostG prometheus.Gauge
  117. networkNatGatewayIngressCostG prometheus.Gauge
  118. clusterManagementCostGv *prometheus.GaugeVec
  119. lbCostGv *prometheus.GaugeVec
  120. )
  121. // initCostModelMetrics uses a sync.Once to ensure that these metrics are only created once
  122. func initCostModelMetrics(clusterInfo clusters.ClusterInfoProvider, metricsConfig *metrics.MetricsConfig) {
  123. disabledMetrics := metricsConfig.GetDisabledMetricsMap()
  124. var toRegisterGV []*prometheus.GaugeVec
  125. var toRegisterGauge []prometheus.Gauge
  126. metricsInit.Do(func() {
  127. cpuGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  128. Name: "node_cpu_hourly_cost",
  129. Help: "node_cpu_hourly_cost hourly cost for each cpu on this node",
  130. }, []string{"instance", "node", "instance_type", "region", "provider_id", "arch", "uid"})
  131. if _, disabled := disabledMetrics["node_cpu_hourly_cost"]; !disabled {
  132. toRegisterGV = append(toRegisterGV, cpuGv)
  133. }
  134. ramGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  135. Name: "node_ram_hourly_cost",
  136. Help: "node_ram_hourly_cost hourly cost for each gb of ram on this node",
  137. }, []string{"instance", "node", "instance_type", "region", "provider_id", "arch", "uid"})
  138. if _, disabled := disabledMetrics["node_ram_hourly_cost"]; !disabled {
  139. toRegisterGV = append(toRegisterGV, ramGv)
  140. }
  141. gpuGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  142. Name: "node_gpu_hourly_cost",
  143. Help: "node_gpu_hourly_cost hourly cost for each gpu on this node",
  144. }, []string{"instance", "node", "instance_type", "region", "provider_id", "arch", "uid"})
  145. if _, disabled := disabledMetrics["node_gpu_hourly_cost"]; !disabled {
  146. toRegisterGV = append(toRegisterGV, gpuGv)
  147. }
  148. gpuCountGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  149. Name: "node_gpu_count",
  150. Help: "node_gpu_count count of gpu on this node",
  151. }, []string{"instance", "node", "instance_type", "region", "provider_id", "arch", "uid"})
  152. if _, disabled := disabledMetrics["node_gpu_count"]; !disabled {
  153. toRegisterGV = append(toRegisterGV, gpuCountGv)
  154. }
  155. pvGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  156. Name: "pv_hourly_cost",
  157. Help: "pv_hourly_cost Cost per GB per hour on a persistent disk",
  158. }, []string{"volumename", "persistentvolume", "provider_id", "uid"})
  159. if _, disabled := disabledMetrics["pv_hourly_cost"]; !disabled {
  160. toRegisterGV = append(toRegisterGV, pvGv)
  161. }
  162. spotGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  163. Name: "kubecost_node_is_spot",
  164. Help: "kubecost_node_is_spot Cloud provider info about node preemptibility",
  165. }, []string{"instance", "node", "instance_type", "region", "provider_id", "arch", "uid"})
  166. if _, disabled := disabledMetrics["kubecost_node_is_spot"]; !disabled {
  167. toRegisterGV = append(toRegisterGV, spotGv)
  168. }
  169. totalGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  170. Name: "node_total_hourly_cost",
  171. Help: "node_total_hourly_cost Total node cost per hour",
  172. }, []string{"instance", "node", "instance_type", "region", "provider_id", "arch", "uid"})
  173. if _, disabled := disabledMetrics["node_total_hourly_cost"]; !disabled {
  174. toRegisterGV = append(toRegisterGV, totalGv)
  175. }
  176. ramAllocGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  177. Name: "container_memory_allocation_bytes",
  178. Help: "container_memory_allocation_bytes Bytes of RAM used",
  179. }, []string{"namespace", "pod", "container", "instance", "node", "uid"})
  180. if _, disabled := disabledMetrics["container_memory_allocation_bytes"]; !disabled {
  181. toRegisterGV = append(toRegisterGV, ramAllocGv)
  182. }
  183. cpuAllocGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  184. Name: "container_cpu_allocation",
  185. Help: "container_cpu_allocation Percent of a single CPU used in a minute",
  186. }, []string{"namespace", "pod", "container", "instance", "node", "uid"})
  187. if _, disabled := disabledMetrics["container_cpu_allocation"]; !disabled {
  188. toRegisterGV = append(toRegisterGV, cpuAllocGv)
  189. }
  190. gpuAllocGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  191. Name: "container_gpu_allocation",
  192. Help: "container_gpu_allocation GPU used",
  193. }, []string{"namespace", "pod", "container", "instance", "node", "uid"})
  194. if _, disabled := disabledMetrics["container_gpu_allocation"]; !disabled {
  195. toRegisterGV = append(toRegisterGV, gpuAllocGv)
  196. }
  197. pvAllocGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  198. Name: "pod_pvc_allocation",
  199. Help: "pod_pvc_allocation Bytes used by a PVC attached to a pod",
  200. }, []string{"namespace", "pod", "persistentvolumeclaim", "persistentvolume", "uid"})
  201. if _, disabled := disabledMetrics["pod_pvc_allocation"]; !disabled {
  202. toRegisterGV = append(toRegisterGV, pvAllocGv)
  203. }
  204. networkZoneEgressCostG = prometheus.NewGauge(prometheus.GaugeOpts{
  205. Name: "kubecost_network_zone_egress_cost",
  206. Help: "kubecost_network_zone_egress_cost Total cost per GB egress across zones",
  207. })
  208. if _, disabled := disabledMetrics["kubecost_network_zone_egress_cost"]; !disabled {
  209. toRegisterGauge = append(toRegisterGauge, networkZoneEgressCostG)
  210. }
  211. networkRegionEgressCostG = prometheus.NewGauge(prometheus.GaugeOpts{
  212. Name: "kubecost_network_region_egress_cost",
  213. Help: "kubecost_network_region_egress_cost Total cost per GB egress across regions",
  214. })
  215. if _, disabled := disabledMetrics["kubecost_network_region_egress_cost"]; !disabled {
  216. toRegisterGauge = append(toRegisterGauge, networkRegionEgressCostG)
  217. }
  218. networkInternetEgressCostG = prometheus.NewGauge(prometheus.GaugeOpts{
  219. Name: "kubecost_network_internet_egress_cost",
  220. Help: "kubecost_network_internet_egress_cost Total cost per GB of internet egress.",
  221. })
  222. if _, disabled := disabledMetrics["kubecost_network_internet_egress_cost"]; !disabled {
  223. toRegisterGauge = append(toRegisterGauge, networkInternetEgressCostG)
  224. }
  225. networkNatGatewayEgressCostG = prometheus.NewGauge(prometheus.GaugeOpts{
  226. Name: "kubecost_network_nat_gateway_egress_cost",
  227. Help: "kubecost_network_nat_gateway_egress_cost Total cost per GB of nat gateway egress.",
  228. })
  229. if _, disabled := disabledMetrics["kubecost_network_nat_gateway_egress_cost"]; !disabled {
  230. toRegisterGauge = append(toRegisterGauge, networkNatGatewayEgressCostG)
  231. }
  232. networkNatGatewayIngressCostG = prometheus.NewGauge(prometheus.GaugeOpts{
  233. Name: "kubecost_network_nat_gateway_ingress_cost",
  234. Help: "kubecost_network_nat_gateway_ingress_cost Total cost per GB of nat gateway ingress.",
  235. })
  236. if _, disabled := disabledMetrics["kubecost_network_nat_gateway_ingress_cost"]; !disabled {
  237. toRegisterGauge = append(toRegisterGauge, networkNatGatewayIngressCostG)
  238. }
  239. clusterManagementCostGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
  240. Name: "kubecost_cluster_management_cost",
  241. Help: "kubecost_cluster_management_cost Hourly cost paid as a cluster management fee.",
  242. }, []string{"provisioner_name"})
  243. if _, disabled := disabledMetrics["kubecost_cluster_management_cost"]; !disabled {
  244. toRegisterGV = append(toRegisterGV, clusterManagementCostGv)
  245. }
  246. lbCostGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{ // no differentiation between ELB and ALB right now
  247. Name: "kubecost_load_balancer_cost",
  248. Help: "kubecost_load_balancer_cost Hourly cost of load balancer",
  249. }, []string{"ingress_ip", "namespace", "service_name", "uid"}) // assumes one ingress IP per load balancer
  250. if _, disabled := disabledMetrics["kubecost_load_balancer_cost"]; !disabled {
  251. toRegisterGV = append(toRegisterGV, lbCostGv)
  252. }
  253. // Register cost-model metrics for emission
  254. for _, gv := range toRegisterGV {
  255. prometheus.MustRegister(gv)
  256. }
  257. for _, g := range toRegisterGauge {
  258. prometheus.MustRegister(g)
  259. }
  260. // General Metric Collectors
  261. prometheus.MustRegister(ClusterInfoCollector{
  262. ClusterInfo: clusterInfo,
  263. metricsConfig: *metricsConfig,
  264. })
  265. })
  266. }
  267. //--------------------------------------------------------------------------
  268. // CostModelMetricsEmitter
  269. //--------------------------------------------------------------------------
  270. // CostModelMetricsEmitter emits all cost-model specific metrics calculated by
  271. // the CostModel.ComputeCostData() method.
  272. type CostModelMetricsEmitter struct {
  273. PrometheusClient promclient.Client
  274. KubeClusterCache clustercache.ClusterCache
  275. CloudProvider models.Provider
  276. Model *CostModel
  277. // Metrics
  278. CPUPriceRecorder *prometheus.GaugeVec
  279. RAMPriceRecorder *prometheus.GaugeVec
  280. PersistentVolumePriceRecorder *prometheus.GaugeVec
  281. GPUPriceRecorder *prometheus.GaugeVec
  282. GPUCountRecorder *prometheus.GaugeVec
  283. PVAllocationRecorder *prometheus.GaugeVec
  284. NodeSpotRecorder *prometheus.GaugeVec
  285. NodeTotalPriceRecorder *prometheus.GaugeVec
  286. RAMAllocationRecorder *prometheus.GaugeVec
  287. CPUAllocationRecorder *prometheus.GaugeVec
  288. GPUAllocationRecorder *prometheus.GaugeVec
  289. ClusterManagementCostRecorder *prometheus.GaugeVec
  290. LBCostRecorder *prometheus.GaugeVec
  291. NetworkZoneEgressRecorder prometheus.Gauge
  292. NetworkRegionEgressRecorder prometheus.Gauge
  293. NetworkInternetEgressRecorder prometheus.Gauge
  294. NetworkNatGatewayEgressRecorder prometheus.Gauge
  295. NetworkNatGatewayIngressRecorder prometheus.Gauge
  296. // Concurrent Flow Control - Manages the run state of the metric emitter
  297. runState atomic.AtomicRunState
  298. }
  299. // NewCostModelMetricsEmitter creates a new cost-model metrics emitter. Use Start() to begin metric emission.
  300. func NewCostModelMetricsEmitter(clusterCache clustercache.ClusterCache, provider models.Provider, clusterInfo clusters.ClusterInfoProvider, model *CostModel) *CostModelMetricsEmitter {
  301. // Get metric configurations, if any
  302. metricsConfig, err := metrics.GetMetricsConfig()
  303. if err != nil {
  304. log.Infof("Failed to get metrics config before init: %s", err)
  305. }
  306. if len(metricsConfig.DisabledMetrics) > 0 {
  307. log.Infof("Starting metrics init with disabled metrics: %v", metricsConfig.DisabledMetrics)
  308. }
  309. // init will only actually execute once to register the custom gauges
  310. initCostModelMetrics(clusterInfo, metricsConfig)
  311. metrics.InitKubeMetrics(clusterInfo, clusterCache, metricsConfig, &metrics.KubeMetricsOpts{
  312. EmitKubecostControllerMetrics: true,
  313. EmitNamespaceAnnotations: coreenv.IsEmitNamespaceAnnotationsMetric(),
  314. EmitPodAnnotations: coreenv.IsEmitPodAnnotationsMetric(),
  315. EmitKubeStateMetrics: coreenv.IsEmitKsmV1Metrics(),
  316. EmitKubeStateMetricsV1Only: coreenv.IsEmitKsmV1MetricsOnly(),
  317. EmitDeprecatedMetrics: coreenv.IsEmitDeprecatedMetrics(),
  318. })
  319. metrics.InitOpencostTelemetry(metricsConfig)
  320. if model != nil {
  321. metrics.InitWALMetrics(model.DataSource, metricsConfig)
  322. }
  323. return &CostModelMetricsEmitter{
  324. KubeClusterCache: clusterCache,
  325. CloudProvider: provider,
  326. Model: model,
  327. CPUPriceRecorder: cpuGv,
  328. RAMPriceRecorder: ramGv,
  329. GPUPriceRecorder: gpuGv,
  330. GPUCountRecorder: gpuCountGv,
  331. PersistentVolumePriceRecorder: pvGv,
  332. NodeSpotRecorder: spotGv,
  333. NodeTotalPriceRecorder: totalGv,
  334. RAMAllocationRecorder: ramAllocGv,
  335. CPUAllocationRecorder: cpuAllocGv,
  336. GPUAllocationRecorder: gpuAllocGv,
  337. PVAllocationRecorder: pvAllocGv,
  338. NetworkZoneEgressRecorder: networkZoneEgressCostG,
  339. NetworkRegionEgressRecorder: networkRegionEgressCostG,
  340. NetworkInternetEgressRecorder: networkInternetEgressCostG,
  341. NetworkNatGatewayEgressRecorder: networkNatGatewayEgressCostG,
  342. NetworkNatGatewayIngressRecorder: networkNatGatewayIngressCostG,
  343. ClusterManagementCostRecorder: clusterManagementCostGv,
  344. LBCostRecorder: lbCostGv,
  345. }
  346. }
  347. // IsRunning returns true if metric recording is running.
  348. func (cmme *CostModelMetricsEmitter) IsRunning() bool {
  349. return cmme.runState.IsRunning()
  350. }
  351. // NodeCostAverages tracks a running average of a node's cost attributes.
  352. // The averages are used to detect and discard spurrious outliers.
  353. type NodeCostAverages struct {
  354. CpuCostAverage float64
  355. RamCostAverage float64
  356. NumCpuDataPoints float64
  357. NumRamDataPoints float64
  358. }
  359. // StartCostModelMetricRecording starts the go routine that emits metrics used to determine
  360. // cluster costs.
  361. func (cmme *CostModelMetricsEmitter) Start() bool {
  362. // wait for a reset to prevent a race between start and stop calls
  363. cmme.runState.WaitForReset()
  364. // Check to see if we're already recording, and atomically advance the run state to start if we're not
  365. if !cmme.runState.Start() {
  366. log.Errorf("Attempted to start cost model metric recording when it's already running.")
  367. return false
  368. }
  369. go func() {
  370. defer errors.HandlePanic()
  371. containerSeen := make(map[string]bool)
  372. nodeSeen := make(map[string]bool)
  373. loadBalancerSeen := make(map[string]bool)
  374. pvSeen := make(map[string]bool)
  375. pvcSeen := make(map[string]bool)
  376. nodeCostAverages := make(map[string]NodeCostAverages)
  377. getKeyFromLabelStrings := func(labels ...string) string {
  378. return strings.Join(labels, ",")
  379. }
  380. getLabelStringsFromKey := func(key string) []string {
  381. return strings.Split(key, ",")
  382. }
  383. var defaultRegion string = ""
  384. nodeList := cmme.KubeClusterCache.GetAllNodes()
  385. if len(nodeList) > 0 {
  386. var ok bool
  387. defaultRegion, ok = util.GetRegion(nodeList[0].Labels)
  388. if !ok {
  389. log.DedupedWarningf(5, "Failed to read default region from labels on node %s", nodeList[0].Name)
  390. }
  391. }
  392. for {
  393. log.Debugf("Recording prices...")
  394. podlist := cmme.KubeClusterCache.GetAllPods()
  395. podStatus := make(map[string]v1.PodPhase)
  396. podUIDs := make(map[string]string)
  397. for _, pod := range podlist {
  398. podStatus[pod.Name] = pod.Status.Phase
  399. podUIDs[pod.Name] = string(pod.UID)
  400. }
  401. // Create node UID lookup map
  402. nodeList := cmme.KubeClusterCache.GetAllNodes()
  403. nodeUIDs := make(map[string]string)
  404. for _, node := range nodeList {
  405. nodeUIDs[node.Name] = string(node.UID)
  406. }
  407. // Create PV UID lookup map
  408. pvList := cmme.KubeClusterCache.GetAllPersistentVolumes()
  409. pvUIDs := make(map[string]string)
  410. for _, pv := range pvList {
  411. pvUIDs[pv.Name] = string(pv.UID)
  412. }
  413. // Create service UID lookup map
  414. serviceList := cmme.KubeClusterCache.GetAllServices()
  415. serviceUIDs := make(map[string]string)
  416. for _, service := range serviceList {
  417. serviceKey := service.Namespace + "/" + service.Name
  418. serviceUIDs[serviceKey] = string(service.UID)
  419. }
  420. cfg, _ := cmme.CloudProvider.GetConfig()
  421. provisioner, clusterManagementCost, err := cmme.CloudProvider.ClusterManagementPricing()
  422. if err != nil {
  423. log.Errorf("Error getting cluster management cost %s", err.Error())
  424. }
  425. cmme.ClusterManagementCostRecorder.WithLabelValues(provisioner).Set(clusterManagementCost)
  426. // Record network pricing at global scope
  427. networkCosts, err := cmme.CloudProvider.NetworkPricing()
  428. if err != nil {
  429. log.Debugf("Failed to retrieve network costs: %s", err.Error())
  430. } else {
  431. cmme.NetworkZoneEgressRecorder.Set(networkCosts.ZoneNetworkEgressCost)
  432. cmme.NetworkRegionEgressRecorder.Set(networkCosts.RegionNetworkEgressCost)
  433. cmme.NetworkInternetEgressRecorder.Set(networkCosts.InternetNetworkEgressCost)
  434. cmme.NetworkNatGatewayEgressRecorder.Set(networkCosts.NatGatewayEgressCost)
  435. cmme.NetworkNatGatewayIngressRecorder.Set(networkCosts.NatGatewayIngressCost)
  436. }
  437. end := time.Now()
  438. queryWindow := env.GetMetricsEmitterQueryWindow()
  439. start := end.Add(-queryWindow)
  440. data, err := cmme.Model.ComputeCostData(start, end)
  441. if err != nil {
  442. // For an error collection, we'll just log the length of the errors (ComputeCostData already logs the
  443. // actual errors)
  444. if source.IsErrorCollection(err) {
  445. if ec, ok := err.(source.QueryErrorCollection); ok {
  446. log.Errorf("Error in price recording: %d errors occurred", len(ec.Errors()))
  447. }
  448. } else {
  449. log.Errorf("Error in price recording: %s", err)
  450. }
  451. // zero the for loop so the time.Sleep will still work
  452. data = map[string]*CostData{}
  453. }
  454. nodes, err := cmme.Model.GetNodeCost()
  455. if err != nil {
  456. log.Warnf("Error getting Node cost: %s", err)
  457. }
  458. for nodeName, node := range nodes {
  459. // Get node UID first
  460. nodeUID := nodeUIDs[nodeName]
  461. // Emit costs, guarding against NaN inputs for custom pricing.
  462. cpuCost, _ := strconv.ParseFloat(node.VCPUCost, 64)
  463. if math.IsNaN(cpuCost) || math.IsInf(cpuCost, 0) {
  464. cpuCost, _ = strconv.ParseFloat(cfg.CPU, 64)
  465. if math.IsNaN(cpuCost) || math.IsInf(cpuCost, 0) {
  466. cpuCost = 0
  467. }
  468. }
  469. cpu, _ := strconv.ParseFloat(node.VCPU, 64)
  470. if math.IsNaN(cpu) || math.IsInf(cpu, 0) {
  471. cpu = 1 // Assume 1 CPU
  472. }
  473. ramCost, _ := strconv.ParseFloat(node.RAMCost, 64)
  474. if math.IsNaN(ramCost) || math.IsInf(ramCost, 0) {
  475. ramCost, _ = strconv.ParseFloat(cfg.RAM, 64)
  476. if math.IsNaN(ramCost) || math.IsInf(ramCost, 0) {
  477. ramCost = 0
  478. }
  479. }
  480. ram, _ := strconv.ParseFloat(node.RAMBytes, 64)
  481. if math.IsNaN(ram) || math.IsInf(ram, 0) {
  482. ram = 0
  483. }
  484. gpu, _ := strconv.ParseFloat(node.GPU, 64)
  485. if math.IsNaN(gpu) || math.IsInf(gpu, 0) {
  486. gpu = 0
  487. }
  488. gpuCost, _ := strconv.ParseFloat(node.GPUCost, 64)
  489. if math.IsNaN(gpuCost) || math.IsInf(gpuCost, 0) {
  490. gpuCost, _ = strconv.ParseFloat(cfg.GPU, 64)
  491. if math.IsNaN(gpuCost) || math.IsInf(gpuCost, 0) {
  492. gpuCost = 0
  493. }
  494. }
  495. nodeType := node.InstanceType
  496. nodeRegion := node.Region
  497. totalCost := cpu*cpuCost + ramCost*(ram/1024/1024/1024) + gpu*gpuCost
  498. labelKey := getKeyFromLabelStrings(nodeName, nodeName, nodeType, nodeRegion, node.ProviderID, node.ArchType, nodeUID)
  499. avgCosts, ok := nodeCostAverages[labelKey]
  500. // initialize average cost tracking for this node if there is none
  501. if !ok {
  502. avgCosts = NodeCostAverages{
  503. CpuCostAverage: cpuCost,
  504. RamCostAverage: ramCost,
  505. NumCpuDataPoints: 1,
  506. NumRamDataPoints: 1,
  507. }
  508. nodeCostAverages[labelKey] = avgCosts
  509. }
  510. cmme.GPUCountRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion, node.ProviderID, node.ArchType, nodeUID).Set(gpu)
  511. cmme.GPUPriceRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion, node.ProviderID, node.ArchType, nodeUID).Set(gpuCost)
  512. const outlierFactor float64 = 30
  513. // don't record cpuCost, ramCost, or gpuCost in the case of wild outliers
  514. // k8s api sometimes causes cost spikes as described here:
  515. // https://github.com/opencost/opencost/issues/927
  516. cpuOutlierCutoff := outlierFactor * avgCosts.CpuCostAverage
  517. if cpuCost < cpuOutlierCutoff {
  518. cmme.CPUPriceRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion, node.ProviderID, node.ArchType, nodeUID).Set(cpuCost)
  519. avgCosts.CpuCostAverage = (avgCosts.CpuCostAverage*avgCosts.NumCpuDataPoints + cpuCost) / (avgCosts.NumCpuDataPoints + 1)
  520. avgCosts.NumCpuDataPoints += 1
  521. } else {
  522. log.Debugf("CPU cost outlier detected; skipping data point: %s had %f as cost, which is above %f.", nodeName, cpuCost, cpuOutlierCutoff)
  523. }
  524. ramOutlierCutoff := outlierFactor * avgCosts.RamCostAverage
  525. if ramCost < ramOutlierCutoff {
  526. cmme.RAMPriceRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion, node.ProviderID, node.ArchType, nodeUID).Set(ramCost)
  527. avgCosts.RamCostAverage = (avgCosts.RamCostAverage*avgCosts.NumRamDataPoints + ramCost) / (avgCosts.NumRamDataPoints + 1)
  528. avgCosts.NumRamDataPoints += 1
  529. } else {
  530. log.Debugf("RAM cost outlier detected; skipping data point: %s had %f as cost, which is above %f.", nodeName, ramCost, ramOutlierCutoff)
  531. }
  532. // skip redording totalCost if any constituent costs were outliers
  533. if cpuCost < cpuOutlierCutoff && ramCost < ramOutlierCutoff {
  534. cmme.NodeTotalPriceRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion, node.ProviderID, node.ArchType, nodeUID).Set(totalCost)
  535. } else {
  536. log.Debugf("CPU and RAM outlier detected, not recording node %s total cost %f", nodeName, totalCost)
  537. }
  538. nodeCostAverages[labelKey] = avgCosts
  539. if node.IsSpot() {
  540. cmme.NodeSpotRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion, node.ProviderID, node.ArchType, nodeUID).Set(1.0)
  541. } else {
  542. cmme.NodeSpotRecorder.WithLabelValues(nodeName, nodeName, nodeType, nodeRegion, node.ProviderID, node.ArchType, nodeUID).Set(0.0)
  543. }
  544. nodeSeen[labelKey] = true
  545. }
  546. loadBalancers, err := cmme.Model.GetLBCost()
  547. if err != nil {
  548. log.Warnf("Error getting LoadBalancer cost: %s", err)
  549. }
  550. for lbKey, lb := range loadBalancers {
  551. // TODO: parse (if necessary) and calculate cost associated with loadBalancer based on dynamic cloud prices fetched into each lb struct on GetLBCost() call
  552. namespace := lbKey.Namespace
  553. serviceName := lbKey.Service
  554. ingressIP := ""
  555. if len(lb.IngressIPAddresses) > 0 {
  556. ingressIP = lb.IngressIPAddresses[0] // assumes one ingress IP per load balancer
  557. }
  558. serviceKey := namespace + "/" + serviceName
  559. serviceUID := serviceUIDs[serviceKey]
  560. cmme.LBCostRecorder.WithLabelValues(ingressIP, namespace, serviceName, serviceUID).Set(lb.Cost)
  561. labelKey := getKeyFromLabelStrings(ingressIP, namespace, serviceName, serviceUID)
  562. loadBalancerSeen[labelKey] = true
  563. }
  564. for _, costs := range data {
  565. nodeName := costs.NodeName
  566. namespace := costs.Namespace
  567. podName := costs.PodName
  568. containerName := costs.Name
  569. if costs.PVCData != nil {
  570. for _, pvc := range costs.PVCData {
  571. if pvc.Volume != nil {
  572. timesClaimed := pvc.TimesClaimed
  573. if timesClaimed == 0 {
  574. timesClaimed = 1 // unallocated PVs are unclaimed but have a full allocation
  575. }
  576. podUID := podUIDs[podName]
  577. cmme.PVAllocationRecorder.WithLabelValues(namespace, podName, pvc.Claim, pvc.VolumeName, podUID).Set(pvc.Values[0].Value / float64(timesClaimed))
  578. labelKey := getKeyFromLabelStrings(namespace, podName, pvc.Claim, pvc.VolumeName, podUID)
  579. pvcSeen[labelKey] = true
  580. }
  581. }
  582. }
  583. if len(costs.RAMAllocation) > 0 {
  584. podUID := podUIDs[podName]
  585. cmme.RAMAllocationRecorder.WithLabelValues(namespace, podName, containerName, nodeName, nodeName, podUID).Set(costs.RAMAllocation[0].Value)
  586. }
  587. if len(costs.CPUAllocation) > 0 {
  588. podUID := podUIDs[podName]
  589. cmme.CPUAllocationRecorder.WithLabelValues(namespace, podName, containerName, nodeName, nodeName, podUID).Set(costs.CPUAllocation[0].Value)
  590. }
  591. if len(costs.GPUReq) > 0 {
  592. // allocation here is set to the request because shared GPU usage not yet supported.
  593. // if VPGUs, request x (actual/virtual)
  594. vgpu := 0.0
  595. gpu := 0.0
  596. var err, verr error
  597. if matchedNode, found := nodes[nodeName]; found {
  598. vgpu, verr = strconv.ParseFloat(matchedNode.VGPU, 64)
  599. gpu, err = strconv.ParseFloat(matchedNode.GPU, 64)
  600. } else {
  601. log.Tracef("cost data for node %s had GPUReq, but there was no cost data available for the node", nodeName)
  602. log.Trace("defaulting GPU to 0 cost")
  603. }
  604. gpualloc := costs.GPUReq[0].Value
  605. if verr != nil && err != nil && vgpu != 0 {
  606. gpualloc = gpualloc * (gpu / vgpu)
  607. }
  608. podUID := podUIDs[podName]
  609. cmme.GPUAllocationRecorder.WithLabelValues(namespace, podName, containerName, nodeName, nodeName, podUID).Set(gpualloc)
  610. }
  611. podUID := podUIDs[podName]
  612. labelKey := getKeyFromLabelStrings(namespace, podName, containerName, nodeName, nodeName, podUID)
  613. if podStatus[podName] == v1.PodRunning { // Only report data for current pods
  614. containerSeen[labelKey] = true
  615. } else {
  616. containerSeen[labelKey] = false
  617. }
  618. }
  619. storageClasses := cmme.KubeClusterCache.GetAllStorageClasses()
  620. storageClassMap := make(map[string]map[string]string)
  621. for _, storageClass := range storageClasses {
  622. params := maps.Clone(storageClass.Parameters)
  623. storageClassMap[storageClass.Name] = params
  624. if storageClass.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.Annotations["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
  625. storageClassMap["default"] = params
  626. storageClassMap[""] = params
  627. }
  628. }
  629. pvs := cmme.KubeClusterCache.GetAllPersistentVolumes()
  630. for _, pv := range pvs {
  631. // Omit pv_hourly_cost if the volume status is failed
  632. if pv.Status.Phase == v1.VolumeFailed {
  633. continue
  634. }
  635. parameters, ok := storageClassMap[pv.Spec.StorageClassName]
  636. if !ok {
  637. log.Debugf("Unable to find parameters for storage class \"%s\". Pv \"%s\" might have an empty or invalid storageClassName.", pv.Spec.StorageClassName, pv.Name)
  638. }
  639. var region string
  640. if r, ok := util.GetRegion(pv.Labels); ok {
  641. region = r
  642. } else {
  643. region = defaultRegion
  644. }
  645. cacPv := &models.PV{
  646. Class: pv.Spec.StorageClassName,
  647. Region: region,
  648. Parameters: parameters,
  649. }
  650. cmme.Model.GetPVCost(cacPv, pv, region)
  651. c, _ := strconv.ParseFloat(cacPv.Cost, 64)
  652. pvUID := pvUIDs[pv.Name]
  653. cmme.PersistentVolumePriceRecorder.WithLabelValues(pv.Name, pv.Name, cacPv.ProviderID, pvUID).Set(c)
  654. labelKey := getKeyFromLabelStrings(pv.Name, pv.Name, cacPv.ProviderID, pvUID)
  655. pvSeen[labelKey] = true
  656. }
  657. // Remove metrics on Nodes/LoadBalancers/Containers/PVs that no
  658. // longer exist
  659. for labelString, seen := range nodeSeen {
  660. if !seen {
  661. log.Debugf("Removing metrics for %s, no data observed recently", labelString)
  662. labels := getLabelStringsFromKey(labelString)
  663. ok := cmme.NodeTotalPriceRecorder.DeleteLabelValues(labels...)
  664. if ok {
  665. log.Debugf("No data observed for node with labels %v, removed from totalprice", labels)
  666. } else {
  667. log.Warnf("Failed to remove label set %v from metric node_total_hourly_cost. Failure to remove stale metrics may result in inaccurate data.", labels)
  668. }
  669. ok = cmme.NodeSpotRecorder.DeleteLabelValues(labels...)
  670. if ok {
  671. log.Debugf("No data observed for node with labels %v, removed from spot records", labels)
  672. } else {
  673. log.Warnf("Failed to remove label set %v from metric kubecost_node_is_spot. Failure to remove stale metrics may result in inaccurate data.", labels)
  674. }
  675. ok = cmme.CPUPriceRecorder.DeleteLabelValues(labels...)
  676. if ok {
  677. log.Debugf("No data observed for node with labels %v, removed from cpuprice", labels)
  678. } else {
  679. log.Warnf("Failed to remove label set %v from metric node_cpu_hourly_cost. Failure to remove stale metrics may result in inaccurate data.", labels)
  680. }
  681. ok = cmme.GPUPriceRecorder.DeleteLabelValues(labels...)
  682. if ok {
  683. log.Debugf("No data observed for node with labels %v, removed from gpuprice", labels)
  684. } else {
  685. log.Warnf("Failed to remove label set %v from metric node_gpu_hourly_cost. Failure to remove stale metrics may result in inaccurate data.", labels)
  686. }
  687. ok = cmme.GPUCountRecorder.DeleteLabelValues(labels...)
  688. if ok {
  689. log.Debugf("No data observed for node with labels %v, removed from gpucount", labels)
  690. } else {
  691. log.Warnf("Failed to remove label set %v from metric node_gpu_count. Failure to remove stale metrics may result in inaccurate data.", labels)
  692. }
  693. ok = cmme.RAMPriceRecorder.DeleteLabelValues(labels...)
  694. if ok {
  695. log.Debugf("No data observed for node with labels %v, removed from ramprice", labels)
  696. } else {
  697. log.Warnf("Failed to remove label set %v from metric node_ram_hourly_cost. Failure to remove stale metrics may result in inaccurate data.", labels)
  698. }
  699. delete(nodeSeen, labelString)
  700. delete(nodeCostAverages, labelString)
  701. } else {
  702. nodeSeen[labelString] = false
  703. }
  704. }
  705. for labelString, seen := range loadBalancerSeen {
  706. if !seen {
  707. labels := getLabelStringsFromKey(labelString)
  708. ok := cmme.LBCostRecorder.DeleteLabelValues(labels...)
  709. if !ok {
  710. log.Warnf("Failed to remove label set %v from metric kubecost_load_balancer_cost. Failure to remove stale metrics may result in inaccurate data.", labels)
  711. }
  712. delete(loadBalancerSeen, labelString)
  713. } else {
  714. loadBalancerSeen[labelString] = false
  715. }
  716. }
  717. for labelString, seen := range containerSeen {
  718. if !seen {
  719. labels := getLabelStringsFromKey(labelString)
  720. if len(labels) >= 2 && labels[1] != unmountedPVsContainer { // special "pod" to contain the unmounted PVs - does not have RAM/CPU/...
  721. ok := cmme.RAMAllocationRecorder.DeleteLabelValues(labels...)
  722. if !ok {
  723. log.Warnf("Failed to remove label set %v from metric container_memory_allocation_bytes. Failure to remove stale metrics may result in inaccurate data.", labels)
  724. }
  725. ok = cmme.CPUAllocationRecorder.DeleteLabelValues(labels...)
  726. if !ok {
  727. log.Warnf("Failed to remove label set %v from metric container_cpu_allocation. Failure to remove stale metrics may result in inaccurate data.", labels)
  728. }
  729. ok = cmme.GPUAllocationRecorder.DeleteLabelValues(labels...)
  730. if !ok {
  731. log.Warnf("Failed to remove label set %v from metric container_gpu_allocation. Failure to remove stale metrics may result in inaccurate data.", labels)
  732. }
  733. } else {
  734. log.Debugf("Did not try to delete RAM/CPU/GPU for fake '%s' container: %v", unmountedPVsContainer, labels)
  735. }
  736. delete(containerSeen, labelString)
  737. } else {
  738. containerSeen[labelString] = false
  739. }
  740. }
  741. for labelString, seen := range pvSeen {
  742. if !seen {
  743. labels := getLabelStringsFromKey(labelString)
  744. ok := cmme.PersistentVolumePriceRecorder.DeleteLabelValues(labels...)
  745. if !ok {
  746. log.Warnf("Failed to remove label set %v from metric pv_hourly_cost. Failure to remove stale metrics may result in inaccurate data.", labels)
  747. }
  748. delete(pvSeen, labelString)
  749. } else {
  750. pvSeen[labelString] = false
  751. }
  752. }
  753. for labelString, seen := range pvcSeen {
  754. if !seen {
  755. labels := getLabelStringsFromKey(labelString)
  756. ok := cmme.PVAllocationRecorder.DeleteLabelValues(labels...)
  757. if !ok {
  758. log.Warnf("Failed to remove label set %v from metric pod_pvc_allocation. Failure to remove stale metrics may result in inaccurate data.", labels)
  759. }
  760. delete(pvcSeen, labelString)
  761. } else {
  762. pvcSeen[labelString] = false
  763. }
  764. }
  765. select {
  766. case <-time.After(time.Minute):
  767. case <-cmme.runState.OnStop():
  768. cmme.runState.Reset()
  769. return
  770. }
  771. }
  772. }()
  773. return true
  774. }
  775. // Stop halts the metrics emission loop after the current emission is completed
  776. // or if the emission is paused.
  777. func (cmme *CostModelMetricsEmitter) Stop() {
  778. cmme.runState.Stop()
  779. }