cluster.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. package costmodel
  2. import (
  3. "fmt"
  4. "time"
  5. costAnalyzerCloud "github.com/kubecost/cost-model/cloud"
  6. prometheusClient "github.com/prometheus/client_golang/api"
  7. "k8s.io/klog"
  8. )
  9. const (
  10. queryClusterCores = `sum(
  11. avg(kube_node_status_capacity_cpu_cores %s) by (node) * avg(node_cpu_hourly_cost %s) by (node) * 730 +
  12. avg(node_gpu_hourly_cost %s) by (node) * 730
  13. )`
  14. queryClusterRAM = `sum(
  15. avg(kube_node_status_capacity_memory_bytes %s) by (node) / 1024 / 1024 / 1024 * avg(node_ram_hourly_cost %s) by (node) * 730
  16. )`
  17. queryStorage = `sum(
  18. avg(avg_over_time(pv_hourly_cost[%s] %s)) by (persistentvolume) * 730
  19. * avg(avg_over_time(kube_persistentvolume_capacity_bytes[%s] %s)) by (persistentvolume) / 1024 / 1024 / 1024
  20. ) %s`
  21. queryTotal = `sum(avg(node_total_hourly_cost) by (node)) * 730 +
  22. sum(
  23. avg(avg_over_time(pv_hourly_cost[1h])) by (persistentvolume) * 730
  24. * avg(avg_over_time(kube_persistentvolume_capacity_bytes[1h])) by (persistentvolume) / 1024 / 1024 / 1024
  25. ) %s`
  26. )
  27. type Totals struct {
  28. TotalCost [][]string `json:"totalcost"`
  29. CPUCost [][]string `json:"cpucost"`
  30. MemCost [][]string `json:"memcost"`
  31. StorageCost [][]string `json:"storageCost"`
  32. }
  33. func resultToTotals(qr interface{}) ([][]string, error) {
  34. data, ok := qr.(map[string]interface{})["data"]
  35. if !ok {
  36. return nil, fmt.Errorf("Improperly formatted response from prometheus, response %+v has no data field", data)
  37. }
  38. r, ok := data.(map[string]interface{})["result"]
  39. if !ok {
  40. return nil, fmt.Errorf("Improperly formatted data from prometheus, data has no result field")
  41. }
  42. results, ok := r.([]interface{})
  43. if !ok {
  44. return nil, fmt.Errorf("Improperly formatted results from prometheus, result field is not a slice")
  45. }
  46. if len(results) == 0 {
  47. return nil, fmt.Errorf("Not enough data available in the selected time range")
  48. }
  49. res, ok := results[0].(map[string]interface{})["values"]
  50. totals := [][]string{}
  51. for _, val := range res.([]interface{}) {
  52. if !ok {
  53. return nil, fmt.Errorf("Improperly formatted results from prometheus, value is not a field in the vector")
  54. }
  55. dataPoint, ok := val.([]interface{})
  56. if !ok || len(dataPoint) != 2 {
  57. return nil, fmt.Errorf("Improperly formatted datapoint from Prometheus")
  58. }
  59. d0 := fmt.Sprintf("%f", dataPoint[0].(float64))
  60. toAppend := []string{
  61. d0,
  62. dataPoint[1].(string),
  63. }
  64. totals = append(totals, toAppend)
  65. }
  66. return totals, nil
  67. }
  68. func resultToTotal(qr interface{}) ([][]string, error) {
  69. data, ok := qr.(map[string]interface{})["data"]
  70. if !ok {
  71. return nil, fmt.Errorf("Improperly formatted response from prometheus, response %+v has no data field", data)
  72. }
  73. r, ok := data.(map[string]interface{})["result"]
  74. if !ok {
  75. return nil, fmt.Errorf("Improperly formatted data from prometheus, data has no result field")
  76. }
  77. results, ok := r.([]interface{})
  78. if !ok {
  79. return nil, fmt.Errorf("Improperly formatted results from prometheus, result field is not a slice")
  80. }
  81. if len(results) == 0 {
  82. return nil, fmt.Errorf("Not enough data available in the selected time range")
  83. }
  84. val, ok := results[0].(map[string]interface{})["value"]
  85. totals := [][]string{}
  86. if !ok {
  87. return nil, fmt.Errorf("Improperly formatted results from prometheus, value is not a field in the vector")
  88. }
  89. dataPoint, ok := val.([]interface{})
  90. if !ok || len(dataPoint) != 2 {
  91. return nil, fmt.Errorf("Improperly formatted datapoint from Prometheus")
  92. }
  93. d0 := fmt.Sprintf("%f", dataPoint[0].(float64))
  94. toAppend := []string{
  95. d0,
  96. dataPoint[1].(string),
  97. }
  98. totals = append(totals, toAppend)
  99. return totals, nil
  100. }
  101. // ClusterCostsOverTime gives the current full cluster costs averaged over a window of time.
  102. func ClusterCosts(cli prometheusClient.Client, cloud costAnalyzerCloud.Provider, windowString, offset string) (*Totals, error) {
  103. localStorageQuery, err := cloud.GetLocalStorageQuery()
  104. if err != nil {
  105. return nil, err
  106. }
  107. if localStorageQuery != "" {
  108. localStorageQuery = fmt.Sprintf("+ %s", localStorageQuery)
  109. }
  110. qCores := fmt.Sprintf(queryClusterCores, offset, offset, offset)
  111. qRAM := fmt.Sprintf(queryClusterRAM, offset, offset)
  112. qStorage := fmt.Sprintf(queryStorage, windowString, offset, windowString, offset, localStorageQuery)
  113. qTotal := fmt.Sprintf(queryTotal, localStorageQuery)
  114. resultClusterCores, err := query(cli, qCores)
  115. if err != nil {
  116. return nil, err
  117. }
  118. resultClusterRAM, err := query(cli, qRAM)
  119. if err != nil {
  120. return nil, err
  121. }
  122. resultStorage, err := query(cli, qStorage)
  123. if err != nil {
  124. return nil, err
  125. }
  126. resultTotal, err := query(cli, qTotal)
  127. if err != nil {
  128. return nil, err
  129. }
  130. coreTotal, err := resultToTotal(resultClusterCores)
  131. if err != nil {
  132. return nil, err
  133. }
  134. ramTotal, err := resultToTotal(resultClusterRAM)
  135. if err != nil {
  136. return nil, err
  137. }
  138. storageTotal, err := resultToTotal(resultStorage)
  139. if err != nil {
  140. return nil, err
  141. }
  142. clusterTotal, err := resultToTotal(resultTotal)
  143. if err != nil {
  144. return nil, err
  145. }
  146. return &Totals{
  147. TotalCost: clusterTotal,
  148. CPUCost: coreTotal,
  149. MemCost: ramTotal,
  150. StorageCost: storageTotal,
  151. }, nil
  152. }
  153. // ClusterCostsOverTime gives the full cluster costs over time
  154. func ClusterCostsOverTime(cli prometheusClient.Client, cloud costAnalyzerCloud.Provider, startString, endString, windowString, offset string) (*Totals, error) {
  155. localStorageQuery, err := cloud.GetLocalStorageQuery()
  156. if err != nil {
  157. return nil, err
  158. }
  159. if localStorageQuery != "" {
  160. localStorageQuery = fmt.Sprintf("+ %s", localStorageQuery)
  161. }
  162. layout := "2006-01-02T15:04:05.000Z"
  163. start, err := time.Parse(layout, startString)
  164. if err != nil {
  165. klog.V(1).Infof("Error parsing time " + startString + ". Error: " + err.Error())
  166. return nil, err
  167. }
  168. end, err := time.Parse(layout, endString)
  169. if err != nil {
  170. klog.V(1).Infof("Error parsing time " + endString + ". Error: " + err.Error())
  171. return nil, err
  172. }
  173. window, err := time.ParseDuration(windowString)
  174. if err != nil {
  175. klog.V(1).Infof("Error parsing time " + windowString + ". Error: " + err.Error())
  176. return nil, err
  177. }
  178. qCores := fmt.Sprintf(queryClusterCores, offset, offset, offset)
  179. qRAM := fmt.Sprintf(queryClusterRAM, offset, offset)
  180. qStorage := fmt.Sprintf(queryStorage, windowString, offset, windowString, offset, localStorageQuery)
  181. qTotal := fmt.Sprintf(queryTotal, localStorageQuery)
  182. resultClusterCores, err := QueryRange(cli, qCores, start, end, window)
  183. if err != nil {
  184. return nil, err
  185. }
  186. resultClusterRAM, err := QueryRange(cli, qRAM, start, end, window)
  187. if err != nil {
  188. return nil, err
  189. }
  190. resultStorage, err := QueryRange(cli, qStorage, start, end, window)
  191. if err != nil {
  192. return nil, err
  193. }
  194. resultTotal, err := QueryRange(cli, qTotal, start, end, window)
  195. if err != nil {
  196. return nil, err
  197. }
  198. coreTotal, err := resultToTotals(resultClusterCores)
  199. if err != nil {
  200. return nil, err
  201. }
  202. ramTotal, err := resultToTotals(resultClusterRAM)
  203. if err != nil {
  204. return nil, err
  205. }
  206. storageTotal, err := resultToTotals(resultStorage)
  207. if err != nil {
  208. return nil, err
  209. }
  210. clusterTotal, err := resultToTotals(resultTotal)
  211. if err != nil {
  212. return nil, err
  213. }
  214. return &Totals{
  215. TotalCost: clusterTotal,
  216. CPUCost: coreTotal,
  217. MemCost: ramTotal,
  218. StorageCost: storageTotal,
  219. }, nil
  220. }