util.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. import { forEach, get, round } from "lodash";
  2. // rangeToCumulative takes an AllocationSetRange (type: array[AllocationSet])
  3. // and accumulates the values into a single AllocationSet (type: object)
  4. export function rangeToCumulative(allocationSetRange, aggregateBy) {
  5. if (allocationSetRange.length === 0) {
  6. return null;
  7. }
  8. const result = {};
  9. forEach(allocationSetRange, (allocSet) => {
  10. forEach(allocSet, (alloc) => {
  11. if (result[alloc.name] === undefined) {
  12. const hrs = get(alloc, "minutes", 0) / 60.0;
  13. result[alloc.name] = {
  14. name: alloc.name,
  15. [aggregateBy]: alloc.name,
  16. cpuCost: get(alloc, "cpuCost", 0),
  17. gpuCost: get(alloc, "gpuCost", 0),
  18. ramCost: get(alloc, "ramCost", 0),
  19. pvCost: get(alloc, "pvCost", 0),
  20. networkCost: get(alloc, "networkCost", 0),
  21. sharedCost: get(alloc, "sharedCost", 0),
  22. externalCost: get(alloc, "externalCost", 0),
  23. totalCost: get(alloc, "totalCost", 0),
  24. cpuUseCoreHrs: get(alloc, "cpuCoreUsageAverage", 0) * hrs,
  25. cpuReqCoreHrs: get(alloc, "cpuCoreRequestAverage", 0) * hrs,
  26. ramUseByteHrs: get(alloc, "ramByteUsageAverage", 0) * hrs,
  27. ramReqByteHrs: get(alloc, "ramByteRequestAverage", 0) * hrs,
  28. cpuEfficiency: get(alloc, "cpuEfficiency", 0),
  29. ramEfficiency: get(alloc, "ramEfficiency", 0),
  30. totalEfficiency: get(alloc, "totalEfficiency", 0),
  31. };
  32. } else {
  33. const hrs = get(alloc, "minutes", 0) / 60.0;
  34. result[alloc.name].cpuCost += get(alloc, "cpuCost", 0);
  35. result[alloc.name].gpuCost += get(alloc, "gpuCost", 0);
  36. result[alloc.name].ramCost += get(alloc, "ramCost", 0);
  37. result[alloc.name].pvCost += get(alloc, "pvCost", 0);
  38. result[alloc.name].networkCost += get(alloc, "networkCost", 0);
  39. result[alloc.name].sharedCost += get(alloc, "sharedCost", 0);
  40. result[alloc.name].externalCost += get(alloc, "externalCost", 0);
  41. result[alloc.name].totalCost += get(alloc, "totalCost", 0);
  42. result[alloc.name].cpuUseCoreHrs +=
  43. get(alloc, "cpuCoreUsageAverage", 0) * hrs;
  44. result[alloc.name].cpuReqCoreHrs +=
  45. get(alloc, "cpuCoreRequestAverage", 0) * hrs;
  46. result[alloc.name].ramUseByteHrs +=
  47. get(alloc, "ramByteUsageAverage", 0) * hrs;
  48. result[alloc.name].ramReqByteHrs +=
  49. get(alloc, "ramByteRequestAverage", 0) * hrs;
  50. }
  51. });
  52. });
  53. // If the range is of length > 1 (i.e. it is not just a single set) then
  54. // compute efficiency for each result after accumulating.
  55. if (allocationSetRange.length > 1) {
  56. forEach(result, (alloc, name) => {
  57. // If we can't compute total efficiency, it defaults to 0.0
  58. let totalEfficiency = 0.0;
  59. // CPU efficiency is defined as (usage/request). If request == 0.0 but
  60. // usage > 0, then efficiency gets set to 1.0.
  61. let cpuEfficiency = 0.0;
  62. if (alloc.cpuReqCoreHrs > 0) {
  63. cpuEfficiency = alloc.cpuUseCoreHrs / alloc.cpuReqCoreHrs;
  64. } else if (alloc.cpuUseCoreHrs > 0) {
  65. cpuEfficiency = 1.0;
  66. }
  67. // RAM efficiency is defined as (usage/request). If request == 0.0 but
  68. // usage > 0, then efficiency gets set to 1.0.
  69. let ramEfficiency = 0.0;
  70. if (alloc.ramReqByteHrs > 0) {
  71. ramEfficiency = alloc.ramUseByteHrs / alloc.ramReqByteHrs;
  72. } else if (alloc.ramUseByteHrs > 0) {
  73. ramEfficiency = 1.0;
  74. }
  75. // Compute efficiency as the cost-weighted average of CPU and RAM
  76. // efficiency
  77. if (alloc.cpuCost + alloc.ramCost > 0.0) {
  78. totalEfficiency =
  79. (alloc.cpuCost * cpuEfficiency + alloc.ramCost * ramEfficiency) /
  80. (alloc.cpuCost + alloc.ramCost);
  81. }
  82. result[name].cpuEfficiency = cpuEfficiency;
  83. result[name].ramEfficiency = ramEfficiency;
  84. result[name].totalEfficiency = totalEfficiency;
  85. });
  86. }
  87. return result;
  88. }
  89. // cumulativeToTotals adds each entry in the given AllocationSet (type: object)
  90. // and returns a single Allocation (type: object) representing the totals
  91. export function cumulativeToTotals(allocationSet) {
  92. let totals = {
  93. name: "Totals",
  94. cpuCost: 0,
  95. gpuCost: 0,
  96. ramCost: 0,
  97. pvCost: 0,
  98. networkCost: 0,
  99. sharedCost: 0,
  100. externalCost: 0,
  101. totalCost: 0,
  102. cpuEfficiency: 0,
  103. ramEfficiency: 0,
  104. totalEfficiency: 0,
  105. };
  106. // Use these for computing efficiency. As such, idle will not factor into
  107. // these numbers, including CPU and RAM cost.
  108. let cpuReqCoreHrs = 0;
  109. let cpuUseCoreHrs = 0;
  110. let ramReqByteHrs = 0;
  111. let ramUseByteHrs = 0;
  112. let cpuCost = 0;
  113. let ramCost = 0;
  114. forEach(allocationSet, (alloc, name) => {
  115. // Accumulate efficiency-related fields
  116. if (name !== "__idle__") {
  117. cpuReqCoreHrs += get(alloc, "cpuReqCoreHrs", 0.0);
  118. cpuUseCoreHrs += get(alloc, "cpuUseCoreHrs", 0.0);
  119. ramReqByteHrs += get(alloc, "ramReqByteHrs", 0.0);
  120. ramUseByteHrs += get(alloc, "ramUseByteHrs", 0.0);
  121. cpuCost += get(alloc, "cpuCost", 0.0);
  122. ramCost += get(alloc, "ramCost", 0.0);
  123. }
  124. // Sum cumulative fields
  125. totals.cpuCost += get(alloc, "cpuCost", 0);
  126. totals.gpuCost += get(alloc, "gpuCost", 0);
  127. totals.ramCost += get(alloc, "ramCost", 0);
  128. totals.pvCost += get(alloc, "pvCost", 0);
  129. totals.networkCost += get(alloc, "networkCost", 0);
  130. totals.sharedCost += get(alloc, "sharedCost", 0);
  131. totals.externalCost += get(alloc, "externalCost", 0);
  132. totals.totalCost += get(alloc, "totalCost", 0);
  133. });
  134. // Compute efficiency
  135. if (cpuReqCoreHrs > 0) {
  136. totals.cpuEfficiency = cpuUseCoreHrs / cpuReqCoreHrs;
  137. } else if (cpuUseCoreHrs > 0) {
  138. totals.cpuEfficiency = 1.0;
  139. }
  140. if (ramReqByteHrs > 0) {
  141. totals.ramEfficiency = ramUseByteHrs / ramReqByteHrs;
  142. } else if (ramUseByteHrs > 0) {
  143. totals.ramEfficiency = 1.0;
  144. }
  145. if (cpuCost + ramCost > 0) {
  146. totals.totalEfficiency =
  147. (cpuCost * totals.cpuEfficiency + ramCost * totals.ramEfficiency) /
  148. (cpuCost + ramCost);
  149. }
  150. totals.cpuReqCoreHrs = cpuReqCoreHrs;
  151. totals.cpuUseCoreHrs = cpuUseCoreHrs;
  152. totals.ramReqByteHrs = ramReqByteHrs;
  153. totals.ramUseByteHrs = ramUseByteHrs;
  154. return totals;
  155. }
  156. export function toVerboseTimeRange(window) {
  157. const months = [
  158. "January",
  159. "February",
  160. "March",
  161. "April",
  162. "May",
  163. "June",
  164. "July",
  165. "August",
  166. "September",
  167. "October",
  168. "November",
  169. "December",
  170. ];
  171. const start = new Date();
  172. start.setUTCHours(0, 0, 0, 0);
  173. const end = new Date();
  174. end.setUTCHours(0, 0, 0, 0);
  175. switch (window) {
  176. case "today":
  177. return `${start.getUTCDate()} ${
  178. months[start.getUTCMonth()]
  179. } ${start.getUTCFullYear()}`;
  180. case "yesterday":
  181. start.setUTCDate(start.getUTCDate() - 1);
  182. return `${start.getUTCDate()} ${
  183. months[start.getUTCMonth()]
  184. } ${start.getUTCFullYear()}`;
  185. case "week":
  186. start.setUTCDate(start.getUTCDate() - start.getUTCDay());
  187. return `${start.getUTCDate()} ${
  188. months[start.getUTCMonth()]
  189. } ${start.getUTCFullYear()} until now`;
  190. case "month":
  191. start.setUTCDate(1);
  192. return `${start.getUTCDate()} ${
  193. months[start.getUTCMonth()]
  194. } ${start.getUTCFullYear()} until now`;
  195. case "lastweek":
  196. start.setUTCDate(start.getUTCDate() - (start.getUTCDay() + 7));
  197. end.setUTCDate(end.getUTCDate() - (end.getUTCDay() + 1));
  198. return `${start.getUTCDate()} ${
  199. months[start.getUTCMonth()]
  200. } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${
  201. months[end.getUTCMonth()]
  202. } ${end.getUTCFullYear()}`;
  203. case "lastmonth":
  204. end.setUTCDate(1);
  205. end.setUTCDate(end.getUTCDate() - 1);
  206. start.setUTCDate(1);
  207. start.setUTCDate(start.getUTCDate() - 1);
  208. start.setUTCDate(1);
  209. return `${start.getUTCDate()} ${
  210. months[start.getUTCMonth()]
  211. } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${
  212. months[end.getUTCMonth()]
  213. } ${end.getUTCFullYear()}`;
  214. case "6d":
  215. start.setUTCDate(start.getUTCDate() - 6);
  216. return `${start.getUTCDate()} ${
  217. months[start.getUTCMonth()]
  218. } ${start.getUTCFullYear()} through now`;
  219. case "29d":
  220. start.setUTCDate(start.getUTCDate() - 29);
  221. return `${start.getUTCDate()} ${
  222. months[start.getUTCMonth()]
  223. } ${start.getUTCFullYear()} through now`;
  224. case "59d":
  225. start.setUTCDate(start.getUTCDate() - 59);
  226. return `${start.getUTCDate()} ${
  227. months[start.getUTCMonth()]
  228. } ${start.getUTCFullYear()} through now`;
  229. case "89d":
  230. start.setUTCDate(start.getUTCDate() - 89);
  231. return `${start.getUTCDate()} ${
  232. months[start.getUTCMonth()]
  233. } ${start.getUTCFullYear()} through now`;
  234. }
  235. const splitDates = window.split(",");
  236. if (checkCustomWindow(window) && splitDates.length > 1) {
  237. let s = splitDates[0].split(/\D+/).slice(0, 3);
  238. let e = splitDates[1].split(/\D+/).slice(0, 3);
  239. if (s.length === 3 && e.length === 3) {
  240. start.setUTCFullYear(s[0], s[1] - 1, s[2]);
  241. end.setUTCFullYear(e[0], e[1] - 1, e[2]);
  242. if (start === end) {
  243. return `${start.getUTCDate()} ${
  244. months[start.getUTCMonth()]
  245. } ${start.getUTCFullYear()}`;
  246. } else {
  247. return `${start.getUTCDate()} ${
  248. months[start.getUTCMonth()]
  249. } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${
  250. months[end.getUTCMonth()]
  251. } ${end.getUTCFullYear()}`;
  252. }
  253. }
  254. }
  255. return null;
  256. }
  257. export function bytesToString(bytes) {
  258. const ei = Math.pow(1024, 6);
  259. if (bytes >= ei) {
  260. return `${round(bytes / ei, 1)} EiB`;
  261. }
  262. const pi = Math.pow(1024, 5);
  263. if (bytes >= pi) {
  264. return `${round(bytes / pi, 1)} PiB`;
  265. }
  266. const ti = Math.pow(1024, 4);
  267. if (bytes >= ti) {
  268. return `${round(bytes / ti, 1)} TiB`;
  269. }
  270. const gi = Math.pow(1024, 3);
  271. if (bytes >= gi) {
  272. return `${round(bytes / gi, 1)} GiB`;
  273. }
  274. const mi = Math.pow(1024, 2);
  275. if (bytes >= mi) {
  276. return `${round(bytes / mi, 1)} MiB`;
  277. }
  278. const ki = Math.pow(1024, 1);
  279. if (bytes >= ki) {
  280. return `${round(bytes / ki, 1)} KiB`;
  281. }
  282. return `${round(bytes, 1)} B`;
  283. }
  284. const currencyLocale = "en-US";
  285. export function toCurrency(amount, currency, precision) {
  286. if (typeof amount !== "number") {
  287. console.warn(
  288. `Tried to convert "${amount}" to currency, but it is not a number`
  289. );
  290. return "";
  291. }
  292. if (currency === undefined || currency === "") {
  293. currency = "USD";
  294. }
  295. const opts = {
  296. style: "currency",
  297. currency: currency,
  298. };
  299. if (typeof precision === "number") {
  300. opts.minimumFractionDigits = precision;
  301. opts.maximumFractionDigits = precision;
  302. }
  303. return amount.toLocaleString(currencyLocale, opts);
  304. }
  305. export function checkCustomWindow(window) {
  306. // Example ISO interval string: 2020-12-02T00:00:00Z,2020-12-03T23:59:59Z
  307. const customDateRegex =
  308. /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z,\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/;
  309. return customDateRegex.test(window);
  310. }
  311. export function getCloudFilters(filters) {
  312. const filterNamesMap = {
  313. "invoice entity": "filterInvoiceEntityIDs",
  314. provider: "filterProviders",
  315. providerids: "filterProviderIDs",
  316. service: "filterServices",
  317. account: "filterAccountIDs",
  318. };
  319. const params = new URLSearchParams();
  320. const labelFilters = [];
  321. for (let filter of filters) {
  322. const mapped = filterNamesMap[filter.property.toLowerCase()];
  323. if (mapped) {
  324. params.set(mapped, filter.value);
  325. } else if (filter.property === "Labels") {
  326. labelFilters.push(filter.value);
  327. } else if (filter.property.startsWith(":")) {
  328. labelFilters.push(`${filter.property.slice(6)}:${filter.value}`);
  329. }
  330. }
  331. if (labelFilters.length) {
  332. params.set("filterLabels", labelFilters.join(","));
  333. }
  334. return `&${params.toString()}`;
  335. }
  336. export function formatSampleItemsForGraph({ data }) {
  337. const graphData = data.sets.map(({ cloudCosts, window: { end, start } }) => {
  338. return {
  339. end,
  340. items: Object.entries(cloudCosts).map(([name, item]) => ({
  341. name,
  342. value: item.netCost.cost,
  343. })),
  344. start,
  345. };
  346. });
  347. const accumulator = {};
  348. data.sets.forEach(({ cloudCosts, window }) => {
  349. Object.entries(cloudCosts).forEach(
  350. ([name, { amortizedNetCost, netCost, properties }]) => {
  351. accumulator[name] ||= {
  352. cost: 0,
  353. netCost: 0,
  354. start: "",
  355. end: "",
  356. providerID: "",
  357. labelName: "",
  358. kubernetesCost: 0,
  359. get kubernetesPercent() {
  360. return this.kubernetesCost / this.cost;
  361. },
  362. };
  363. accumulator[name].cost += amortizedNetCost.cost;
  364. accumulator[name].kubernetesCost +=
  365. amortizedNetCost.cost * amortizedNetCost.kubernetesPercent;
  366. accumulator[name].netCost += netCost.cost;
  367. accumulator[name].start = window.start;
  368. accumulator[name].end = window.end;
  369. accumulator[name].providerID = properties.providerID;
  370. accumulator[name].labelName = properties.labels?.name;
  371. }
  372. );
  373. });
  374. const tableRows = Object.entries(accumulator).map(
  375. ([
  376. name,
  377. {
  378. cost,
  379. netCost,
  380. start,
  381. end,
  382. providerID,
  383. kubernetesCost,
  384. kubernetesPercent,
  385. labelName,
  386. },
  387. ]) => ({
  388. cost,
  389. name,
  390. kubernetesCost,
  391. get kubernetesPercent() {
  392. return this.kubernetesCost / this.cost;
  393. },
  394. netCost,
  395. start,
  396. end,
  397. providerID,
  398. labelName,
  399. })
  400. );
  401. const tableTotal = tableRows.reduce((tr1, tr2) => ({
  402. ...tr1,
  403. cost: tr2.cost,
  404. name: "",
  405. kubernetesCost:
  406. tr1.kubernetesCost * tr1.cost + tr2.kubernetesCost * tr2.cost,
  407. netCost: tr1.netCost + tr2.netCost,
  408. start: "",
  409. end: "",
  410. labelName: "",
  411. providerID: "",
  412. }));
  413. return { graphData, tableRows, tableTotal };
  414. }
  415. export default {
  416. rangeToCumulative,
  417. cumulativeToTotals,
  418. toVerboseTimeRange,
  419. bytesToString,
  420. toCurrency,
  421. checkCustomWindow,
  422. };