cluster.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. e, err := wrapPrometheusError(qr)
  37. if err != nil {
  38. return nil, err
  39. }
  40. return nil, fmt.Errorf(e)
  41. }
  42. r, ok := data.(map[string]interface{})["result"]
  43. if !ok {
  44. return nil, fmt.Errorf("Improperly formatted data from prometheus, data has no result field")
  45. }
  46. results, ok := r.([]interface{})
  47. if !ok {
  48. return nil, fmt.Errorf("Improperly formatted results from prometheus, result field is not a slice")
  49. }
  50. if len(results) == 0 {
  51. return nil, fmt.Errorf("Not enough data available in the selected time range")
  52. }
  53. res, ok := results[0].(map[string]interface{})["values"]
  54. totals := [][]string{}
  55. for _, val := range res.([]interface{}) {
  56. if !ok {
  57. return nil, fmt.Errorf("Improperly formatted results from prometheus, value is not a field in the vector")
  58. }
  59. dataPoint, ok := val.([]interface{})
  60. if !ok || len(dataPoint) != 2 {
  61. return nil, fmt.Errorf("Improperly formatted datapoint from Prometheus")
  62. }
  63. d0 := fmt.Sprintf("%f", dataPoint[0].(float64))
  64. toAppend := []string{
  65. d0,
  66. dataPoint[1].(string),
  67. }
  68. totals = append(totals, toAppend)
  69. }
  70. return totals, nil
  71. }
  72. func resultToTotal(qr interface{}) ([][]string, error) {
  73. data, ok := qr.(map[string]interface{})["data"]
  74. if !ok {
  75. e, err := wrapPrometheusError(qr)
  76. if err != nil {
  77. return nil, err
  78. }
  79. return nil, fmt.Errorf(e)
  80. }
  81. r, ok := data.(map[string]interface{})["result"]
  82. if !ok {
  83. return nil, fmt.Errorf("Improperly formatted data from prometheus, data has no result field")
  84. }
  85. results, ok := r.([]interface{})
  86. if !ok {
  87. return nil, fmt.Errorf("Improperly formatted results from prometheus, result field is not a slice")
  88. }
  89. if len(results) == 0 {
  90. return nil, fmt.Errorf("Not enough data available in the selected time range")
  91. }
  92. val, ok := results[0].(map[string]interface{})["value"]
  93. totals := [][]string{}
  94. if !ok {
  95. return nil, fmt.Errorf("Improperly formatted results from prometheus, value is not a field in the vector")
  96. }
  97. dataPoint, ok := val.([]interface{})
  98. if !ok || len(dataPoint) != 2 {
  99. return nil, fmt.Errorf("Improperly formatted datapoint from Prometheus")
  100. }
  101. d0 := fmt.Sprintf("%f", dataPoint[0].(float64))
  102. toAppend := []string{
  103. d0,
  104. dataPoint[1].(string),
  105. }
  106. totals = append(totals, toAppend)
  107. return totals, nil
  108. }
  109. // ClusterCostsOverTime gives the current full cluster costs averaged over a window of time.
  110. func ClusterCosts(cli prometheusClient.Client, cloud costAnalyzerCloud.Provider, windowString, offset string) (*Totals, error) {
  111. localStorageQuery, err := cloud.GetLocalStorageQuery()
  112. if err != nil {
  113. return nil, err
  114. }
  115. if localStorageQuery != "" {
  116. localStorageQuery = fmt.Sprintf("+ %s", localStorageQuery)
  117. }
  118. qCores := fmt.Sprintf(queryClusterCores, offset, offset, offset)
  119. qRAM := fmt.Sprintf(queryClusterRAM, offset, offset)
  120. qStorage := fmt.Sprintf(queryStorage, windowString, offset, windowString, offset, localStorageQuery)
  121. qTotal := fmt.Sprintf(queryTotal, localStorageQuery)
  122. resultClusterCores, err := Query(cli, qCores)
  123. if err != nil {
  124. return nil, err
  125. }
  126. resultClusterRAM, err := Query(cli, qRAM)
  127. if err != nil {
  128. return nil, err
  129. }
  130. resultStorage, err := Query(cli, qStorage)
  131. if err != nil {
  132. return nil, err
  133. }
  134. resultTotal, err := Query(cli, qTotal)
  135. if err != nil {
  136. return nil, err
  137. }
  138. coreTotal, err := resultToTotal(resultClusterCores)
  139. if err != nil {
  140. return nil, err
  141. }
  142. ramTotal, err := resultToTotal(resultClusterRAM)
  143. if err != nil {
  144. return nil, err
  145. }
  146. storageTotal, err := resultToTotal(resultStorage)
  147. if err != nil {
  148. return nil, err
  149. }
  150. clusterTotal, err := resultToTotal(resultTotal)
  151. if err != nil {
  152. return nil, err
  153. }
  154. return &Totals{
  155. TotalCost: clusterTotal,
  156. CPUCost: coreTotal,
  157. MemCost: ramTotal,
  158. StorageCost: storageTotal,
  159. }, nil
  160. }
  161. // ClusterCostsOverTime gives the full cluster costs over time
  162. func ClusterCostsOverTime(cli prometheusClient.Client, cloud costAnalyzerCloud.Provider, startString, endString, windowString, offset string) (*Totals, error) {
  163. localStorageQuery, err := cloud.GetLocalStorageQuery()
  164. if err != nil {
  165. return nil, err
  166. }
  167. if localStorageQuery != "" {
  168. localStorageQuery = fmt.Sprintf("+ %s", localStorageQuery)
  169. }
  170. layout := "2006-01-02T15:04:05.000Z"
  171. start, err := time.Parse(layout, startString)
  172. if err != nil {
  173. klog.V(1).Infof("Error parsing time " + startString + ". Error: " + err.Error())
  174. return nil, err
  175. }
  176. end, err := time.Parse(layout, endString)
  177. if err != nil {
  178. klog.V(1).Infof("Error parsing time " + endString + ". Error: " + err.Error())
  179. return nil, err
  180. }
  181. window, err := time.ParseDuration(windowString)
  182. if err != nil {
  183. klog.V(1).Infof("Error parsing time " + windowString + ". Error: " + err.Error())
  184. return nil, err
  185. }
  186. qCores := fmt.Sprintf(queryClusterCores, offset, offset, offset)
  187. qRAM := fmt.Sprintf(queryClusterRAM, offset, offset)
  188. qStorage := fmt.Sprintf(queryStorage, windowString, offset, windowString, offset, localStorageQuery)
  189. qTotal := fmt.Sprintf(queryTotal, localStorageQuery)
  190. resultClusterCores, err := QueryRange(cli, qCores, start, end, window)
  191. if err != nil {
  192. return nil, err
  193. }
  194. resultClusterRAM, err := QueryRange(cli, qRAM, start, end, window)
  195. if err != nil {
  196. return nil, err
  197. }
  198. resultStorage, err := QueryRange(cli, qStorage, start, end, window)
  199. if err != nil {
  200. return nil, err
  201. }
  202. resultTotal, err := QueryRange(cli, qTotal, start, end, window)
  203. if err != nil {
  204. return nil, err
  205. }
  206. coreTotal, err := resultToTotals(resultClusterCores)
  207. if err != nil {
  208. return nil, err
  209. }
  210. ramTotal, err := resultToTotals(resultClusterRAM)
  211. if err != nil {
  212. return nil, err
  213. }
  214. storageTotal, err := resultToTotals(resultStorage)
  215. if err != nil {
  216. return nil, err
  217. }
  218. clusterTotal, err := resultToTotals(resultTotal)
  219. if err != nil {
  220. return nil, err
  221. }
  222. return &Totals{
  223. TotalCost: clusterTotal,
  224. CPUCost: coreTotal,
  225. MemCost: ramTotal,
  226. StorageCost: storageTotal,
  227. }, nil
  228. }