statefulsetmetrics_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  8. "k8s.io/apimachinery/pkg/types"
  9. )
  10. func TestKubecostStatefulsetCollector_Describe(t *testing.T) {
  11. tests := []struct {
  12. name string
  13. disabledMetrics []string
  14. expectMetric bool
  15. }{
  16. {
  17. name: "statefulSet_match_labels enabled",
  18. disabledMetrics: []string{},
  19. expectMetric: true,
  20. },
  21. {
  22. name: "statefulSet_match_labels disabled",
  23. disabledMetrics: []string{"statefulSet_match_labels"},
  24. expectMetric: false,
  25. },
  26. }
  27. for _, tt := range tests {
  28. t.Run(tt.name, func(t *testing.T) {
  29. mc := MetricsConfig{
  30. DisabledMetrics: tt.disabledMetrics,
  31. }
  32. sc := KubecostStatefulsetCollector{
  33. KubeClusterCache: NewFakeStatefulsetCache([]*clustercache.StatefulSet{}),
  34. metricsConfig: mc,
  35. }
  36. ch := make(chan *prometheus.Desc, 10)
  37. sc.Describe(ch)
  38. close(ch)
  39. count := 0
  40. for range ch {
  41. count++
  42. }
  43. if tt.expectMetric && count == 0 {
  44. t.Error("Expected metric description but got none")
  45. }
  46. if !tt.expectMetric && count > 0 {
  47. t.Error("Expected no metric description but got some")
  48. }
  49. })
  50. }
  51. }
  52. func TestKubecostStatefulsetCollector_Collect(t *testing.T) {
  53. tests := []struct {
  54. name string
  55. statefulsets []*clustercache.StatefulSet
  56. disabledMetrics []string
  57. expectedCount int
  58. }{
  59. {
  60. name: "single statefulset with match labels",
  61. statefulsets: []*clustercache.StatefulSet{
  62. {
  63. UID: types.UID("test-uid-1"),
  64. Name: "test-statefulset",
  65. Namespace: "default",
  66. SpecSelector: &metav1.LabelSelector{
  67. MatchLabels: map[string]string{"app": "test", "version": "v1"},
  68. },
  69. },
  70. },
  71. disabledMetrics: []string{},
  72. expectedCount: 1,
  73. },
  74. {
  75. name: "statefulset without match labels",
  76. statefulsets: []*clustercache.StatefulSet{
  77. {
  78. UID: types.UID("test-uid-2"),
  79. Name: "empty-statefulset",
  80. Namespace: "default",
  81. SpecSelector: &metav1.LabelSelector{
  82. MatchLabels: map[string]string{},
  83. },
  84. },
  85. },
  86. disabledMetrics: []string{},
  87. expectedCount: 0,
  88. },
  89. {
  90. name: "statefulset with nil selector",
  91. statefulsets: []*clustercache.StatefulSet{
  92. {
  93. UID: types.UID("test-uid-3"),
  94. Name: "nil-selector-statefulset",
  95. Namespace: "default",
  96. SpecSelector: nil,
  97. },
  98. },
  99. disabledMetrics: []string{},
  100. expectedCount: 0,
  101. },
  102. {
  103. name: "multiple statefulsets with match labels",
  104. statefulsets: []*clustercache.StatefulSet{
  105. {
  106. UID: types.UID("test-uid-4"),
  107. Name: "statefulset1",
  108. Namespace: "ns1",
  109. SpecSelector: &metav1.LabelSelector{
  110. MatchLabels: map[string]string{"app": "app1"},
  111. },
  112. },
  113. {
  114. UID: types.UID("test-uid-5"),
  115. Name: "statefulset2",
  116. Namespace: "ns2",
  117. SpecSelector: &metav1.LabelSelector{
  118. MatchLabels: map[string]string{"component": "database"},
  119. },
  120. },
  121. },
  122. disabledMetrics: []string{},
  123. expectedCount: 2,
  124. },
  125. {
  126. name: "metric disabled",
  127. statefulsets: []*clustercache.StatefulSet{
  128. {
  129. UID: types.UID("test-uid-6"),
  130. Name: "test-statefulset",
  131. Namespace: "default",
  132. SpecSelector: &metav1.LabelSelector{
  133. MatchLabels: map[string]string{"app": "test"},
  134. },
  135. },
  136. },
  137. disabledMetrics: []string{"statefulSet_match_labels"},
  138. expectedCount: 0,
  139. },
  140. }
  141. for _, tt := range tests {
  142. t.Run(tt.name, func(t *testing.T) {
  143. mc := MetricsConfig{
  144. DisabledMetrics: tt.disabledMetrics,
  145. }
  146. sc := KubecostStatefulsetCollector{
  147. KubeClusterCache: NewFakeStatefulsetCache(tt.statefulsets),
  148. metricsConfig: mc,
  149. }
  150. ch := make(chan prometheus.Metric, 10)
  151. sc.Collect(ch)
  152. close(ch)
  153. count := 0
  154. for range ch {
  155. count++
  156. }
  157. if count != tt.expectedCount {
  158. t.Errorf("Expected %d metrics, got %d", tt.expectedCount, count)
  159. }
  160. })
  161. }
  162. }
  163. func TestStatefulsetMatchLabelsMetric(t *testing.T) {
  164. labelNames := []string{"app", "version"}
  165. labelValues := []string{"test-app", "v1.0"}
  166. uid := "test-uid"
  167. metric := newStatefulsetMatchLabelsMetric("test-statefulset", "default", "statefulSet_match_labels", labelNames, labelValues, uid)
  168. // Test Desc method
  169. desc := metric.Desc()
  170. if desc == nil {
  171. t.Error("Expected non-nil descriptor")
  172. }
  173. // Test Write method
  174. var dtoMetric dto.Metric
  175. err := metric.Write(&dtoMetric)
  176. if err != nil {
  177. t.Errorf("Expected no error, got %v", err)
  178. }
  179. if dtoMetric.Gauge == nil {
  180. t.Error("Expected gauge metric")
  181. }
  182. if *dtoMetric.Gauge.Value != 1.0 {
  183. t.Errorf("Expected gauge value 1.0, got %f", *dtoMetric.Gauge.Value)
  184. }
  185. // Verify labels
  186. expectedLabels := map[string]string{
  187. "app": "test-app",
  188. "version": "v1.0",
  189. "statefulSet": "test-statefulset",
  190. "namespace": "default",
  191. "uid": uid,
  192. }
  193. actualLabels := make(map[string]string)
  194. for _, label := range dtoMetric.Label {
  195. actualLabels[*label.Name] = *label.Value
  196. }
  197. for key, expectedValue := range expectedLabels {
  198. if actualValue, ok := actualLabels[key]; !ok {
  199. t.Errorf("Missing label %s", key)
  200. } else if actualValue != expectedValue {
  201. t.Errorf("Label %s: expected %s, got %s", key, expectedValue, actualValue)
  202. }
  203. }
  204. }
  205. func TestStatefulsetMatchLabelsMetric_EmptyLabels(t *testing.T) {
  206. metric := newStatefulsetMatchLabelsMetric("empty-statefulset", "test-ns", "statefulSet_match_labels", []string{}, []string{}, "empty-uid")
  207. var dtoMetric dto.Metric
  208. err := metric.Write(&dtoMetric)
  209. if err != nil {
  210. t.Errorf("Expected no error, got %v", err)
  211. }
  212. // Should still have the statefulset metadata labels
  213. expectedCount := 3 // statefulSet, namespace, uid
  214. if len(dtoMetric.Label) != expectedCount {
  215. t.Errorf("Expected %d labels, got %d", expectedCount, len(dtoMetric.Label))
  216. }
  217. }
  218. func TestStatefulsetMatchLabelsMetric_MissingFields(t *testing.T) {
  219. tests := []struct {
  220. name string
  221. statefulsetName string
  222. namespace string
  223. uid string
  224. }{
  225. {
  226. name: "empty statefulset name",
  227. statefulsetName: "",
  228. namespace: "test-ns",
  229. uid: "test-uid",
  230. },
  231. {
  232. name: "empty namespace",
  233. statefulsetName: "test-statefulset",
  234. namespace: "",
  235. uid: "test-uid",
  236. },
  237. {
  238. name: "empty uid",
  239. statefulsetName: "test-statefulset",
  240. namespace: "test-ns",
  241. uid: "",
  242. },
  243. }
  244. for _, tt := range tests {
  245. t.Run(tt.name, func(t *testing.T) {
  246. metric := newStatefulsetMatchLabelsMetric(tt.statefulsetName, tt.namespace, "statefulSet_match_labels", []string{}, []string{}, tt.uid)
  247. var dtoMetric dto.Metric
  248. err := metric.Write(&dtoMetric)
  249. if err != nil {
  250. t.Errorf("Expected no error, got %v", err)
  251. }
  252. // Should still create the metric with empty values
  253. if len(dtoMetric.Label) != 3 {
  254. t.Errorf("Expected 3 labels, got %d", len(dtoMetric.Label))
  255. }
  256. })
  257. }
  258. }
  259. // FakeStatefulsetCache implements ClusterCache interface for testing
  260. type FakeStatefulsetCache struct {
  261. clustercache.ClusterCache
  262. statefulsets []*clustercache.StatefulSet
  263. }
  264. func (f FakeStatefulsetCache) GetAllStatefulSets() []*clustercache.StatefulSet {
  265. return f.statefulsets
  266. }
  267. func NewFakeStatefulsetCache(statefulsets []*clustercache.StatefulSet) FakeStatefulsetCache {
  268. return FakeStatefulsetCache{
  269. statefulsets: statefulsets,
  270. }
  271. }