targetscraper_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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 dcgmScrape = `
  94. # HELP DCGM_FI_PROF_GR_ENGINE_ACTIVE Ratio of time the graphics engine is active.
  95. # TYPE DCGM_FI_PROF_GR_ENGINE_ACTIVE gauge
  96. 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
  97. # HELP DCGM_FI_DEV_DEC_UTIL Decoder utilization (in %).
  98. # TYPE DCGM_FI_DEV_DEC_UTIL gauge
  99. 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
  100. `
  101. type CloseableStringReader struct {
  102. *strings.Reader
  103. closed *atomic.Bool
  104. }
  105. func newCloseableStringReader(reader *strings.Reader, closed *atomic.Bool) *CloseableStringReader {
  106. return &CloseableStringReader{
  107. Reader: reader,
  108. closed: closed,
  109. }
  110. }
  111. func (csr *CloseableStringReader) Close() error {
  112. if csr.closed != nil {
  113. csr.closed.Store(true)
  114. }
  115. return nil
  116. }
  117. type CloseableStringTarget struct {
  118. sTarget *target.StringTarget
  119. closed *atomic.Bool
  120. }
  121. func newCloseableStringTarget(sTarget *target.StringTarget, closed *atomic.Bool) *CloseableStringTarget {
  122. return &CloseableStringTarget{
  123. sTarget: sTarget,
  124. closed: closed,
  125. }
  126. }
  127. func (cst *CloseableStringTarget) Load() (io.Reader, error) {
  128. reader, err := cst.sTarget.Load()
  129. if err != nil {
  130. return nil, err
  131. }
  132. sReader, ok := reader.(*strings.Reader)
  133. if !ok {
  134. return nil, fmt.Errorf("Reader was not a string reader")
  135. }
  136. return newCloseableStringReader(sReader, cst.closed), nil
  137. }
  138. func TestTargetScraper_Scrape(t *testing.T) {
  139. tests := []struct {
  140. name string
  141. scrapeText string
  142. targetScraperFactory func(provider target.TargetProvider) *TargetScraper
  143. expected []metric.Update
  144. }{
  145. {
  146. name: "Network Scrape",
  147. scrapeText: networkScape,
  148. targetScraperFactory: newNetworkTargetScraper,
  149. expected: []metric.Update{
  150. {
  151. Name: metric.KubecostPodNetworkEgressBytesTotal,
  152. Labels: map[string]string{
  153. "pod_name": "pod1",
  154. "namespace": "namespace1",
  155. "internet": "false",
  156. "same_region": "true",
  157. "same_zone": "true",
  158. "service": "service1",
  159. },
  160. Value: 3127969647,
  161. },
  162. {
  163. Name: metric.KubecostPodNetworkEgressBytesTotal,
  164. Labels: map[string]string{
  165. "pod_name": "pod2",
  166. "namespace": "namespace1",
  167. "internet": "true",
  168. "same_region": "false",
  169. "same_zone": "false",
  170. "service": "",
  171. },
  172. Value: 335188219,
  173. },
  174. {
  175. Name: metric.KubecostPodNetworkIngressBytesTotal,
  176. Labels: map[string]string{
  177. "pod_name": "pod1",
  178. "namespace": "namespace1",
  179. "internet": "true",
  180. "same_region": "false",
  181. "same_zone": "false",
  182. "service": "service1",
  183. },
  184. Value: 17941460,
  185. },
  186. {
  187. Name: metric.KubecostPodNetworkIngressBytesTotal,
  188. Labels: map[string]string{
  189. "pod_name": "pod2",
  190. "namespace": "namespace1",
  191. "internet": "false",
  192. "same_region": "true",
  193. "same_zone": "false",
  194. "service": "",
  195. },
  196. Value: 13948766,
  197. },
  198. },
  199. },
  200. {
  201. name: "Opencost Metric",
  202. scrapeText: opencostScrape,
  203. targetScraperFactory: newOpencostTargetScraper,
  204. expected: []metric.Update{
  205. {
  206. Name: metric.KubecostClusterManagementCost,
  207. Labels: map[string]string{
  208. "provisioner_name": "GKE",
  209. },
  210. Value: 0.1,
  211. },
  212. {
  213. Name: metric.KubecostNetworkZoneEgressCost,
  214. Value: 0.01,
  215. },
  216. {
  217. Name: metric.KubecostNetworkRegionEgressCost,
  218. Value: 0.01,
  219. },
  220. {
  221. Name: metric.KubecostNetworkInternetEgressCost,
  222. Value: 0.12,
  223. },
  224. {
  225. Name: metric.PVHourlyCost,
  226. Labels: map[string]string{
  227. "persistentvolume": "pvc-1",
  228. "provider_id": "pvc-1",
  229. "volumename": "pvc-1",
  230. },
  231. Value: 5.479452054794521e-05,
  232. },
  233. {
  234. Name: metric.PVHourlyCost,
  235. Labels: map[string]string{
  236. "persistentvolume": "pvc-2",
  237. "provider_id": "pvc-2",
  238. "volumename": "pvc-2",
  239. },
  240. Value: 5.479452054794521e-05,
  241. },
  242. {
  243. Name: metric.KubecostLoadBalancerCost,
  244. Labels: map[string]string{
  245. "ingress_ip": "127.0.0.1",
  246. "namespace": "namespace1",
  247. "service_name": "service1",
  248. },
  249. Value: 0.025,
  250. },
  251. {
  252. Name: metric.NodeTotalHourlyCost,
  253. Labels: map[string]string{
  254. "arch": "amd64",
  255. "instance": "node1",
  256. "instance_type": "e2-standard-2",
  257. "node": "node1",
  258. "provider_id": "node1",
  259. "region": "region1",
  260. },
  261. Value: 0.06631302438846588,
  262. },
  263. {
  264. Name: metric.NodeTotalHourlyCost,
  265. Labels: map[string]string{
  266. "arch": "amd64",
  267. "instance": "node2",
  268. "instance_type": "e2-standard-2",
  269. "node": "node2",
  270. "provider_id": "node2",
  271. "region": "region1",
  272. },
  273. Value: 0.06631302438846588,
  274. },
  275. {
  276. Name: metric.NodeCPUHourlyCost,
  277. Labels: map[string]string{
  278. "arch": "amd64",
  279. "instance": "node1",
  280. "instance_type": "e2-standard-2",
  281. "node": "node1",
  282. "provider_id": "node1",
  283. "region": "region1",
  284. },
  285. Value: 0.021811590000000002,
  286. },
  287. {
  288. Name: metric.NodeCPUHourlyCost,
  289. Labels: map[string]string{
  290. "arch": "amd64",
  291. "instance": "node2",
  292. "instance_type": "e2-standard-2",
  293. "node": "node2",
  294. "provider_id": "node2",
  295. "region": "region1",
  296. },
  297. Value: 0.021811590000000002,
  298. },
  299. {
  300. Name: metric.NodeRAMHourlyCost,
  301. Labels: map[string]string{
  302. "arch": "amd64",
  303. "instance": "node1",
  304. "instance_type": "e2-standard-2",
  305. "node": "node1",
  306. "provider_id": "node1",
  307. "region": "region1",
  308. },
  309. Value: 0.00292353,
  310. },
  311. {
  312. Name: metric.NodeRAMHourlyCost,
  313. Labels: map[string]string{
  314. "arch": "amd64",
  315. "instance": "node2",
  316. "instance_type": "e2-standard-2",
  317. "node": "node2",
  318. "provider_id": "node2",
  319. "region": "region1",
  320. },
  321. Value: 0.00292353,
  322. },
  323. {
  324. Name: metric.NodeGPUHourlyCost,
  325. Labels: map[string]string{
  326. "arch": "amd64",
  327. "instance": "node1",
  328. "instance_type": "e2-standard-2",
  329. "node": "node1",
  330. "provider_id": "node1",
  331. "region": "region1",
  332. },
  333. Value: 0,
  334. },
  335. {
  336. Name: metric.NodeGPUHourlyCost,
  337. Labels: map[string]string{
  338. "arch": "amd64",
  339. "instance": "node2",
  340. "instance_type": "e2-standard-2",
  341. "node": "node2",
  342. "provider_id": "node2",
  343. "region": "region1",
  344. },
  345. Value: 0,
  346. },
  347. {
  348. Name: metric.NodeGPUCount,
  349. Labels: map[string]string{
  350. "arch": "amd64",
  351. "instance": "node1",
  352. "instance_type": "e2-standard-2",
  353. "node": "node1",
  354. "provider_id": "node1",
  355. "region": "region1",
  356. },
  357. Value: 0,
  358. },
  359. {
  360. Name: metric.NodeGPUCount,
  361. Labels: map[string]string{
  362. "arch": "amd64",
  363. "instance": "node2",
  364. "instance_type": "e2-standard-2",
  365. "node": "node2",
  366. "provider_id": "node2",
  367. "region": "region1",
  368. },
  369. Value: 0,
  370. },
  371. {
  372. Name: metric.KubecostNodeIsSpot,
  373. Labels: map[string]string{
  374. "arch": "amd64",
  375. "instance": "node1",
  376. "instance_type": "e2-standard-2",
  377. "node": "node1",
  378. "provider_id": "node1",
  379. "region": "region1",
  380. },
  381. Value: 0,
  382. },
  383. {
  384. Name: metric.KubecostNodeIsSpot,
  385. Labels: map[string]string{
  386. "arch": "amd64",
  387. "instance": "node2",
  388. "instance_type": "e2-standard-2",
  389. "node": "node2",
  390. "provider_id": "node2",
  391. "region": "region1",
  392. },
  393. Value: 0,
  394. },
  395. },
  396. },
  397. {
  398. name: "GPU Metric",
  399. scrapeText: dcgmScrape,
  400. targetScraperFactory: newDCGMTargetScraper,
  401. expected: []metric.Update{
  402. {
  403. Name: metric.DCGMFIPROFGRENGINEACTIVE,
  404. Labels: map[string]string{
  405. "gpu": "0",
  406. "UUID": "GPU-1",
  407. "pci_bus_id": "00000000:00:0A.0",
  408. "device": "nvidia0",
  409. "modelName": "Tesla T4",
  410. "Hostname": "localhost",
  411. },
  412. Value: 0.999999,
  413. },
  414. {
  415. Name: metric.DCGMFIDEVDECUTIL,
  416. Labels: map[string]string{
  417. "gpu": "0",
  418. "UUID": "GPU-1",
  419. "pci_bus_id": "00000000:00:0A.0",
  420. "device": "nvidia0",
  421. "modelName": "Tesla T4",
  422. "Hostname": "localhost",
  423. },
  424. Value: 0,
  425. },
  426. },
  427. },
  428. }
  429. for _, tt := range tests {
  430. t.Run(tt.name, func(t *testing.T) {
  431. closed := new(atomic.Bool)
  432. for i := range 2 {
  433. var sTarget target.ScrapeTarget
  434. if i == 0 {
  435. sTarget = target.NewStringTarget(tt.scrapeText)
  436. } else {
  437. sTarget = newCloseableStringTarget(target.NewStringTarget(tt.scrapeText), closed)
  438. }
  439. scraper := tt.targetScraperFactory(target.NewDefaultTargetProvider(sTarget))
  440. scrapeResults := scraper.Scrape()
  441. if len(scrapeResults) != len(tt.expected) {
  442. t.Errorf("Expected result length of %d, got %d", len(tt.expected), len(scrapeResults))
  443. }
  444. for i, expected := range tt.expected {
  445. got := scrapeResults[i]
  446. if !reflect.DeepEqual(expected, got) {
  447. t.Errorf("Result did not match expected at index %d: got %v, want %v", i, got, expected)
  448. }
  449. }
  450. }
  451. if !closed.Load() {
  452. t.Errorf("Closeable target did not call Close on Scrape()")
  453. t.Fail()
  454. }
  455. })
  456. }
  457. }