podmetrics_test.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  1. package metrics
  2. import (
  3. "testing"
  4. "github.com/opencost/opencost/core/pkg/clustercache"
  5. "github.com/prometheus/client_golang/prometheus"
  6. dto "github.com/prometheus/client_model/go"
  7. v1 "k8s.io/api/core/v1"
  8. "k8s.io/apimachinery/pkg/api/resource"
  9. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  10. "k8s.io/apimachinery/pkg/types"
  11. )
  12. func TestKubecostPodCollector_Describe(t *testing.T) {
  13. tests := []struct {
  14. name string
  15. disabledMetrics []string
  16. expectMetric bool
  17. }{
  18. {
  19. name: "annotations enabled",
  20. disabledMetrics: []string{},
  21. expectMetric: true,
  22. },
  23. {
  24. name: "annotations disabled",
  25. disabledMetrics: []string{"kube_pod_annotations"},
  26. expectMetric: false,
  27. },
  28. }
  29. for _, tt := range tests {
  30. t.Run(tt.name, func(t *testing.T) {
  31. mc := MetricsConfig{
  32. DisabledMetrics: tt.disabledMetrics,
  33. }
  34. kpc := KubecostPodCollector{
  35. KubeClusterCache: NewFakePodCache([]*clustercache.Pod{}),
  36. metricsConfig: mc,
  37. }
  38. ch := make(chan *prometheus.Desc, 10)
  39. kpc.Describe(ch)
  40. close(ch)
  41. count := 0
  42. for range ch {
  43. count++
  44. }
  45. if tt.expectMetric && count == 0 {
  46. t.Error("Expected metric description but got none")
  47. }
  48. if !tt.expectMetric && count > 0 {
  49. t.Error("Expected no metric description but got some")
  50. }
  51. })
  52. }
  53. }
  54. func TestKubecostPodCollector_Collect(t *testing.T) {
  55. tests := []struct {
  56. name string
  57. pods []*clustercache.Pod
  58. disabledMetrics []string
  59. expectedCount int
  60. }{
  61. {
  62. name: "pod with annotations",
  63. pods: []*clustercache.Pod{
  64. {
  65. UID: types.UID("pod-uid-1"),
  66. Name: "test-pod",
  67. Namespace: "default",
  68. Annotations: map[string]string{
  69. "prometheus.io/scrape": "true",
  70. "prometheus.io/port": "8080",
  71. },
  72. },
  73. },
  74. disabledMetrics: []string{},
  75. expectedCount: 1,
  76. },
  77. {
  78. name: "pod without annotations",
  79. pods: []*clustercache.Pod{
  80. {
  81. UID: types.UID("pod-uid-2"),
  82. Name: "empty-pod",
  83. Namespace: "default",
  84. Annotations: map[string]string{},
  85. },
  86. },
  87. disabledMetrics: []string{},
  88. expectedCount: 0,
  89. },
  90. {
  91. name: "multiple pods with mixed annotations",
  92. pods: []*clustercache.Pod{
  93. {
  94. UID: types.UID("pod-uid-3"),
  95. Name: "pod1",
  96. Namespace: "ns1",
  97. Annotations: map[string]string{"key": "value"},
  98. },
  99. {
  100. UID: types.UID("pod-uid-4"),
  101. Name: "pod2",
  102. Namespace: "ns1",
  103. Annotations: map[string]string{},
  104. },
  105. },
  106. disabledMetrics: []string{},
  107. expectedCount: 1,
  108. },
  109. {
  110. name: "metric disabled",
  111. pods: []*clustercache.Pod{
  112. {
  113. UID: types.UID("pod-uid-5"),
  114. Name: "test-pod",
  115. Namespace: "default",
  116. Annotations: map[string]string{"test": "annotation"},
  117. },
  118. },
  119. disabledMetrics: []string{"kube_pod_annotations"},
  120. expectedCount: 0,
  121. },
  122. }
  123. for _, tt := range tests {
  124. t.Run(tt.name, func(t *testing.T) {
  125. mc := MetricsConfig{
  126. DisabledMetrics: tt.disabledMetrics,
  127. }
  128. kpc := KubecostPodCollector{
  129. KubeClusterCache: NewFakePodCache(tt.pods),
  130. metricsConfig: mc,
  131. }
  132. ch := make(chan prometheus.Metric, 10)
  133. kpc.Collect(ch)
  134. close(ch)
  135. count := 0
  136. for range ch {
  137. count++
  138. }
  139. if count != tt.expectedCount {
  140. t.Errorf("Expected %d metrics, got %d", tt.expectedCount, count)
  141. }
  142. })
  143. }
  144. }
  145. func TestPodAnnotationMetric(t *testing.T) {
  146. labelNames := []string{"annotation_key1", "annotation_key2"}
  147. labelValues := []string{"value1", "value2"}
  148. metric := newPodAnnotationMetric("kube_pod_annotations", "test-ns", "test-pod", "test-uid", labelNames, labelValues)
  149. // Test Desc method
  150. desc := metric.Desc()
  151. if desc == nil {
  152. t.Error("Expected non-nil descriptor")
  153. }
  154. // Test Write method
  155. var dtoMetric dto.Metric
  156. err := metric.Write(&dtoMetric)
  157. if err != nil {
  158. t.Errorf("Expected no error, got %v", err)
  159. }
  160. if dtoMetric.Gauge == nil {
  161. t.Error("Expected gauge metric")
  162. }
  163. if *dtoMetric.Gauge.Value != 1.0 {
  164. t.Errorf("Expected gauge value 1.0, got %f", *dtoMetric.Gauge.Value)
  165. }
  166. // Verify labels
  167. expectedLabels := map[string]string{
  168. "annotation_key1": "value1",
  169. "annotation_key2": "value2",
  170. "namespace": "test-ns",
  171. "pod": "test-pod",
  172. "uid": "test-uid",
  173. }
  174. actualLabels := make(map[string]string)
  175. for _, label := range dtoMetric.Label {
  176. actualLabels[*label.Name] = *label.Value
  177. }
  178. for key, expectedValue := range expectedLabels {
  179. if actualValue, ok := actualLabels[key]; !ok {
  180. t.Errorf("Missing label %s", key)
  181. } else if actualValue != expectedValue {
  182. t.Errorf("Label %s: expected %s, got %s", key, expectedValue, actualValue)
  183. }
  184. }
  185. }
  186. func TestKubePodCollector_Describe(t *testing.T) {
  187. tests := []struct {
  188. name string
  189. disabledMetrics []string
  190. expectedCount int
  191. }{
  192. {
  193. name: "all metrics enabled",
  194. disabledMetrics: []string{},
  195. expectedCount: 10,
  196. },
  197. {
  198. name: "some metrics disabled",
  199. disabledMetrics: []string{
  200. "kube_pod_labels",
  201. "kube_pod_owner",
  202. "kube_pod_container_status_running",
  203. },
  204. expectedCount: 7,
  205. },
  206. {
  207. name: "all metrics disabled",
  208. disabledMetrics: []string{
  209. "kube_pod_labels",
  210. "kube_pod_owner",
  211. "kube_pod_container_status_running",
  212. "kube_pod_container_status_terminated_reason",
  213. "kube_pod_container_status_restarts_total",
  214. "kube_pod_container_resource_requests",
  215. "kube_pod_container_resource_limits",
  216. "kube_pod_container_resource_limits_cpu_cores",
  217. "kube_pod_container_resource_limits_memory_bytes",
  218. "kube_pod_status_phase",
  219. },
  220. expectedCount: 0,
  221. },
  222. }
  223. for _, tt := range tests {
  224. t.Run(tt.name, func(t *testing.T) {
  225. mc := MetricsConfig{
  226. DisabledMetrics: tt.disabledMetrics,
  227. }
  228. kpc := KubePodCollector{
  229. KubeClusterCache: NewFakePodCache([]*clustercache.Pod{}),
  230. metricsConfig: mc,
  231. }
  232. ch := make(chan *prometheus.Desc, 15)
  233. kpc.Describe(ch)
  234. close(ch)
  235. count := 0
  236. for range ch {
  237. count++
  238. }
  239. if count != tt.expectedCount {
  240. t.Errorf("Expected %d metrics, got %d", tt.expectedCount, count)
  241. }
  242. })
  243. }
  244. }
  245. func TestKubePodCollector_Collect(t *testing.T) {
  246. boolTrue := true
  247. tests := []struct {
  248. name string
  249. pods []*clustercache.Pod
  250. disabledMetrics []string
  251. expectedCount int
  252. }{
  253. {
  254. name: "pod with all features",
  255. pods: []*clustercache.Pod{
  256. {
  257. UID: types.UID("pod-uid-1"),
  258. Name: "test-pod",
  259. Namespace: "default",
  260. Labels: map[string]string{
  261. "app": "test",
  262. "version": "v1",
  263. },
  264. OwnerReferences: []metav1.OwnerReference{
  265. {
  266. Name: "test-deployment",
  267. Kind: "Deployment",
  268. Controller: &boolTrue,
  269. },
  270. },
  271. Status: clustercache.PodStatus{
  272. Phase: v1.PodRunning,
  273. ContainerStatuses: []v1.ContainerStatus{
  274. {
  275. Name: "container1",
  276. RestartCount: 2,
  277. State: v1.ContainerState{
  278. Running: &v1.ContainerStateRunning{},
  279. },
  280. },
  281. },
  282. },
  283. Spec: clustercache.PodSpec{
  284. NodeName: "node1",
  285. Containers: []clustercache.Container{
  286. {
  287. Name: "container1",
  288. Resources: v1.ResourceRequirements{
  289. Requests: v1.ResourceList{
  290. v1.ResourceCPU: resource.MustParse("100m"),
  291. v1.ResourceMemory: resource.MustParse("128Mi"),
  292. },
  293. Limits: v1.ResourceList{
  294. v1.ResourceCPU: resource.MustParse("200m"),
  295. v1.ResourceMemory: resource.MustParse("256Mi"),
  296. },
  297. },
  298. },
  299. },
  300. },
  301. },
  302. },
  303. disabledMetrics: []string{},
  304. expectedCount: 15, // 5 phases + 1 labels + 1 owner + 1 restarts + 1 running + 2 requests + 4 limits
  305. },
  306. {
  307. name: "pod without containers",
  308. pods: []*clustercache.Pod{
  309. {
  310. UID: types.UID("pod-uid-2"),
  311. Name: "empty-pod",
  312. Namespace: "default",
  313. Labels: map[string]string{"test": "label"},
  314. Status: clustercache.PodStatus{
  315. Phase: v1.PodPending,
  316. },
  317. Spec: clustercache.PodSpec{
  318. Containers: []clustercache.Container{},
  319. },
  320. },
  321. },
  322. disabledMetrics: []string{},
  323. expectedCount: 6, // 5 phases + 1 labels
  324. },
  325. {
  326. name: "pod with terminated container",
  327. pods: []*clustercache.Pod{
  328. {
  329. UID: types.UID("pod-uid-3"),
  330. Name: "terminated-pod",
  331. Namespace: "default",
  332. Labels: map[string]string{},
  333. Status: clustercache.PodStatus{
  334. Phase: v1.PodFailed,
  335. ContainerStatuses: []v1.ContainerStatus{
  336. {
  337. Name: "failed-container",
  338. RestartCount: 5,
  339. State: v1.ContainerState{
  340. Terminated: &v1.ContainerStateTerminated{
  341. Reason: "OOMKilled",
  342. },
  343. },
  344. },
  345. },
  346. },
  347. Spec: clustercache.PodSpec{
  348. Containers: []clustercache.Container{
  349. {
  350. Name: "failed-container",
  351. Resources: v1.ResourceRequirements{},
  352. },
  353. },
  354. },
  355. },
  356. },
  357. disabledMetrics: []string{},
  358. expectedCount: 8, // 5 phases + 1 labels + 1 restarts + 1 terminated reason
  359. },
  360. {
  361. name: "pod without phase",
  362. pods: []*clustercache.Pod{
  363. {
  364. UID: types.UID("pod-uid-4"),
  365. Name: "no-phase-pod",
  366. Namespace: "default",
  367. Labels: map[string]string{"app": "test"},
  368. Status: clustercache.PodStatus{
  369. Phase: "", // Empty phase
  370. },
  371. Spec: clustercache.PodSpec{},
  372. },
  373. },
  374. disabledMetrics: []string{},
  375. expectedCount: 1, // Only labels
  376. },
  377. {
  378. name: "multiple containers",
  379. pods: []*clustercache.Pod{
  380. {
  381. UID: types.UID("pod-uid-5"),
  382. Name: "multi-container-pod",
  383. Namespace: "default",
  384. Labels: map[string]string{},
  385. Status: clustercache.PodStatus{
  386. Phase: v1.PodRunning,
  387. ContainerStatuses: []v1.ContainerStatus{
  388. {
  389. Name: "container1",
  390. RestartCount: 0,
  391. State: v1.ContainerState{
  392. Running: &v1.ContainerStateRunning{},
  393. },
  394. },
  395. {
  396. Name: "container2",
  397. RestartCount: 1,
  398. State: v1.ContainerState{
  399. Running: &v1.ContainerStateRunning{},
  400. },
  401. },
  402. },
  403. },
  404. Spec: clustercache.PodSpec{
  405. NodeName: "node2",
  406. Containers: []clustercache.Container{
  407. {
  408. Name: "container1",
  409. Resources: v1.ResourceRequirements{
  410. Requests: v1.ResourceList{
  411. v1.ResourceCPU: resource.MustParse("50m"),
  412. },
  413. Limits: v1.ResourceList{
  414. v1.ResourceCPU: resource.MustParse("100m"),
  415. },
  416. },
  417. },
  418. {
  419. Name: "container2",
  420. Resources: v1.ResourceRequirements{
  421. Requests: v1.ResourceList{
  422. v1.ResourceMemory: resource.MustParse("64Mi"),
  423. },
  424. Limits: v1.ResourceList{
  425. v1.ResourceMemory: resource.MustParse("128Mi"),
  426. },
  427. },
  428. },
  429. },
  430. },
  431. },
  432. },
  433. disabledMetrics: []string{},
  434. expectedCount: 16, // 5 phases + 1 labels + 2 restarts + 2 running + 2 requests + 4 limits
  435. },
  436. {
  437. name: "metrics disabled",
  438. pods: []*clustercache.Pod{
  439. {
  440. UID: types.UID("pod-uid-6"),
  441. Name: "test-pod",
  442. Namespace: "default",
  443. Labels: map[string]string{"app": "test"},
  444. Status: clustercache.PodStatus{
  445. Phase: v1.PodRunning,
  446. },
  447. Spec: clustercache.PodSpec{},
  448. },
  449. },
  450. disabledMetrics: []string{"kube_pod_labels", "kube_pod_status_phase"},
  451. expectedCount: 0,
  452. },
  453. }
  454. for _, tt := range tests {
  455. t.Run(tt.name, func(t *testing.T) {
  456. mc := MetricsConfig{
  457. DisabledMetrics: tt.disabledMetrics,
  458. }
  459. kpc := KubePodCollector{
  460. KubeClusterCache: NewFakePodCache(tt.pods),
  461. metricsConfig: mc,
  462. }
  463. ch := make(chan prometheus.Metric, 30)
  464. kpc.Collect(ch)
  465. close(ch)
  466. count := 0
  467. for range ch {
  468. count++
  469. }
  470. if count != tt.expectedCount {
  471. t.Errorf("Expected %d metrics, got %d", tt.expectedCount, count)
  472. }
  473. })
  474. }
  475. }
  476. func TestKubePodLabelsMetric(t *testing.T) {
  477. labelNames := []string{"label_app", "label_env"}
  478. labelValues := []string{"webapp", "production"}
  479. metric := newKubePodLabelsMetric("kube_pod_labels", "prod", "web-pod", "pod-uid", labelNames, labelValues)
  480. // Test Desc method
  481. desc := metric.Desc()
  482. if desc == nil {
  483. t.Error("Expected non-nil descriptor")
  484. }
  485. // Test Write method
  486. var dtoMetric dto.Metric
  487. err := metric.Write(&dtoMetric)
  488. if err != nil {
  489. t.Errorf("Expected no error, got %v", err)
  490. }
  491. if dtoMetric.Gauge == nil {
  492. t.Error("Expected gauge metric")
  493. }
  494. if *dtoMetric.Gauge.Value != 1.0 {
  495. t.Errorf("Expected gauge value 1.0, got %f", *dtoMetric.Gauge.Value)
  496. }
  497. // Verify labels
  498. expectedLabels := map[string]string{
  499. "label_app": "webapp",
  500. "label_env": "production",
  501. "namespace": "prod",
  502. "pod": "web-pod",
  503. "uid": "pod-uid",
  504. }
  505. actualLabels := make(map[string]string)
  506. for _, label := range dtoMetric.Label {
  507. actualLabels[*label.Name] = *label.Value
  508. }
  509. for key, expectedValue := range expectedLabels {
  510. if actualValue, ok := actualLabels[key]; !ok {
  511. t.Errorf("Missing label %s", key)
  512. } else if actualValue != expectedValue {
  513. t.Errorf("Label %s: expected %s, got %s", key, expectedValue, actualValue)
  514. }
  515. }
  516. }
  517. func TestKubePodContainerStatusRestartsTotalMetric(t *testing.T) {
  518. metric := newKubePodContainerStatusRestartsTotalMetric("kube_pod_container_status_restarts_total", "default", "test-pod", "pod-uid", "app-container", 3.0)
  519. // Test Desc method
  520. desc := metric.Desc()
  521. if desc == nil {
  522. t.Error("Expected non-nil descriptor")
  523. }
  524. // Test Write method
  525. var dtoMetric dto.Metric
  526. err := metric.Write(&dtoMetric)
  527. if err != nil {
  528. t.Errorf("Expected no error, got %v", err)
  529. }
  530. if dtoMetric.Counter == nil {
  531. t.Error("Expected counter metric")
  532. }
  533. if *dtoMetric.Counter.Value != 3.0 {
  534. t.Errorf("Expected counter value 3.0, got %f", *dtoMetric.Counter.Value)
  535. }
  536. }
  537. func TestKubePodContainerStatusTerminatedReasonMetric(t *testing.T) {
  538. metric := newKubePodContainerStatusTerminatedReasonMetric("kube_pod_container_status_terminated_reason", "default", "crashed-pod", "pod-uid", "failing-container", "Error")
  539. var dtoMetric dto.Metric
  540. err := metric.Write(&dtoMetric)
  541. if err != nil {
  542. t.Errorf("Expected no error, got %v", err)
  543. }
  544. if dtoMetric.Gauge == nil {
  545. t.Error("Expected gauge metric")
  546. }
  547. if *dtoMetric.Gauge.Value != 1.0 {
  548. t.Errorf("Expected gauge value 1.0, got %f", *dtoMetric.Gauge.Value)
  549. }
  550. // Check for reason label
  551. hasReason := false
  552. for _, label := range dtoMetric.Label {
  553. if *label.Name == "reason" && *label.Value == "Error" {
  554. hasReason = true
  555. break
  556. }
  557. }
  558. if !hasReason {
  559. t.Error("Expected reason label with value 'Error'")
  560. }
  561. }
  562. func TestKubePodStatusPhaseMetric(t *testing.T) {
  563. metric := newKubePodStatusPhaseMetric("kube_pod_status_phase", "default", "test-pod", "pod-uid", "Running", 1.0)
  564. var dtoMetric dto.Metric
  565. err := metric.Write(&dtoMetric)
  566. if err != nil {
  567. t.Errorf("Expected no error, got %v", err)
  568. }
  569. if dtoMetric.Gauge == nil {
  570. t.Error("Expected gauge metric")
  571. }
  572. // Check phase label
  573. hasPhase := false
  574. for _, label := range dtoMetric.Label {
  575. if *label.Name == "phase" && *label.Value == "Running" {
  576. hasPhase = true
  577. break
  578. }
  579. }
  580. if !hasPhase {
  581. t.Error("Expected phase label with value 'Running'")
  582. }
  583. }
  584. func TestKubePodContainerStatusRunningMetric(t *testing.T) {
  585. metric := newKubePodContainerStatusRunningMetric("kube_pod_container_status_running", "default", "running-pod", "pod-uid", "web-container")
  586. var dtoMetric dto.Metric
  587. err := metric.Write(&dtoMetric)
  588. if err != nil {
  589. t.Errorf("Expected no error, got %v", err)
  590. }
  591. if dtoMetric.Gauge == nil {
  592. t.Error("Expected gauge metric")
  593. }
  594. if *dtoMetric.Gauge.Value != 1.0 {
  595. t.Errorf("Expected gauge value 1.0, got %f", *dtoMetric.Gauge.Value)
  596. }
  597. }
  598. func TestKubePodContainerResourceRequestsMetric(t *testing.T) {
  599. metric := newKubePodContainerResourceRequestsMetric("kube_pod_container_resource_requests", "default", "test-pod", "pod-uid", "container1", "node1", "cpu", "core", 0.1)
  600. var dtoMetric dto.Metric
  601. err := metric.Write(&dtoMetric)
  602. if err != nil {
  603. t.Errorf("Expected no error, got %v", err)
  604. }
  605. if dtoMetric.Gauge == nil {
  606. t.Error("Expected gauge metric")
  607. }
  608. if *dtoMetric.Gauge.Value != 0.1 {
  609. t.Errorf("Expected gauge value 0.1, got %f", *dtoMetric.Gauge.Value)
  610. }
  611. // Verify all labels
  612. expectedLabels := map[string]string{
  613. "namespace": "default",
  614. "pod": "test-pod",
  615. "container": "container1",
  616. "uid": "pod-uid",
  617. "node": "node1",
  618. "resource": "cpu",
  619. "unit": "core",
  620. }
  621. actualLabels := make(map[string]string)
  622. for _, label := range dtoMetric.Label {
  623. actualLabels[*label.Name] = *label.Value
  624. }
  625. for key, expectedValue := range expectedLabels {
  626. if actualValue, ok := actualLabels[key]; !ok {
  627. t.Errorf("Missing label %s", key)
  628. } else if actualValue != expectedValue {
  629. t.Errorf("Label %s: expected %s, got %s", key, expectedValue, actualValue)
  630. }
  631. }
  632. }
  633. func TestKubePodContainerResourceLimitsMetric(t *testing.T) {
  634. metric := newKubePodContainerResourceLimitsMetric("kube_pod_container_resource_limits", "default", "test-pod", "pod-uid", "container1", "node1", "memory", "byte", 268435456)
  635. var dtoMetric dto.Metric
  636. err := metric.Write(&dtoMetric)
  637. if err != nil {
  638. t.Errorf("Expected no error, got %v", err)
  639. }
  640. if dtoMetric.Gauge == nil {
  641. t.Error("Expected gauge metric")
  642. }
  643. if *dtoMetric.Gauge.Value != 268435456 {
  644. t.Errorf("Expected gauge value 268435456, got %f", *dtoMetric.Gauge.Value)
  645. }
  646. }
  647. func TestKubePodContainerResourceLimitsCPUCoresMetric(t *testing.T) {
  648. metric := newKubePodContainerResourceLimitsCPUCoresMetric("kube_pod_container_resource_limits_cpu_cores", "default", "test-pod", "pod-uid", "container1", "node1", 2.0)
  649. var dtoMetric dto.Metric
  650. err := metric.Write(&dtoMetric)
  651. if err != nil {
  652. t.Errorf("Expected no error, got %v", err)
  653. }
  654. if dtoMetric.Gauge == nil {
  655. t.Error("Expected gauge metric")
  656. }
  657. if *dtoMetric.Gauge.Value != 2.0 {
  658. t.Errorf("Expected gauge value 2.0, got %f", *dtoMetric.Gauge.Value)
  659. }
  660. }
  661. func TestKubePodContainerResourceLimitsMemoryBytesMetric(t *testing.T) {
  662. metric := newKubePodContainerResourceLimitsMemoryBytesMetric("kube_pod_container_resource_limits_memory_bytes", "default", "test-pod", "pod-uid", "container1", "node1", 536870912)
  663. var dtoMetric dto.Metric
  664. err := metric.Write(&dtoMetric)
  665. if err != nil {
  666. t.Errorf("Expected no error, got %v", err)
  667. }
  668. if dtoMetric.Gauge == nil {
  669. t.Error("Expected gauge metric")
  670. }
  671. if *dtoMetric.Gauge.Value != 536870912 {
  672. t.Errorf("Expected gauge value 536870912, got %f", *dtoMetric.Gauge.Value)
  673. }
  674. }
  675. func TestKubePodOwnerMetric(t *testing.T) {
  676. metric := newKubePodOwnerMetric("kube_pod_owner", "default", "test-pod", "test-uid", "test-replicaset", "ReplicaSet", true)
  677. var dtoMetric dto.Metric
  678. err := metric.Write(&dtoMetric)
  679. if err != nil {
  680. t.Errorf("Expected no error, got %v", err)
  681. }
  682. if dtoMetric.Gauge == nil {
  683. t.Error("Expected gauge metric")
  684. }
  685. if *dtoMetric.Gauge.Value != 1.0 {
  686. t.Errorf("Expected gauge value 1.0, got %f", *dtoMetric.Gauge.Value)
  687. }
  688. // Verify owner-specific labels
  689. expectedLabels := map[string]string{
  690. "namespace": "default",
  691. "pod": "test-pod",
  692. "uid": "test-uid",
  693. "owner_name": "test-replicaset",
  694. "owner_kind": "ReplicaSet",
  695. "owner_is_controller": "true",
  696. }
  697. actualLabels := make(map[string]string)
  698. for _, label := range dtoMetric.Label {
  699. actualLabels[*label.Name] = *label.Value
  700. }
  701. for key, expectedValue := range expectedLabels {
  702. if actualValue, ok := actualLabels[key]; !ok {
  703. t.Errorf("Missing label %s", key)
  704. } else if actualValue != expectedValue {
  705. t.Errorf("Label %s: expected %s, got %s", key, expectedValue, actualValue)
  706. }
  707. }
  708. }
  709. func TestPodPhaseMetrics(t *testing.T) {
  710. // Test that all pod phases generate correct metrics
  711. pod := &clustercache.Pod{
  712. UID: types.UID("phase-test-uid"),
  713. Name: "phase-test-pod",
  714. Namespace: "default",
  715. Labels: map[string]string{},
  716. Status: clustercache.PodStatus{
  717. Phase: v1.PodRunning,
  718. },
  719. Spec: clustercache.PodSpec{},
  720. }
  721. mc := MetricsConfig{
  722. DisabledMetrics: []string{"kube_pod_labels"}, // Only test phase metrics
  723. }
  724. kpc := KubePodCollector{
  725. KubeClusterCache: NewFakePodCache([]*clustercache.Pod{pod}),
  726. metricsConfig: mc,
  727. }
  728. ch := make(chan prometheus.Metric, 10)
  729. kpc.Collect(ch)
  730. close(ch)
  731. phaseMetrics := make(map[string]float64)
  732. for metric := range ch {
  733. var dtoMetric dto.Metric
  734. metric.Write(&dtoMetric)
  735. for _, label := range dtoMetric.Label {
  736. if *label.Name == "phase" {
  737. phaseMetrics[*label.Value] = *dtoMetric.Gauge.Value
  738. }
  739. }
  740. }
  741. // Verify all phases are emitted
  742. expectedPhases := map[string]float64{
  743. "Pending": 0.0,
  744. "Succeeded": 0.0,
  745. "Failed": 0.0,
  746. "Unknown": 0.0,
  747. "Running": 1.0, // Only Running should be 1
  748. }
  749. for phase, expectedValue := range expectedPhases {
  750. if actualValue, ok := phaseMetrics[phase]; !ok {
  751. t.Errorf("Missing phase metric for %s", phase)
  752. } else if actualValue != expectedValue {
  753. t.Errorf("Phase %s: expected value %f, got %f", phase, expectedValue, actualValue)
  754. }
  755. }
  756. }
  757. // FakePodCache implements ClusterCache interface for testing
  758. type FakePodCache struct {
  759. clustercache.ClusterCache
  760. pods []*clustercache.Pod
  761. }
  762. func (f FakePodCache) GetAllPods() []*clustercache.Pod {
  763. return f.pods
  764. }
  765. func NewFakePodCache(pods []*clustercache.Pod) FakePodCache {
  766. return FakePodCache{
  767. pods: pods,
  768. }
  769. }