kubemodel.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. package metrics
  2. import (
  3. "fmt"
  4. "github.com/opencost/opencost/core/pkg/clustercache"
  5. "github.com/opencost/opencost/core/pkg/clusters"
  6. "github.com/opencost/opencost/core/pkg/log"
  7. "github.com/opencost/opencost/core/pkg/model/kubemodel"
  8. "github.com/opencost/opencost/core/pkg/source"
  9. coreutil "github.com/opencost/opencost/core/pkg/util"
  10. "github.com/opencost/opencost/core/pkg/util/promutil"
  11. "github.com/prometheus/client_golang/prometheus"
  12. dto "github.com/prometheus/client_model/go"
  13. v1 "k8s.io/api/core/v1"
  14. "k8s.io/apimachinery/pkg/api/resource"
  15. "k8s.io/apimachinery/pkg/types"
  16. )
  17. //--------------------------------------------------------------------------
  18. // KubeModelCollector
  19. //--------------------------------------------------------------------------
  20. // kubeModelMetricNames lists every metric name emitted by KubeModelCollector.
  21. // These are checked against the disabled-metrics map in Describe/Collect.
  22. var kubeModelMetricNames = []string{
  23. "node_info",
  24. "cluster_info",
  25. "pod_info",
  26. "pod_pvc_volume",
  27. "namespace_info",
  28. "deployment_info",
  29. "deployment_labels",
  30. "deployment_annotations",
  31. "statefulset_info",
  32. "statefulset_labels",
  33. "statefulset_annotations",
  34. "daemonset_info",
  35. "daemonset_labels",
  36. "daemonset_annotations",
  37. "job_info",
  38. "job_labels",
  39. "job_annotations",
  40. "cronjob_info",
  41. "cronjob_labels",
  42. "cronjob_annotations",
  43. "replicaset_info",
  44. "replicaset_labels",
  45. "replicaset_annotations",
  46. "resourcequota_info",
  47. }
  48. // KubeModelCollector emits a unified set of info/labels/annotations metrics for
  49. // all Kubernetes resource types. It mirrors the collector-source ClusterCacheScraper:
  50. // indexes are built once per Collect call and per-resource scrapes run concurrently.
  51. type KubeModelCollector struct {
  52. KubeClusterCache clustercache.ClusterCache
  53. ClusterInfo clusters.ClusterInfoProvider
  54. metricsConfig MetricsConfig
  55. }
  56. // Describe sends a generic descriptor for each metric emitted by this collector.
  57. func (c KubeModelCollector) Describe(ch chan<- *prometheus.Desc) {
  58. disabled := c.metricsConfig.GetDisabledMetricsMap()
  59. for _, name := range kubeModelMetricNames {
  60. if _, ok := disabled[name]; ok {
  61. continue
  62. }
  63. ch <- prometheus.NewDesc(name, name, []string{}, nil)
  64. }
  65. }
  66. // Collect fetches all cluster resources, builds cross-reference indexes, then
  67. // emits info/labels/annotations metrics concurrently per resource type.
  68. func (c KubeModelCollector) Collect(ch chan<- prometheus.Metric) {
  69. disabled := c.metricsConfig.GetDisabledMetricsMap()
  70. // Fetch all resources from the cache up front.
  71. nodes := c.KubeClusterCache.GetAllNodes()
  72. namespaces := c.KubeClusterCache.GetAllNamespaces()
  73. pods := c.KubeClusterCache.GetAllPods()
  74. pvcs := c.KubeClusterCache.GetAllPersistentVolumeClaims()
  75. deployments := c.KubeClusterCache.GetAllDeployments()
  76. statefulSets := c.KubeClusterCache.GetAllStatefulSets()
  77. daemonSets := c.KubeClusterCache.GetAllDaemonSets()
  78. jobs := c.KubeClusterCache.GetAllJobs()
  79. cronJobs := c.KubeClusterCache.GetAllCronJobs()
  80. replicaSets := c.KubeClusterCache.GetAllReplicaSets()
  81. resourceQuotas := c.KubeClusterCache.GetAllResourceQuotas()
  82. // Build cross-reference indexes.
  83. nsIndex := make(map[string]types.UID, len(namespaces))
  84. for _, ns := range namespaces {
  85. nsIndex[ns.Name] = ns.UID
  86. }
  87. nodeIndex := make(map[string]types.UID, len(nodes))
  88. for _, node := range nodes {
  89. nodeIndex[node.Name] = node.UID
  90. }
  91. pvcIndex := make(map[string]types.UID, len(pvcs))
  92. for _, pvc := range pvcs {
  93. pvcIndex[pvcIndexKey(pvc.Namespace, pvc.Name)] = pvc.UID
  94. }
  95. // Collect concurrently using a channel.
  96. type scrapeFn func() []kubeModelMetric
  97. fns := []scrapeFn{
  98. func() []kubeModelMetric { return c.scrapeClusterInfo(disabled) },
  99. func() []kubeModelMetric { return c.scrapeNodes(nodes, disabled) },
  100. func() []kubeModelMetric { return c.scrapeNamespaces(namespaces, disabled) },
  101. func() []kubeModelMetric { return c.scrapePods(pods, nsIndex, nodeIndex, pvcIndex, disabled) },
  102. func() []kubeModelMetric { return c.scrapeDeployments(deployments, nsIndex, disabled) },
  103. func() []kubeModelMetric { return c.scrapeStatefulSets(statefulSets, nsIndex, disabled) },
  104. func() []kubeModelMetric { return c.scrapeDaemonSets(daemonSets, nsIndex, disabled) },
  105. func() []kubeModelMetric { return c.scrapeJobs(jobs, nsIndex, disabled) },
  106. func() []kubeModelMetric { return c.scrapeCronJobs(cronJobs, nsIndex, disabled) },
  107. func() []kubeModelMetric { return c.scrapeReplicaSets(replicaSets, nsIndex, disabled) },
  108. func() []kubeModelMetric { return c.scrapeResourceQuotas(resourceQuotas, nsIndex, disabled) },
  109. }
  110. results := make(chan []kubeModelMetric, len(fns))
  111. for _, fn := range fns {
  112. fn := fn
  113. go func() { results <- fn() }()
  114. }
  115. for range fns {
  116. for _, m := range <-results {
  117. ch <- m
  118. }
  119. }
  120. }
  121. // pvcIndexKey returns a map key for a PVC by namespace+name.
  122. func pvcIndexKey(namespace, name string) string {
  123. return fmt.Sprintf("%s/%s", namespace, name)
  124. }
  125. //--------------------------------------------------------------------------
  126. // kubeModelMetric — generic prometheus.Metric for kube-model emissions
  127. //--------------------------------------------------------------------------
  128. // kubeModelMetric implements prometheus.Metric for any kube-model info/labels metric.
  129. // All labels are stored in a map and emitted via Write; the gauge value defaults to 1.
  130. type kubeModelMetric struct {
  131. name string
  132. help string
  133. labels map[string]string
  134. value float64
  135. }
  136. func newInfoMetric(name string, labels map[string]string) kubeModelMetric {
  137. return kubeModelMetric{name: name, help: name, labels: labels, value: 1}
  138. }
  139. func newValueMetric(name string, labels map[string]string, value float64) kubeModelMetric {
  140. return kubeModelMetric{name: name, help: name, labels: labels, value: value}
  141. }
  142. func (m kubeModelMetric) Desc() *prometheus.Desc {
  143. return prometheus.NewDesc(m.name, m.help, promutil.LabelNamesFrom(m.labels), prometheus.Labels{})
  144. }
  145. func (m kubeModelMetric) Write(pb *dto.Metric) error {
  146. pb.Gauge = &dto.Gauge{Value: &m.value}
  147. pairs := make([]*dto.LabelPair, 0, len(m.labels))
  148. for k, v := range m.labels {
  149. pairs = append(pairs, &dto.LabelPair{
  150. Name: toStringPtr(k),
  151. Value: toStringPtr(v),
  152. })
  153. }
  154. pb.Label = pairs
  155. return nil
  156. }
  157. //--------------------------------------------------------------------------
  158. // Per-resource scrape helpers
  159. //--------------------------------------------------------------------------
  160. func (c KubeModelCollector) scrapeClusterInfo(disabled map[string]struct{}) []kubeModelMetric {
  161. if _, ok := disabled["cluster_info"]; ok {
  162. return nil
  163. }
  164. if c.ClusterInfo == nil {
  165. return nil
  166. }
  167. info := c.ClusterInfo.GetClusterInfo()
  168. labels := map[string]string{
  169. "uid": info[clusters.ClusterInfoIdKey],
  170. "provider": info[clusters.ClusterInfoProviderKey],
  171. "account_id": info[clusters.ClusterInfoAccountKey],
  172. "provisioner_name": info[clusters.ClusterInfoProvisionerKey],
  173. "region": info[clusters.ClusterInfoRegionKey],
  174. source.KubeModelVersion: fmt.Sprintf("%d", kubemodel.DefaultCodecVersion),
  175. }
  176. // GCP uses "project" instead of "account"
  177. if labels["account_id"] == "" {
  178. labels["account_id"] = info[clusters.ClusterInfoProjectKey]
  179. }
  180. return []kubeModelMetric{newInfoMetric("cluster_info", labels)}
  181. }
  182. func (c KubeModelCollector) scrapeNodes(nodes []*clustercache.Node, disabled map[string]struct{}) []kubeModelMetric {
  183. var out []kubeModelMetric
  184. emitInfo := !isDisabled(disabled, "node_info")
  185. for _, node := range nodes {
  186. nodeInfo := map[string]string{
  187. "node": node.Name,
  188. "uid": string(node.UID),
  189. "provider_id": node.SpecProviderID,
  190. }
  191. if instanceType, ok := coreutil.GetInstanceType(node.Labels); ok {
  192. nodeInfo["instance_type"] = instanceType
  193. }
  194. if emitInfo {
  195. out = append(out, newInfoMetric("node_info", nodeInfo))
  196. }
  197. }
  198. return out
  199. }
  200. func (c KubeModelCollector) scrapeNamespaces(namespaces []*clustercache.Namespace, disabled map[string]struct{}) []kubeModelMetric {
  201. var out []kubeModelMetric
  202. emitInfo := !isDisabled(disabled, "namespace_info")
  203. for _, ns := range namespaces {
  204. if emitInfo {
  205. out = append(out, newInfoMetric("namespace_info", map[string]string{
  206. "uid": string(ns.UID),
  207. "namespace": ns.Name,
  208. }))
  209. }
  210. }
  211. return out
  212. }
  213. func (c KubeModelCollector) scrapePods(
  214. pods []*clustercache.Pod,
  215. nsIndex map[string]types.UID,
  216. nodeIndex map[string]types.UID,
  217. pvcIndex map[string]types.UID,
  218. disabled map[string]struct{},
  219. ) []kubeModelMetric {
  220. var out []kubeModelMetric
  221. emitInfo := !isDisabled(disabled, "pod_info")
  222. emitPVC := !isDisabled(disabled, "pod_pvc_volume")
  223. for _, pod := range pods {
  224. nsUID, ok := nsIndex[pod.Namespace]
  225. if !ok {
  226. log.Debugf("KubeModelCollector: pod namespace uid missing for namespace '%s'", pod.Namespace)
  227. }
  228. nodeUID, ok := nodeIndex[pod.Spec.NodeName]
  229. if !ok && pod.Spec.NodeName != "" {
  230. log.Debugf("KubeModelCollector: pod node uid missing for node '%s'", pod.Spec.NodeName)
  231. }
  232. if emitInfo {
  233. out = append(out, newInfoMetric("pod_info", map[string]string{
  234. "uid": string(pod.UID),
  235. "pod": pod.Name,
  236. "namespace_uid": string(nsUID),
  237. "node_uid": string(nodeUID),
  238. }))
  239. }
  240. if emitPVC {
  241. for _, vol := range pod.Spec.Volumes {
  242. if vol.PersistentVolumeClaim == nil {
  243. continue
  244. }
  245. pvcUID := pvcIndex[pvcIndexKey(pod.Namespace, vol.PersistentVolumeClaim.ClaimName)]
  246. out = append(out, newInfoMetric("pod_pvc_volume", map[string]string{
  247. "uid": string(pod.UID),
  248. "persistentvolumeclaim_uid": string(pvcUID),
  249. "pod_volume_name": vol.Name,
  250. }))
  251. }
  252. }
  253. }
  254. return out
  255. }
  256. func (c KubeModelCollector) scrapeDeployments(
  257. deployments []*clustercache.Deployment,
  258. nsIndex map[string]types.UID,
  259. disabled map[string]struct{},
  260. ) []kubeModelMetric {
  261. var out []kubeModelMetric
  262. emitInfo := !isDisabled(disabled, "deployment_info")
  263. emitLabels := !isDisabled(disabled, "deployment_labels")
  264. emitAnno := !isDisabled(disabled, "deployment_annotations")
  265. for _, d := range deployments {
  266. nsUID, ok := nsIndex[d.Namespace]
  267. if !ok {
  268. log.Debugf("KubeModelCollector: deployment namespace uid missing for namespace '%s'", d.Namespace)
  269. }
  270. if emitInfo {
  271. out = append(out, newInfoMetric("deployment_info", map[string]string{
  272. "uid": string(d.UID),
  273. "namespace_uid": string(nsUID),
  274. "deployment": d.Name,
  275. }))
  276. }
  277. if emitLabels {
  278. out = append(out, kubeLabelsMetric("deployment_labels", string(d.UID), d.Labels))
  279. }
  280. if emitAnno {
  281. out = append(out, kubeAnnotationsMetric("deployment_annotations", string(d.UID), d.Annotations))
  282. }
  283. }
  284. return out
  285. }
  286. func (c KubeModelCollector) scrapeStatefulSets(
  287. sets []*clustercache.StatefulSet,
  288. nsIndex map[string]types.UID,
  289. disabled map[string]struct{},
  290. ) []kubeModelMetric {
  291. var out []kubeModelMetric
  292. emitInfo := !isDisabled(disabled, "statefulset_info")
  293. emitLabels := !isDisabled(disabled, "statefulset_labels")
  294. emitAnno := !isDisabled(disabled, "statefulset_annotations")
  295. for _, s := range sets {
  296. nsUID, ok := nsIndex[s.Namespace]
  297. if !ok {
  298. log.Debugf("KubeModelCollector: statefulset namespace uid missing for namespace '%s'", s.Namespace)
  299. }
  300. if emitInfo {
  301. out = append(out, newInfoMetric("statefulset_info", map[string]string{
  302. "uid": string(s.UID),
  303. "namespace_uid": string(nsUID),
  304. "statefulSet": s.Name,
  305. }))
  306. }
  307. if emitLabels {
  308. out = append(out, kubeLabelsMetric("statefulset_labels", string(s.UID), s.Labels))
  309. }
  310. if emitAnno {
  311. out = append(out, kubeAnnotationsMetric("statefulset_annotations", string(s.UID), s.Annotations))
  312. }
  313. }
  314. return out
  315. }
  316. func (c KubeModelCollector) scrapeDaemonSets(
  317. sets []*clustercache.DaemonSet,
  318. nsIndex map[string]types.UID,
  319. disabled map[string]struct{},
  320. ) []kubeModelMetric {
  321. var out []kubeModelMetric
  322. emitInfo := !isDisabled(disabled, "daemonset_info")
  323. emitLabels := !isDisabled(disabled, "daemonset_labels")
  324. emitAnno := !isDisabled(disabled, "daemonset_annotations")
  325. for _, ds := range sets {
  326. nsUID, ok := nsIndex[ds.Namespace]
  327. if !ok {
  328. log.Debugf("KubeModelCollector: daemonset namespace uid missing for namespace '%s'", ds.Namespace)
  329. }
  330. if emitInfo {
  331. out = append(out, newInfoMetric("daemonset_info", map[string]string{
  332. "uid": string(ds.UID),
  333. "namespace_uid": string(nsUID),
  334. "daemonset": ds.Name,
  335. }))
  336. }
  337. if emitLabels {
  338. out = append(out, kubeLabelsMetric("daemonset_labels", string(ds.UID), ds.Labels))
  339. }
  340. if emitAnno {
  341. out = append(out, kubeAnnotationsMetric("daemonset_annotations", string(ds.UID), ds.Annotations))
  342. }
  343. }
  344. return out
  345. }
  346. func (c KubeModelCollector) scrapeJobs(
  347. jobs []*clustercache.Job,
  348. nsIndex map[string]types.UID,
  349. disabled map[string]struct{},
  350. ) []kubeModelMetric {
  351. var out []kubeModelMetric
  352. emitInfo := !isDisabled(disabled, "job_info")
  353. emitLabels := !isDisabled(disabled, "job_labels")
  354. emitAnno := !isDisabled(disabled, "job_annotations")
  355. for _, j := range jobs {
  356. nsUID, ok := nsIndex[j.Namespace]
  357. if !ok {
  358. log.Debugf("KubeModelCollector: job namespace uid missing for namespace '%s'", j.Namespace)
  359. }
  360. if emitInfo {
  361. out = append(out, newInfoMetric("job_info", map[string]string{
  362. "uid": string(j.UID),
  363. "namespace_uid": string(nsUID),
  364. "job": j.Name,
  365. }))
  366. }
  367. if emitLabels {
  368. out = append(out, kubeLabelsMetric("job_labels", string(j.UID), j.Labels))
  369. }
  370. if emitAnno {
  371. out = append(out, kubeAnnotationsMetric("job_annotations", string(j.UID), j.Annotations))
  372. }
  373. }
  374. return out
  375. }
  376. func (c KubeModelCollector) scrapeCronJobs(
  377. cronJobs []*clustercache.CronJob,
  378. nsIndex map[string]types.UID,
  379. disabled map[string]struct{},
  380. ) []kubeModelMetric {
  381. var out []kubeModelMetric
  382. emitInfo := !isDisabled(disabled, "cronjob_info")
  383. emitLabels := !isDisabled(disabled, "cronjob_labels")
  384. emitAnno := !isDisabled(disabled, "cronjob_annotations")
  385. for _, cj := range cronJobs {
  386. nsUID, ok := nsIndex[cj.Namespace]
  387. if !ok {
  388. log.Debugf("KubeModelCollector: cronjob namespace uid missing for namespace '%s'", cj.Namespace)
  389. }
  390. if emitInfo {
  391. out = append(out, newInfoMetric("cronjob_info", map[string]string{
  392. "uid": string(cj.UID),
  393. "namespace_uid": string(nsUID),
  394. "cronjob": cj.Name,
  395. }))
  396. }
  397. if emitLabels {
  398. out = append(out, kubeLabelsMetric("cronjob_labels", string(cj.UID), cj.Labels))
  399. }
  400. if emitAnno {
  401. out = append(out, kubeAnnotationsMetric("cronjob_annotations", string(cj.UID), cj.Annotations))
  402. }
  403. }
  404. return out
  405. }
  406. func (c KubeModelCollector) scrapeReplicaSets(
  407. sets []*clustercache.ReplicaSet,
  408. nsIndex map[string]types.UID,
  409. disabled map[string]struct{},
  410. ) []kubeModelMetric {
  411. var out []kubeModelMetric
  412. emitInfo := !isDisabled(disabled, "replicaset_info")
  413. emitLabels := !isDisabled(disabled, "replicaset_labels")
  414. emitAnno := !isDisabled(disabled, "replicaset_annotations")
  415. for _, rs := range sets {
  416. nsUID, ok := nsIndex[rs.Namespace]
  417. if !ok {
  418. log.Debugf("KubeModelCollector: replicaset namespace uid missing for namespace '%s'", rs.Namespace)
  419. }
  420. if emitInfo {
  421. out = append(out, newInfoMetric("replicaset_info", map[string]string{
  422. "uid": string(rs.UID),
  423. "namespace_uid": string(nsUID),
  424. "replicaset": rs.Name,
  425. }))
  426. }
  427. if emitLabels {
  428. out = append(out, kubeLabelsMetric("replicaset_labels", string(rs.UID), rs.Labels))
  429. }
  430. if emitAnno {
  431. out = append(out, kubeAnnotationsMetric("replicaset_annotations", string(rs.UID), rs.Annotations))
  432. }
  433. }
  434. return out
  435. }
  436. func (c KubeModelCollector) scrapeResourceQuotas(
  437. quotas []*clustercache.ResourceQuota,
  438. nsIndex map[string]types.UID,
  439. disabled map[string]struct{},
  440. ) []kubeModelMetric {
  441. if isDisabled(disabled, "resourcequota_info") {
  442. return nil
  443. }
  444. var out []kubeModelMetric
  445. for _, rq := range quotas {
  446. nsUID, ok := nsIndex[rq.Namespace]
  447. if !ok {
  448. log.Debugf("KubeModelCollector: resourcequota namespace uid missing for namespace '%s'", rq.Namespace)
  449. }
  450. out = append(out, newInfoMetric("resourcequota_info", map[string]string{
  451. "uid": string(rq.UID),
  452. "namespace_uid": string(nsUID),
  453. "resourcequota": rq.Name,
  454. }))
  455. }
  456. return out
  457. }
  458. //--------------------------------------------------------------------------
  459. // Helpers
  460. //--------------------------------------------------------------------------
  461. // isDisabled returns true if the named metric appears in the disabled map.
  462. func isDisabled(disabled map[string]struct{}, name string) bool {
  463. _, ok := disabled[name]
  464. return ok
  465. }
  466. // kubeLabelsMetric builds a labels metric for a resource, adding the resource
  467. // uid as a fixed label alongside the k8s labels (prefixed with "label_").
  468. func kubeLabelsMetric(name, uid string, k8sLabels map[string]string) kubeModelMetric {
  469. labelNames, labelValues := promutil.KubeLabelsToLabels(promutil.SanitizeLabels(k8sLabels))
  470. m := make(map[string]string, len(labelNames)+1)
  471. m["uid"] = uid
  472. for i, k := range labelNames {
  473. m[k] = labelValues[i]
  474. }
  475. return newInfoMetric(name, m)
  476. }
  477. // kubeAnnotationsMetric builds an annotations metric for a resource.
  478. func kubeAnnotationsMetric(name, uid string, k8sAnnotations map[string]string) kubeModelMetric {
  479. annoNames, annoValues := promutil.KubeAnnotationsToLabels(k8sAnnotations)
  480. m := make(map[string]string, len(annoNames)+1)
  481. m["uid"] = uid
  482. for i, k := range annoNames {
  483. m[k] = annoValues[i]
  484. }
  485. return newInfoMetric(name, m)
  486. }
  487. // kubeModelResourceValue converts a Kubernetes resource quantity to a float64 value.
  488. // It mirrors the collector-source toResourceUnitValue logic for the cases we need.
  489. func kubeModelResourceValue(resourceName v1.ResourceName, quantity resource.Quantity) float64 {
  490. switch resourceName {
  491. case v1.ResourceCPU:
  492. return float64(quantity.MilliValue()) / 1000.0
  493. default:
  494. return float64(quantity.Value())
  495. }
  496. }