kubemodel.go 18 KB

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