targetscraper_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. package scrape
  2. import (
  3. "fmt"
  4. "io"
  5. "reflect"
  6. "strings"
  7. "sync/atomic"
  8. "testing"
  9. "github.com/opencost/opencost/modules/collector-source/pkg/metric"
  10. "github.com/opencost/opencost/modules/collector-source/pkg/scrape/target"
  11. )
  12. const networkScape = `
  13. # HELP kubecost_pod_network_egress_bytes kubecost_pod_network_egress_bytes_total egressed byte counts by pod.
  14. # TYPE kubecost_pod_network_egress_bytes counter
  15. kubecost_pod_network_egress_bytes_total{pod_name="pod1",namespace="namespace1",internet="false",same_region="true",same_zone="true",service="service1"} 3127969647
  16. kubecost_pod_network_egress_bytes_total{pod_name="pod2",namespace="namespace1",internet="true",same_region="false",same_zone="false",service=""} 335188219
  17. # HELP kubecost_pod_network_ingress_bytes kubecost_pod_network_ingress_bytes_total ingressed byte counts by pod.
  18. # TYPE kubecost_pod_network_ingress_bytes counter
  19. kubecost_pod_network_ingress_bytes_total{pod_name="pod1",namespace="namespace1",internet="true",same_region="false",same_zone="false",service="service1"} 17941460
  20. kubecost_pod_network_ingress_bytes_total{pod_name="pod2",namespace="namespace1",internet="false",same_region="true",same_zone="false",service=""} 13948766
  21. # HELP kubecost_network_costs_parsed_entries kubecost_network_costs_parsed_entries total parsed conntrack entries.
  22. # TYPE kubecost_network_costs_parsed_entries gauge
  23. # HELP kubecost_network_costs_parse_time kubecost_network_costs_parse_time total time in milliseconds it took to parse conntrack entries.
  24. # TYPE kubecost_network_costs_parse_time gauge
  25. # EOF
  26. `
  27. const opencostScrape = `
  28. # HELP kubecost_cluster_management_cost kubecost_cluster_management_cost Hourly cost paid as a cluster management fee.
  29. # TYPE kubecost_cluster_management_cost gauge
  30. kubecost_cluster_management_cost{provisioner_name="GKE"} 0.1
  31. # HELP kubecost_network_zone_egress_cost kubecost_network_zone_egress_cost Total cost per GB egress across zones
  32. # TYPE kubecost_network_zone_egress_cost gauge
  33. kubecost_network_zone_egress_cost 0.01
  34. # HELP kubecost_network_region_egress_cost kubecost_network_region_egress_cost Total cost per GB egress across regions
  35. # TYPE kubecost_network_region_egress_cost gauge
  36. kubecost_network_region_egress_cost 0.01
  37. # HELP kubecost_network_internet_egress_cost kubecost_network_internet_egress_cost Total cost per GB of internet egress.
  38. # TYPE kubecost_network_internet_egress_cost gauge
  39. kubecost_network_internet_egress_cost 0.12
  40. # HELP pv_hourly_cost pv_hourly_cost Cost per GB per hour on a persistent disk
  41. # TYPE pv_hourly_cost gauge
  42. pv_hourly_cost{persistentvolume="pvc-1",provider_id="pvc-1",volumename="pvc-1"} 5.479452054794521e-05
  43. pv_hourly_cost{persistentvolume="pvc-2",provider_id="pvc-2",volumename="pvc-2"} 5.479452054794521e-05
  44. # HELP kubecost_load_balancer_cost kubecost_load_balancer_cost Hourly cost of load balancer
  45. # TYPE kubecost_load_balancer_cost gauge
  46. kubecost_load_balancer_cost{ingress_ip="127.0.0.1",namespace="namespace1",service_name="service1"} 0.025
  47. # HELP container_cpu_allocation container_cpu_allocation Percent of a single CPU used in a minute
  48. # TYPE container_cpu_allocation gauge
  49. # HELP node_total_hourly_cost node_total_hourly_cost Total node cost per hour
  50. # TYPE node_total_hourly_cost gauge
  51. node_total_hourly_cost{arch="amd64",instance="node1",instance_type="e2-standard-2",node="node1",provider_id="node1",region="region1"} 0.06631302438846588
  52. node_total_hourly_cost{arch="amd64",instance="node2",instance_type="e2-standard-2",node="node2",provider_id="node2",region="region1"} 0.06631302438846588
  53. # HELP node_cpu_hourly_cost node_cpu_hourly_cost hourly cost for each cpu on this node
  54. # TYPE node_cpu_hourly_cost gauge
  55. node_cpu_hourly_cost{arch="amd64",instance="node1",instance_type="e2-standard-2",node="node1",provider_id="node1",region="region1"} 0.021811590000000002
  56. node_cpu_hourly_cost{arch="amd64",instance="node2",instance_type="e2-standard-2",node="node2",provider_id="node2",region="region1"} 0.021811590000000002
  57. # HELP node_ram_hourly_cost node_ram_hourly_cost hourly cost for each gb of ram on this node
  58. # TYPE node_ram_hourly_cost gauge
  59. node_ram_hourly_cost{arch="amd64",instance="node1",instance_type="e2-standard-2",node="node1",provider_id="node1",region="region1"} 0.00292353
  60. node_ram_hourly_cost{arch="amd64",instance="node2",instance_type="e2-standard-2",node="node2",provider_id="node2",region="region1"} 0.00292353
  61. # HELP node_gpu_hourly_cost node_gpu_hourly_cost hourly cost for each gpu on this node
  62. # TYPE node_gpu_hourly_cost gauge
  63. node_gpu_hourly_cost{arch="amd64",instance="node1",instance_type="e2-standard-2",node="node1",provider_id="node1",region="region1"} 0
  64. node_gpu_hourly_cost{arch="amd64",instance="node2",instance_type="e2-standard-2",node="node2",provider_id="node2",region="region1"} 0
  65. # HELP node_gpu_count node_gpu_count count of gpu on this node
  66. # TYPE node_gpu_count gauge
  67. node_gpu_count{arch="amd64",instance="node1",instance_type="e2-standard-2",node="node1",provider_id="node1",region="region1"} 0
  68. node_gpu_count{arch="amd64",instance="node2",instance_type="e2-standard-2",node="node2",provider_id="node2",region="region1"} 0
  69. # HELP kubecost_node_is_spot kubecost_node_is_spot Cloud provider info about node preemptibility
  70. # TYPE kubecost_node_is_spot gauge
  71. kubecost_node_is_spot{arch="amd64",instance="node1",instance_type="e2-standard-2",node="node1",provider_id="node1",region="region1"} 0
  72. kubecost_node_is_spot{arch="amd64",instance="node2",instance_type="e2-standard-2",node="node2",provider_id="node2",region="region1"} 0
  73. # HELP ignore_fake_metric fake metric that the scrapper should ignore
  74. # TYPE ignore_fake_metric gauge
  75. ignore_fake_metric{container="container1",instance="node1",namespace="namespace1",node="node1",pod="pod1"} 0.02
  76. # HELP container_cpu_allocation container_cpu_allocation Percent of a single CPU used in a minute
  77. # TYPE container_cpu_allocation gauge
  78. container_cpu_allocation{container="container1",instance="node1",namespace="namespace1",node="node1",pod="pod1"} 0.02
  79. container_cpu_allocation{container="container2",instance="node2",namespace="namespace1",node="node2",pod="pod2"} 0.01
  80. # HELP container_memory_allocation_bytes container_memory_allocation_bytes Bytes of RAM used
  81. # TYPE container_memory_allocation_bytes gauge
  82. container_memory_allocation_bytes{container="container1",instance="node1",namespace="namespace1",node="node1",pod="pod1"} 1.1528192e+07
  83. container_memory_allocation_bytes{container="container2",instance="node2",namespace="namespace1",node="node2",pod="pod2"} 1e+07
  84. # HELP container_gpu_allocation container_gpu_allocation GPU used
  85. # TYPE container_gpu_allocation gauge
  86. container_gpu_allocation{container="container1",instance="node1",namespace="namespace1",node="node1",pod="pod1"} 0
  87. container_gpu_allocation{container="container2",instance="node2",namespace="namespace1",node="node2",pod="pod2"} 0
  88. # HELP pod_pvc_allocation pod_pvc_allocation Bytes used by a PVC attached to a pod
  89. # TYPE pod_pvc_allocation gauge
  90. pod_pvc_allocation{namespace="namespace1",persistentvolume="pvc-1",persistentvolumeclaim="pvc1",pod="pod1"} 3.4359738368e+10
  91. pod_pvc_allocation{namespace="namespace1",persistentvolume="pvc-2",persistentvolumeclaim="pvc2",pod="pod2"} 3.4359738368e+10
  92. `
  93. const enrichScrape = `
  94. # HELP test_metric test metric
  95. # TYPE test_metric gauge
  96. test_metric{name="a"} 1
  97. test_metric{name="b",extra="already-set"} 2
  98. `
  99. const dcgmScrape = `
  100. # HELP DCGM_FI_PROF_GR_ENGINE_ACTIVE Ratio of time the graphics engine is active.
  101. # TYPE DCGM_FI_PROF_GR_ENGINE_ACTIVE gauge
  102. DCGM_FI_PROF_GR_ENGINE_ACTIVE{gpu="0",UUID="GPU-1",pci_bus_id="00000000:00:0A.0",device="nvidia0",modelName="Tesla T4",Hostname="localhost"} 0.999999
  103. # HELP DCGM_FI_DEV_DEC_UTIL Decoder utilization (in %).
  104. # TYPE DCGM_FI_DEV_DEC_UTIL gauge
  105. DCGM_FI_DEV_DEC_UTIL{gpu="0",UUID="GPU-1",pci_bus_id="00000000:00:0A.0",device="nvidia0",modelName="Tesla T4",Hostname="localhost"} 0
  106. `
  107. type CloseableStringReader struct {
  108. *strings.Reader
  109. closed *atomic.Bool
  110. }
  111. func newCloseableStringReader(reader *strings.Reader, closed *atomic.Bool) *CloseableStringReader {
  112. return &CloseableStringReader{
  113. Reader: reader,
  114. closed: closed,
  115. }
  116. }
  117. func (csr *CloseableStringReader) Close() error {
  118. if csr.closed != nil {
  119. csr.closed.Store(true)
  120. }
  121. return nil
  122. }
  123. type CloseableStringTarget struct {
  124. sTarget *target.StringTarget
  125. closed *atomic.Bool
  126. }
  127. func newCloseableStringTarget(sTarget *target.StringTarget, closed *atomic.Bool) *CloseableStringTarget {
  128. return &CloseableStringTarget{
  129. sTarget: sTarget,
  130. closed: closed,
  131. }
  132. }
  133. func (cst *CloseableStringTarget) Load() (io.Reader, error) {
  134. reader, err := cst.sTarget.Load()
  135. if err != nil {
  136. return nil, err
  137. }
  138. sReader, ok := reader.(*strings.Reader)
  139. if !ok {
  140. return nil, fmt.Errorf("Reader was not a string reader")
  141. }
  142. return newCloseableStringReader(sReader, cst.closed), nil
  143. }
  144. func TestTargetScraper_Scrape(t *testing.T) {
  145. tests := []struct {
  146. name string
  147. scrapeText string
  148. targetScraperFactory func(provider target.TargetProvider) *TargetScraper
  149. expected []metric.Update
  150. }{
  151. {
  152. name: "Network Scrape",
  153. scrapeText: networkScape,
  154. targetScraperFactory: newNetworkTargetScraper,
  155. expected: []metric.Update{
  156. {
  157. Name: metric.KubecostPodNetworkEgressBytesTotal,
  158. Labels: map[string]string{
  159. "pod_name": "pod1",
  160. "namespace": "namespace1",
  161. "internet": "false",
  162. "same_region": "true",
  163. "same_zone": "true",
  164. "service": "service1",
  165. },
  166. Value: 3127969647,
  167. },
  168. {
  169. Name: metric.KubecostPodNetworkEgressBytesTotal,
  170. Labels: map[string]string{
  171. "pod_name": "pod2",
  172. "namespace": "namespace1",
  173. "internet": "true",
  174. "same_region": "false",
  175. "same_zone": "false",
  176. "service": "",
  177. },
  178. Value: 335188219,
  179. },
  180. {
  181. Name: metric.KubecostPodNetworkIngressBytesTotal,
  182. Labels: map[string]string{
  183. "pod_name": "pod1",
  184. "namespace": "namespace1",
  185. "internet": "true",
  186. "same_region": "false",
  187. "same_zone": "false",
  188. "service": "service1",
  189. },
  190. Value: 17941460,
  191. },
  192. {
  193. Name: metric.KubecostPodNetworkIngressBytesTotal,
  194. Labels: map[string]string{
  195. "pod_name": "pod2",
  196. "namespace": "namespace1",
  197. "internet": "false",
  198. "same_region": "true",
  199. "same_zone": "false",
  200. "service": "",
  201. },
  202. Value: 13948766,
  203. },
  204. },
  205. },
  206. {
  207. name: "Opencost Metric",
  208. scrapeText: opencostScrape,
  209. targetScraperFactory: newOpencostTargetScraper,
  210. expected: []metric.Update{
  211. {
  212. Name: metric.KubecostClusterManagementCost,
  213. Labels: map[string]string{
  214. "provisioner_name": "GKE",
  215. },
  216. Value: 0.1,
  217. },
  218. {
  219. Name: metric.KubecostNetworkZoneEgressCost,
  220. Value: 0.01,
  221. },
  222. {
  223. Name: metric.KubecostNetworkRegionEgressCost,
  224. Value: 0.01,
  225. },
  226. {
  227. Name: metric.KubecostNetworkInternetEgressCost,
  228. Value: 0.12,
  229. },
  230. {
  231. Name: metric.PVHourlyCost,
  232. Labels: map[string]string{
  233. "persistentvolume": "pvc-1",
  234. "provider_id": "pvc-1",
  235. "volumename": "pvc-1",
  236. },
  237. Value: 5.479452054794521e-05,
  238. },
  239. {
  240. Name: metric.PVHourlyCost,
  241. Labels: map[string]string{
  242. "persistentvolume": "pvc-2",
  243. "provider_id": "pvc-2",
  244. "volumename": "pvc-2",
  245. },
  246. Value: 5.479452054794521e-05,
  247. },
  248. {
  249. Name: metric.KubecostLoadBalancerCost,
  250. Labels: map[string]string{
  251. "ingress_ip": "127.0.0.1",
  252. "namespace": "namespace1",
  253. "service_name": "service1",
  254. },
  255. Value: 0.025,
  256. },
  257. {
  258. Name: metric.NodeTotalHourlyCost,
  259. Labels: map[string]string{
  260. "arch": "amd64",
  261. "instance": "node1",
  262. "instance_type": "e2-standard-2",
  263. "node": "node1",
  264. "provider_id": "node1",
  265. "region": "region1",
  266. },
  267. Value: 0.06631302438846588,
  268. },
  269. {
  270. Name: metric.NodeTotalHourlyCost,
  271. Labels: map[string]string{
  272. "arch": "amd64",
  273. "instance": "node2",
  274. "instance_type": "e2-standard-2",
  275. "node": "node2",
  276. "provider_id": "node2",
  277. "region": "region1",
  278. },
  279. Value: 0.06631302438846588,
  280. },
  281. {
  282. Name: metric.NodeCPUHourlyCost,
  283. Labels: map[string]string{
  284. "arch": "amd64",
  285. "instance": "node1",
  286. "instance_type": "e2-standard-2",
  287. "node": "node1",
  288. "provider_id": "node1",
  289. "region": "region1",
  290. },
  291. Value: 0.021811590000000002,
  292. },
  293. {
  294. Name: metric.NodeCPUHourlyCost,
  295. Labels: map[string]string{
  296. "arch": "amd64",
  297. "instance": "node2",
  298. "instance_type": "e2-standard-2",
  299. "node": "node2",
  300. "provider_id": "node2",
  301. "region": "region1",
  302. },
  303. Value: 0.021811590000000002,
  304. },
  305. {
  306. Name: metric.NodeRAMHourlyCost,
  307. Labels: map[string]string{
  308. "arch": "amd64",
  309. "instance": "node1",
  310. "instance_type": "e2-standard-2",
  311. "node": "node1",
  312. "provider_id": "node1",
  313. "region": "region1",
  314. },
  315. Value: 0.00292353,
  316. },
  317. {
  318. Name: metric.NodeRAMHourlyCost,
  319. Labels: map[string]string{
  320. "arch": "amd64",
  321. "instance": "node2",
  322. "instance_type": "e2-standard-2",
  323. "node": "node2",
  324. "provider_id": "node2",
  325. "region": "region1",
  326. },
  327. Value: 0.00292353,
  328. },
  329. {
  330. Name: metric.NodeGPUHourlyCost,
  331. Labels: map[string]string{
  332. "arch": "amd64",
  333. "instance": "node1",
  334. "instance_type": "e2-standard-2",
  335. "node": "node1",
  336. "provider_id": "node1",
  337. "region": "region1",
  338. },
  339. Value: 0,
  340. },
  341. {
  342. Name: metric.NodeGPUHourlyCost,
  343. Labels: map[string]string{
  344. "arch": "amd64",
  345. "instance": "node2",
  346. "instance_type": "e2-standard-2",
  347. "node": "node2",
  348. "provider_id": "node2",
  349. "region": "region1",
  350. },
  351. Value: 0,
  352. },
  353. {
  354. Name: metric.NodeGPUCount,
  355. Labels: map[string]string{
  356. "arch": "amd64",
  357. "instance": "node1",
  358. "instance_type": "e2-standard-2",
  359. "node": "node1",
  360. "provider_id": "node1",
  361. "region": "region1",
  362. },
  363. Value: 0,
  364. },
  365. {
  366. Name: metric.NodeGPUCount,
  367. Labels: map[string]string{
  368. "arch": "amd64",
  369. "instance": "node2",
  370. "instance_type": "e2-standard-2",
  371. "node": "node2",
  372. "provider_id": "node2",
  373. "region": "region1",
  374. },
  375. Value: 0,
  376. },
  377. {
  378. Name: metric.KubecostNodeIsSpot,
  379. Labels: map[string]string{
  380. "arch": "amd64",
  381. "instance": "node1",
  382. "instance_type": "e2-standard-2",
  383. "node": "node1",
  384. "provider_id": "node1",
  385. "region": "region1",
  386. },
  387. Value: 0,
  388. },
  389. {
  390. Name: metric.KubecostNodeIsSpot,
  391. Labels: map[string]string{
  392. "arch": "amd64",
  393. "instance": "node2",
  394. "instance_type": "e2-standard-2",
  395. "node": "node2",
  396. "provider_id": "node2",
  397. "region": "region1",
  398. },
  399. Value: 0,
  400. },
  401. },
  402. },
  403. {
  404. name: "GPU Metric",
  405. scrapeText: dcgmScrape,
  406. targetScraperFactory: func(provider target.TargetProvider) *TargetScraper {
  407. return newDCGMTargetScraper(provider, nil)
  408. },
  409. expected: []metric.Update{
  410. {
  411. Name: metric.DCGMFIPROFGRENGINEACTIVE,
  412. Labels: map[string]string{
  413. "gpu": "0",
  414. "UUID": "GPU-1",
  415. "pci_bus_id": "00000000:00:0A.0",
  416. "device": "nvidia0",
  417. "modelName": "Tesla T4",
  418. "Hostname": "localhost",
  419. },
  420. Value: 0.999999,
  421. },
  422. {
  423. Name: metric.DCGMFIDEVDECUTIL,
  424. Labels: map[string]string{
  425. "gpu": "0",
  426. "UUID": "GPU-1",
  427. "pci_bus_id": "00000000:00:0A.0",
  428. "device": "nvidia0",
  429. "modelName": "Tesla T4",
  430. "Hostname": "localhost",
  431. },
  432. Value: 0,
  433. },
  434. },
  435. },
  436. {
  437. name: "Enrichment",
  438. scrapeText: enrichScrape,
  439. targetScraperFactory: func(provider target.TargetProvider) *TargetScraper {
  440. enrich := func(updates []metric.Update) {
  441. for i := range updates {
  442. if updates[i].Labels["extra"] != "" {
  443. continue
  444. }
  445. updates[i].Labels["extra"] = "enriched"
  446. }
  447. }
  448. return newTargetScrapper("test-enrich", provider, nil, false, enrich)
  449. },
  450. expected: []metric.Update{
  451. {
  452. Name: "test_metric",
  453. Labels: map[string]string{
  454. "name": "a",
  455. "extra": "enriched",
  456. },
  457. Value: 1,
  458. },
  459. {
  460. Name: "test_metric",
  461. Labels: map[string]string{
  462. "name": "b",
  463. "extra": "already-set",
  464. },
  465. Value: 2,
  466. },
  467. },
  468. },
  469. }
  470. for _, tt := range tests {
  471. t.Run(tt.name, func(t *testing.T) {
  472. closed := new(atomic.Bool)
  473. for i := range 2 {
  474. var sTarget target.ScrapeTarget
  475. if i == 0 {
  476. sTarget = target.NewStringTarget(tt.scrapeText)
  477. } else {
  478. sTarget = newCloseableStringTarget(target.NewStringTarget(tt.scrapeText), closed)
  479. }
  480. scraper := tt.targetScraperFactory(target.NewDefaultTargetProvider(sTarget))
  481. scrapeResults := scraper.Scrape()
  482. if len(scrapeResults) != len(tt.expected) {
  483. t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
  484. }
  485. for i, expected := range tt.expected {
  486. got := scrapeResults[i]
  487. if !reflect.DeepEqual(expected, got) {
  488. t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
  489. }
  490. }
  491. }
  492. if !closed.Load() {
  493. t.Errorf("Closeable target did not call Close on Scrape()")
  494. t.Fail()
  495. }
  496. })
  497. }
  498. }