@electrolux-oss/plugin-infrawallet-backend 0.1.12 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.d.ts +29 -1
- package/dist/index.cjs.js +1021 -563
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/migrations/20241017141420_create-custom-cost-table.js +31 -0
- package/migrations/20241021105140_wallet-budgets.js +22 -0
- package/package.json +19 -13
package/dist/index.cjs.js
CHANGED
|
@@ -6,19 +6,19 @@ var backendCommon = require('@backstage/backend-common');
|
|
|
6
6
|
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
7
7
|
var express = require('express');
|
|
8
8
|
var Router = require('express-promise-router');
|
|
9
|
+
var moment = require('moment');
|
|
9
10
|
var clientCostExplorer = require('@aws-sdk/client-cost-explorer');
|
|
10
11
|
var clientSts = require('@aws-sdk/client-sts');
|
|
11
12
|
var lodash = require('lodash');
|
|
12
|
-
var moment = require('moment');
|
|
13
13
|
var armCostmanagement = require('@azure/arm-costmanagement');
|
|
14
14
|
var coreRestPipeline = require('@azure/core-rest-pipeline');
|
|
15
15
|
var identity = require('@azure/identity');
|
|
16
|
-
var bigquery = require('@google-cloud/bigquery');
|
|
17
16
|
var datadogApiClient = require('@datadog/datadog-api-client');
|
|
18
|
-
var
|
|
17
|
+
var bigquery = require('@google-cloud/bigquery');
|
|
19
18
|
var fs = require('fs');
|
|
20
19
|
var upath = require('upath');
|
|
21
20
|
var urllib = require('urllib');
|
|
21
|
+
var fetch$1 = require('node-fetch');
|
|
22
22
|
|
|
23
23
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
24
24
|
|
|
@@ -43,9 +43,9 @@ function _interopNamespaceCompat(e) {
|
|
|
43
43
|
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
44
44
|
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
45
45
|
var moment__default = /*#__PURE__*/_interopDefaultCompat(moment);
|
|
46
|
-
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch$1);
|
|
47
46
|
var upath__namespace = /*#__PURE__*/_interopNamespaceCompat(upath);
|
|
48
47
|
var urllib__default = /*#__PURE__*/_interopDefaultCompat(urllib);
|
|
48
|
+
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch$1);
|
|
49
49
|
|
|
50
50
|
async function getWallet(database, walletName) {
|
|
51
51
|
const client = await database.getClient();
|
|
@@ -74,8 +74,61 @@ async function deleteWalletMetricSetting(database, walletSetting) {
|
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
async function getBudgets(database, walletName) {
|
|
78
|
+
const knex = await database.getClient();
|
|
79
|
+
const budgets = await knex.select("budgets.*").from("budgets").where("wallets.name", walletName).join("wallets", "budgets.wallet_id", "=", "wallets.id");
|
|
80
|
+
return budgets;
|
|
81
|
+
}
|
|
82
|
+
async function getBudget(database, walletName, provider) {
|
|
83
|
+
const knex = await database.getClient();
|
|
84
|
+
const budgets = await knex.select("budgets.*").from("budgets").where("budgets.provider", provider).where("wallets.name", walletName).join("wallets", "budgets.wallet_id", "=", "wallets.id");
|
|
85
|
+
return budgets;
|
|
86
|
+
}
|
|
87
|
+
async function upsertBudget(database, walletName, budget) {
|
|
88
|
+
const knex = await database.getClient();
|
|
89
|
+
const wallet = await knex.select("id").from("wallets").where("name", walletName).first();
|
|
90
|
+
budget.wallet_id = wallet.id;
|
|
91
|
+
const result = await knex("budgets").insert(budget).onConflict("id").merge();
|
|
92
|
+
if (result[0] > 0) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function createCustomCosts(database, data) {
|
|
99
|
+
const knex = await database.getClient();
|
|
100
|
+
const records = await knex("custom_costs").insert(data).returning("id");
|
|
101
|
+
return records.length;
|
|
102
|
+
}
|
|
103
|
+
async function getCustomCostsByDateRange(database, startDate, endDate) {
|
|
104
|
+
const knex = await database.getClient();
|
|
105
|
+
const records = await knex("custom_costs").whereBetween("usage_month", [
|
|
106
|
+
parseInt(moment__default.default(startDate).format("YYYYMM"), 10),
|
|
107
|
+
parseInt(moment__default.default(endDate).format("YYYYMM"), 10)
|
|
108
|
+
]).select("*");
|
|
109
|
+
return records;
|
|
110
|
+
}
|
|
111
|
+
async function getCustomCosts(database) {
|
|
112
|
+
const knex = await database.getClient();
|
|
113
|
+
const records = await knex("custom_costs").select("*");
|
|
114
|
+
return records;
|
|
115
|
+
}
|
|
116
|
+
async function updateOrInsertCustomCost(database, data) {
|
|
117
|
+
const knex = await database.getClient();
|
|
118
|
+
const [record] = await knex("custom_costs").insert(data).onConflict("id").merge().returning("*");
|
|
119
|
+
return record;
|
|
120
|
+
}
|
|
121
|
+
async function deleteCustomCost(database, data) {
|
|
122
|
+
const knex = await database.getClient();
|
|
123
|
+
const result = await knex("custom_costs").where("id", data.id).del();
|
|
124
|
+
if (result > 0) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
77
130
|
function parseTags(tags) {
|
|
78
|
-
if (!tags
|
|
131
|
+
if (!tags.startsWith("(") || !tags.endsWith(")")) {
|
|
79
132
|
return [];
|
|
80
133
|
}
|
|
81
134
|
const tagString = tags.slice(1, -1);
|
|
@@ -189,7 +242,7 @@ async function setReportsToCache(cache, reports, provider, configKey, query, ttl
|
|
|
189
242
|
query.endTime
|
|
190
243
|
].join("_");
|
|
191
244
|
await cache.set(cacheKey, reports, {
|
|
192
|
-
ttl: ttl
|
|
245
|
+
ttl: ttl ?? 60 * 60 * 2 * 1e3
|
|
193
246
|
});
|
|
194
247
|
}
|
|
195
248
|
async function setMetricsToCache(cache, metrics, provider, configKey, query, ttl) {
|
|
@@ -209,7 +262,7 @@ async function setMetricsToCache(cache, metrics, provider, configKey, query, ttl
|
|
|
209
262
|
}
|
|
210
263
|
function parseFilters(filters) {
|
|
211
264
|
const result = {};
|
|
212
|
-
if (!filters
|
|
265
|
+
if (!filters.startsWith("(") || !filters.endsWith(")")) {
|
|
213
266
|
return result;
|
|
214
267
|
}
|
|
215
268
|
const filterString = filters.slice(1, -1);
|
|
@@ -230,6 +283,16 @@ function parseFilters(filters) {
|
|
|
230
283
|
});
|
|
231
284
|
return result;
|
|
232
285
|
}
|
|
286
|
+
function getDailyPeriodStringsForOneMonth(yyyymm) {
|
|
287
|
+
const dateOjb = moment__default.default(yyyymm.toString(), "YYYYMM");
|
|
288
|
+
const startOfMonth = moment__default.default(dateOjb).startOf("month");
|
|
289
|
+
const endOfMonth = moment__default.default(dateOjb).endOf("month");
|
|
290
|
+
const periods = [];
|
|
291
|
+
for (let date = startOfMonth; date.isBefore(endOfMonth) || date.isSame(endOfMonth); date.add(1, "day")) {
|
|
292
|
+
periods.push(date.format("YYYY-MM-DD"));
|
|
293
|
+
}
|
|
294
|
+
return periods;
|
|
295
|
+
}
|
|
233
296
|
|
|
234
297
|
class InfraWalletClient {
|
|
235
298
|
constructor(provider, config, database, cache, logger) {
|
|
@@ -242,22 +305,64 @@ class InfraWalletClient {
|
|
|
242
305
|
convertServiceName(serviceName) {
|
|
243
306
|
return serviceName;
|
|
244
307
|
}
|
|
308
|
+
evaluateIntegrationFilters(account, integrationConfig) {
|
|
309
|
+
const filters = [];
|
|
310
|
+
for (const filter of integrationConfig.getOptionalConfigArray("filters") || []) {
|
|
311
|
+
filters.push({
|
|
312
|
+
type: filter.getString("type"),
|
|
313
|
+
attribute: filter.getString("attribute"),
|
|
314
|
+
pattern: filter.getString("pattern")
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return this.evaluateFilters(account, filters);
|
|
318
|
+
}
|
|
319
|
+
evaluateFilters(account, filters) {
|
|
320
|
+
if (!filters || filters.length === 0) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
let included = false;
|
|
324
|
+
let hasIncludeFilter = false;
|
|
325
|
+
for (const filter of filters) {
|
|
326
|
+
const regex = new RegExp(filter.pattern);
|
|
327
|
+
if (filter.type === "exclude" && regex.test(account)) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
if (filter.type === "include") {
|
|
331
|
+
hasIncludeFilter = true;
|
|
332
|
+
if (regex.test(account)) {
|
|
333
|
+
included = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (hasIncludeFilter) {
|
|
338
|
+
return included;
|
|
339
|
+
}
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
// Get all cost allocation tag keys from one account
|
|
343
|
+
async fetchTagKeys(_integrationConfig, _client, _query) {
|
|
344
|
+
return { tagKeys: [], provider: this.provider };
|
|
345
|
+
}
|
|
346
|
+
// Get all tag values of the specified tag key from one account
|
|
347
|
+
async fetchTagValues(_integrationConfig, _client, _query, _tagKey) {
|
|
348
|
+
return { tagValues: [], provider: this.provider };
|
|
349
|
+
}
|
|
245
350
|
// Get aggregated unique tag keys across all accounts of this cloud provider
|
|
246
351
|
async getTagKeys(query) {
|
|
247
|
-
const
|
|
352
|
+
const integrationConfigs = this.config.getOptionalConfigArray(
|
|
248
353
|
`backend.infraWallet.integrations.${this.provider.toLowerCase()}`
|
|
249
354
|
);
|
|
250
|
-
if (!
|
|
355
|
+
if (!integrationConfigs) {
|
|
251
356
|
return { tags: [], errors: [] };
|
|
252
357
|
}
|
|
253
358
|
const promises = [];
|
|
254
359
|
const aggregatedTags = [];
|
|
255
360
|
const errors = [];
|
|
256
|
-
for (const
|
|
257
|
-
const
|
|
258
|
-
const cachedTagKeys = await getTagKeysFromCache(this.cache, this.provider,
|
|
361
|
+
for (const integrationConfig of integrationConfigs) {
|
|
362
|
+
const integrationName = integrationConfig.getString("name");
|
|
363
|
+
const cachedTagKeys = await getTagKeysFromCache(this.cache, this.provider, integrationName, query);
|
|
259
364
|
if (cachedTagKeys) {
|
|
260
|
-
this.logger.info(`Reuse ${this.provider}/${
|
|
365
|
+
this.logger.info(`Reuse ${this.provider}/${integrationName} tag keys from cache`);
|
|
261
366
|
for (const tag of cachedTagKeys) {
|
|
262
367
|
if (!tagExists(aggregatedTags, tag)) {
|
|
263
368
|
aggregatedTags.push(tag);
|
|
@@ -267,8 +372,8 @@ class InfraWalletClient {
|
|
|
267
372
|
}
|
|
268
373
|
const promise = (async () => {
|
|
269
374
|
try {
|
|
270
|
-
const client = await this.initCloudClient(
|
|
271
|
-
const response = await this.fetchTagKeys(
|
|
375
|
+
const client = await this.initCloudClient(integrationConfig);
|
|
376
|
+
const response = await this.fetchTagKeys(integrationConfig, client, query);
|
|
272
377
|
const tagKeysCache = [];
|
|
273
378
|
for (const tagKey of response.tagKeys) {
|
|
274
379
|
const tag = { key: tagKey, provider: response.provider };
|
|
@@ -277,12 +382,12 @@ class InfraWalletClient {
|
|
|
277
382
|
aggregatedTags.push(tag);
|
|
278
383
|
}
|
|
279
384
|
}
|
|
280
|
-
await setTagKeysToCache(this.cache, tagKeysCache, this.provider,
|
|
385
|
+
await setTagKeysToCache(this.cache, tagKeysCache, this.provider, integrationName, query);
|
|
281
386
|
} catch (e) {
|
|
282
387
|
this.logger.error(e);
|
|
283
388
|
errors.push({
|
|
284
389
|
provider: this.provider,
|
|
285
|
-
name: `${this.provider}/${
|
|
390
|
+
name: `${this.provider}/${integrationName}`,
|
|
286
391
|
error: e.message
|
|
287
392
|
});
|
|
288
393
|
}
|
|
@@ -290,27 +395,28 @@ class InfraWalletClient {
|
|
|
290
395
|
promises.push(promise);
|
|
291
396
|
}
|
|
292
397
|
await Promise.all(promises);
|
|
398
|
+
aggregatedTags.sort((a, b) => `${a.provider}/${a.key}`.localeCompare(`${b.provider}/${b.key}`));
|
|
293
399
|
return {
|
|
294
|
-
tags: aggregatedTags
|
|
400
|
+
tags: aggregatedTags,
|
|
295
401
|
errors
|
|
296
402
|
};
|
|
297
403
|
}
|
|
298
404
|
// Get aggregated tag values of the specified tag key across all accounts of this cloud provider
|
|
299
405
|
async getTagValues(query, tagKey) {
|
|
300
|
-
const
|
|
406
|
+
const integrationConfigs = this.config.getOptionalConfigArray(
|
|
301
407
|
`backend.infraWallet.integrations.${this.provider.toLowerCase()}`
|
|
302
408
|
);
|
|
303
|
-
if (!
|
|
409
|
+
if (!integrationConfigs) {
|
|
304
410
|
return { tags: [], errors: [] };
|
|
305
411
|
}
|
|
306
412
|
const promises = [];
|
|
307
413
|
const aggregatedTags = [];
|
|
308
414
|
const errors = [];
|
|
309
|
-
for (const
|
|
310
|
-
const
|
|
311
|
-
const cachedTagValues = await getTagValuesFromCache(this.cache, this.provider,
|
|
415
|
+
for (const integrationConfig of integrationConfigs) {
|
|
416
|
+
const integrationName = integrationConfig.getString("name");
|
|
417
|
+
const cachedTagValues = await getTagValuesFromCache(this.cache, this.provider, integrationName, tagKey, query);
|
|
312
418
|
if (cachedTagValues) {
|
|
313
|
-
this.logger.info(`Reuse ${this.provider}/${
|
|
419
|
+
this.logger.info(`Reuse ${this.provider}/${integrationName}/${tagKey} tag values from cache`);
|
|
314
420
|
for (const tag of cachedTagValues) {
|
|
315
421
|
if (!tagExists(aggregatedTags, tag)) {
|
|
316
422
|
aggregatedTags.push(tag);
|
|
@@ -320,8 +426,8 @@ class InfraWalletClient {
|
|
|
320
426
|
}
|
|
321
427
|
const promise = (async () => {
|
|
322
428
|
try {
|
|
323
|
-
const client = await this.initCloudClient(
|
|
324
|
-
const response = await this.fetchTagValues(
|
|
429
|
+
const client = await this.initCloudClient(integrationConfig);
|
|
430
|
+
const response = await this.fetchTagValues(integrationConfig, client, query, tagKey);
|
|
325
431
|
const tagValuesCache = [];
|
|
326
432
|
for (const tagValue of response.tagValues) {
|
|
327
433
|
const tag = { key: tagKey, value: tagValue, provider: response.provider };
|
|
@@ -330,12 +436,12 @@ class InfraWalletClient {
|
|
|
330
436
|
aggregatedTags.push(tag);
|
|
331
437
|
}
|
|
332
438
|
}
|
|
333
|
-
await setTagValuesToCache(this.cache, tagValuesCache, this.provider,
|
|
439
|
+
await setTagValuesToCache(this.cache, tagValuesCache, this.provider, integrationName, tagKey, query);
|
|
334
440
|
} catch (e) {
|
|
335
441
|
this.logger.error(e);
|
|
336
442
|
errors.push({
|
|
337
443
|
provider: this.provider,
|
|
338
|
-
name: `${this.provider}/${
|
|
444
|
+
name: `${this.provider}/${integrationName}`,
|
|
339
445
|
error: e.message
|
|
340
446
|
});
|
|
341
447
|
}
|
|
@@ -343,54 +449,55 @@ class InfraWalletClient {
|
|
|
343
449
|
promises.push(promise);
|
|
344
450
|
}
|
|
345
451
|
await Promise.all(promises);
|
|
452
|
+
aggregatedTags.sort(
|
|
453
|
+
(a, b) => `${a.provider}/${a.key}=${a.value}`.localeCompare(`${b.provider}/${b.key}=${b.value}`)
|
|
454
|
+
);
|
|
346
455
|
return {
|
|
347
|
-
tags: aggregatedTags
|
|
348
|
-
(a, b) => `${a.provider}/${a.key}=${a.value}`.localeCompare(`${b.provider}/${b.key}=${b.value}`)
|
|
349
|
-
),
|
|
456
|
+
tags: aggregatedTags,
|
|
350
457
|
errors
|
|
351
458
|
};
|
|
352
459
|
}
|
|
353
460
|
async getCostReports(query) {
|
|
354
|
-
const
|
|
461
|
+
const integrationConfigs = this.config.getOptionalConfigArray(
|
|
355
462
|
`backend.infraWallet.integrations.${this.provider.toLowerCase()}`
|
|
356
463
|
);
|
|
357
|
-
if (!
|
|
464
|
+
if (!integrationConfigs) {
|
|
358
465
|
return { reports: [], errors: [] };
|
|
359
466
|
}
|
|
360
467
|
const promises = [];
|
|
361
468
|
const results = [];
|
|
362
469
|
const errors = [];
|
|
363
|
-
for (const
|
|
364
|
-
const
|
|
365
|
-
const cachedCosts = await getReportsFromCache(this.cache, this.provider,
|
|
470
|
+
for (const integrationConfig of integrationConfigs) {
|
|
471
|
+
const integrationName = integrationConfig.getString("name");
|
|
472
|
+
const cachedCosts = await getReportsFromCache(this.cache, this.provider, integrationName, query);
|
|
366
473
|
if (cachedCosts) {
|
|
367
|
-
this.logger.debug(`${this.provider}/${
|
|
368
|
-
cachedCosts.
|
|
474
|
+
this.logger.debug(`${this.provider}/${integrationName} costs from cache`);
|
|
475
|
+
cachedCosts.forEach((cost) => {
|
|
369
476
|
results.push(cost);
|
|
370
477
|
});
|
|
371
478
|
continue;
|
|
372
479
|
}
|
|
373
480
|
const promise = (async () => {
|
|
374
481
|
try {
|
|
375
|
-
const client = await this.initCloudClient(
|
|
376
|
-
const costResponse = await this.fetchCosts(
|
|
377
|
-
const transformedReports = await this.transformCostsData(
|
|
482
|
+
const client = await this.initCloudClient(integrationConfig);
|
|
483
|
+
const costResponse = await this.fetchCosts(integrationConfig, client, query);
|
|
484
|
+
const transformedReports = await this.transformCostsData(integrationConfig, query, costResponse);
|
|
378
485
|
await setReportsToCache(
|
|
379
486
|
this.cache,
|
|
380
487
|
transformedReports,
|
|
381
488
|
this.provider,
|
|
382
|
-
|
|
489
|
+
integrationName,
|
|
383
490
|
query,
|
|
384
491
|
getDefaultCacheTTL(CACHE_CATEGORY.COSTS, this.provider)
|
|
385
492
|
);
|
|
386
|
-
transformedReports.
|
|
493
|
+
transformedReports.forEach((value) => {
|
|
387
494
|
results.push(value);
|
|
388
495
|
});
|
|
389
496
|
} catch (e) {
|
|
390
497
|
this.logger.error(e);
|
|
391
498
|
errors.push({
|
|
392
499
|
provider: this.provider,
|
|
393
|
-
name: `${this.provider}/${
|
|
500
|
+
name: `${this.provider}/${integrationName}`,
|
|
394
501
|
error: e.message
|
|
395
502
|
});
|
|
396
503
|
}
|
|
@@ -405,17 +512,8 @@ class InfraWalletClient {
|
|
|
405
512
|
}
|
|
406
513
|
}
|
|
407
514
|
|
|
408
|
-
var __defProp$1 = Object.defineProperty;
|
|
409
|
-
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
410
|
-
var __publicField$1 = (obj, key, value) => {
|
|
411
|
-
__defNormalProp$1(obj, key + "" , value);
|
|
412
|
-
return value;
|
|
413
|
-
};
|
|
414
515
|
class AwsClient extends InfraWalletClient {
|
|
415
|
-
|
|
416
|
-
super(...arguments);
|
|
417
|
-
__publicField$1(this, "accounts", /* @__PURE__ */ new Map());
|
|
418
|
-
}
|
|
516
|
+
accounts = /* @__PURE__ */ new Map();
|
|
419
517
|
static create(config, database, cache, logger) {
|
|
420
518
|
return new AwsClient(CLOUD_PROVIDER.AWS, config, database, cache, logger);
|
|
421
519
|
}
|
|
@@ -445,46 +543,55 @@ class AwsClient extends InfraWalletClient {
|
|
|
445
543
|
}
|
|
446
544
|
return `${this.provider}/${convertedName}`;
|
|
447
545
|
}
|
|
448
|
-
async initCloudClient(
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
546
|
+
async initCloudClient(integrationConfig) {
|
|
547
|
+
const accountId = integrationConfig.getString("accountId");
|
|
548
|
+
const assumedRoleName = integrationConfig.getOptionalString("assumedRoleName");
|
|
549
|
+
const accessKeyId = integrationConfig.getOptionalString("accessKeyId");
|
|
550
|
+
const accessKeySecret = integrationConfig.getOptionalString("accessKeySecret");
|
|
551
|
+
const region = "us-east-1";
|
|
552
|
+
if (!accessKeyId && !accessKeySecret && !assumedRoleName) {
|
|
553
|
+
return new clientCostExplorer.CostExplorerClient({ region });
|
|
554
|
+
}
|
|
555
|
+
let credentials = void 0;
|
|
556
|
+
if (accessKeyId || accessKeySecret) {
|
|
557
|
+
if (accessKeyId && accessKeySecret) {
|
|
558
|
+
credentials = {
|
|
459
559
|
accessKeyId,
|
|
460
560
|
secretAccessKey: accessKeySecret
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
region: "us-east-1"
|
|
466
|
-
};
|
|
561
|
+
};
|
|
562
|
+
} else {
|
|
563
|
+
throw new Error("Both accessKeyId and accessKeySecret must be provided");
|
|
564
|
+
}
|
|
467
565
|
}
|
|
468
|
-
|
|
566
|
+
if (assumedRoleName === void 0) {
|
|
567
|
+
return new clientCostExplorer.CostExplorerClient({
|
|
568
|
+
region,
|
|
569
|
+
credentials
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
const client = new clientSts.STSClient({
|
|
573
|
+
region,
|
|
574
|
+
credentials
|
|
575
|
+
});
|
|
469
576
|
const commandInput = {
|
|
470
577
|
// AssumeRoleRequest
|
|
471
578
|
RoleArn: `arn:aws:iam::${accountId}:role/${assumedRoleName}`,
|
|
472
|
-
RoleSessionName: "
|
|
579
|
+
RoleSessionName: "InfraWallet"
|
|
473
580
|
};
|
|
474
581
|
const assumeRoleCommand = new clientSts.AssumeRoleCommand(commandInput);
|
|
475
582
|
const assumeRoleResponse = await client.send(assumeRoleCommand);
|
|
476
583
|
const awsCeClient = new clientCostExplorer.CostExplorerClient({
|
|
477
|
-
region
|
|
584
|
+
region,
|
|
478
585
|
credentials: {
|
|
479
|
-
accessKeyId:
|
|
480
|
-
secretAccessKey:
|
|
481
|
-
sessionToken:
|
|
586
|
+
accessKeyId: assumeRoleResponse.Credentials?.AccessKeyId,
|
|
587
|
+
secretAccessKey: assumeRoleResponse.Credentials?.SecretAccessKey,
|
|
588
|
+
sessionToken: assumeRoleResponse.Credentials?.SessionToken
|
|
482
589
|
}
|
|
483
590
|
});
|
|
484
591
|
return awsCeClient;
|
|
485
592
|
}
|
|
486
593
|
async _fetchTags(client, query, tagKey) {
|
|
487
|
-
const
|
|
594
|
+
const tags = [];
|
|
488
595
|
let nextPageToken = void 0;
|
|
489
596
|
do {
|
|
490
597
|
const input = {
|
|
@@ -498,23 +605,23 @@ class AwsClient extends InfraWalletClient {
|
|
|
498
605
|
const response = await client.send(command);
|
|
499
606
|
for (const tag of response.Tags) {
|
|
500
607
|
if (tag) {
|
|
501
|
-
|
|
608
|
+
tags.push(tag);
|
|
502
609
|
}
|
|
503
610
|
}
|
|
504
611
|
nextPageToken = response.NextPageToken;
|
|
505
612
|
} while (nextPageToken);
|
|
506
|
-
|
|
507
|
-
return
|
|
613
|
+
tags.sort((a, b) => a.localeCompare(b));
|
|
614
|
+
return tags;
|
|
508
615
|
}
|
|
509
|
-
async fetchTagKeys(
|
|
616
|
+
async fetchTagKeys(_integrationConfig, client, query) {
|
|
510
617
|
const tagKeys = await this._fetchTags(client, query);
|
|
511
|
-
return { tagKeys, provider:
|
|
618
|
+
return { tagKeys, provider: this.provider };
|
|
512
619
|
}
|
|
513
|
-
async fetchTagValues(
|
|
620
|
+
async fetchTagValues(_integrationConfig, client, query, tagKey) {
|
|
514
621
|
const tagValues = await this._fetchTags(client, query, tagKey);
|
|
515
|
-
return { tagValues, provider:
|
|
622
|
+
return { tagValues, provider: this.provider };
|
|
516
623
|
}
|
|
517
|
-
async fetchCosts(
|
|
624
|
+
async fetchCosts(_integrationConfig, client, query) {
|
|
518
625
|
let costAndUsageResults = [];
|
|
519
626
|
let nextPageToken = void 0;
|
|
520
627
|
let filterExpression = { Dimensions: { Key: clientCostExplorer.Dimension.RECORD_TYPE, Values: ["Usage"] } };
|
|
@@ -522,11 +629,15 @@ class AwsClient extends InfraWalletClient {
|
|
|
522
629
|
if (tags.length) {
|
|
523
630
|
let tagsExpression = {};
|
|
524
631
|
if (tags.length === 1) {
|
|
525
|
-
|
|
632
|
+
if (tags[0].value) {
|
|
633
|
+
tagsExpression = { Tags: { Key: tags[0].key, Values: [tags[0].value] } };
|
|
634
|
+
}
|
|
526
635
|
} else {
|
|
527
636
|
const tagList = [];
|
|
528
637
|
for (const tag of tags) {
|
|
529
|
-
|
|
638
|
+
if (tag.value) {
|
|
639
|
+
tagList.push({ Tags: { Key: tag.key, Values: [tag.value] } });
|
|
640
|
+
}
|
|
530
641
|
}
|
|
531
642
|
tagsExpression = { Or: tagList };
|
|
532
643
|
}
|
|
@@ -559,19 +670,18 @@ class AwsClient extends InfraWalletClient {
|
|
|
559
670
|
} while (nextPageToken);
|
|
560
671
|
return costAndUsageResults;
|
|
561
672
|
}
|
|
562
|
-
async transformCostsData(
|
|
673
|
+
async transformCostsData(integrationConfig, query, costResponse) {
|
|
563
674
|
const categoryMappingService = CategoryMappingService.getInstance();
|
|
564
|
-
const tags =
|
|
675
|
+
const tags = integrationConfig.getOptionalStringArray("tags");
|
|
565
676
|
const tagKeyValues = {};
|
|
566
|
-
tags
|
|
677
|
+
tags?.forEach((tag) => {
|
|
567
678
|
const [k, v] = tag.split(":");
|
|
568
679
|
tagKeyValues[k.trim()] = v.trim();
|
|
569
680
|
});
|
|
570
681
|
const transformedData = lodash.reduce(
|
|
571
682
|
costResponse,
|
|
572
683
|
(accumulator, row) => {
|
|
573
|
-
|
|
574
|
-
const rowTime = (_a = row.TimePeriod) == null ? void 0 : _a.Start;
|
|
684
|
+
const rowTime = row.TimePeriod?.Start;
|
|
575
685
|
let period = "unknown";
|
|
576
686
|
if (rowTime) {
|
|
577
687
|
if (query.granularity.toUpperCase() === "MONTHLY") {
|
|
@@ -582,9 +692,11 @@ class AwsClient extends InfraWalletClient {
|
|
|
582
692
|
}
|
|
583
693
|
if (row.Groups) {
|
|
584
694
|
row.Groups.forEach((group) => {
|
|
585
|
-
var _a2;
|
|
586
695
|
const accountId = group.Keys ? group.Keys[0] : "";
|
|
587
696
|
const accountName = this.accounts.get(accountId) || accountId;
|
|
697
|
+
if (!this.evaluateIntegrationFilters(accountName, integrationConfig)) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
588
700
|
const serviceName = group.Keys ? group.Keys[1] : "";
|
|
589
701
|
const keyName = `${accountId}_${serviceName}`;
|
|
590
702
|
if (!accumulator[keyName]) {
|
|
@@ -594,13 +706,14 @@ class AwsClient extends InfraWalletClient {
|
|
|
594
706
|
service: this.convertServiceName(serviceName),
|
|
595
707
|
category: categoryMappingService.getCategoryByServiceName(this.provider, serviceName),
|
|
596
708
|
provider: this.provider,
|
|
709
|
+
providerType: PROVIDER_TYPE.INTEGRATION,
|
|
597
710
|
reports: {},
|
|
598
711
|
...tagKeyValues
|
|
599
712
|
};
|
|
600
713
|
}
|
|
601
714
|
const groupMetrics = group.Metrics;
|
|
602
715
|
if (groupMetrics !== void 0) {
|
|
603
|
-
accumulator[keyName].reports[period] = parseFloat(
|
|
716
|
+
accumulator[keyName].reports[period] = parseFloat(groupMetrics.UnblendedCost.Amount ?? "0.0");
|
|
604
717
|
}
|
|
605
718
|
});
|
|
606
719
|
}
|
|
@@ -694,13 +807,11 @@ class AzureClient extends InfraWalletClient {
|
|
|
694
807
|
if (row[0] && !row[0].startsWith("hidden-")) {
|
|
695
808
|
tags.push(row[0]);
|
|
696
809
|
}
|
|
697
|
-
} else {
|
|
698
|
-
|
|
699
|
-
tags.push(row[1]);
|
|
700
|
-
}
|
|
810
|
+
} else if (row[1]) {
|
|
811
|
+
tags.push(row[1]);
|
|
701
812
|
}
|
|
702
813
|
}
|
|
703
|
-
tags.sort();
|
|
814
|
+
tags.sort((a, b) => a.localeCompare(b));
|
|
704
815
|
return tags;
|
|
705
816
|
}
|
|
706
817
|
async initCloudClient(config) {
|
|
@@ -713,11 +824,11 @@ class AzureClient extends InfraWalletClient {
|
|
|
713
824
|
}
|
|
714
825
|
async fetchTagKeys(subAccountConfig, client, query) {
|
|
715
826
|
const tagKeys = await this._fetchTags(subAccountConfig, client, query, "");
|
|
716
|
-
return { tagKeys, provider:
|
|
827
|
+
return { tagKeys, provider: this.provider };
|
|
717
828
|
}
|
|
718
829
|
async fetchTagValues(subAccountConfig, client, query, tagKey) {
|
|
719
830
|
const tagValues = await this._fetchTags(subAccountConfig, client, query, tagKey);
|
|
720
|
-
return { tagValues, provider:
|
|
831
|
+
return { tagValues, provider: this.provider };
|
|
721
832
|
}
|
|
722
833
|
async fetchCosts(subAccountConfig, client, query) {
|
|
723
834
|
const subscriptionId = subAccountConfig.getString("subscriptionId");
|
|
@@ -727,13 +838,17 @@ class AzureClient extends InfraWalletClient {
|
|
|
727
838
|
const tags = parseTags(query.tags);
|
|
728
839
|
if (tags.length) {
|
|
729
840
|
if (tags.length === 1) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
841
|
+
if (tags[0].value) {
|
|
842
|
+
filter = {
|
|
843
|
+
tags: { name: tags[0].key, operator: "In", values: [tags[0].value] }
|
|
844
|
+
};
|
|
845
|
+
}
|
|
733
846
|
} else {
|
|
734
847
|
const tagList = [];
|
|
735
848
|
for (const tag of tags) {
|
|
736
|
-
|
|
849
|
+
if (tag.value) {
|
|
850
|
+
tagList.push({ tags: { name: tag.key, operator: "In", values: [tag.value] } });
|
|
851
|
+
}
|
|
737
852
|
}
|
|
738
853
|
filter = { or: tagList };
|
|
739
854
|
}
|
|
@@ -767,7 +882,7 @@ class AzureClient extends InfraWalletClient {
|
|
|
767
882
|
const groupPairs = [{ type: "Dimension", name: "ServiceName" }];
|
|
768
883
|
const tags = subAccountConfig.getOptionalStringArray("tags");
|
|
769
884
|
const tagKeyValues = {};
|
|
770
|
-
tags
|
|
885
|
+
tags?.forEach((tag) => {
|
|
771
886
|
const [k, v] = tag.split(":");
|
|
772
887
|
tagKeyValues[k.trim()] = v.trim();
|
|
773
888
|
});
|
|
@@ -791,6 +906,7 @@ class AzureClient extends InfraWalletClient {
|
|
|
791
906
|
service: this.convertServiceName(serviceName),
|
|
792
907
|
category: categoryMappingService.getCategoryByServiceName(this.provider, serviceName),
|
|
793
908
|
provider: this.provider,
|
|
909
|
+
providerType: PROVIDER_TYPE.INTEGRATION,
|
|
794
910
|
reports: {},
|
|
795
911
|
...tagKeyValues
|
|
796
912
|
};
|
|
@@ -811,13 +927,13 @@ class AzureClient extends InfraWalletClient {
|
|
|
811
927
|
}
|
|
812
928
|
}
|
|
813
929
|
|
|
814
|
-
class
|
|
930
|
+
class ConfluentClient extends InfraWalletClient {
|
|
815
931
|
static create(config, database, cache, logger) {
|
|
816
|
-
return new
|
|
932
|
+
return new ConfluentClient(CLOUD_PROVIDER.CONFLUENT, config, database, cache, logger);
|
|
817
933
|
}
|
|
818
934
|
convertServiceName(serviceName) {
|
|
819
935
|
let convertedName = serviceName;
|
|
820
|
-
const prefixes = ["
|
|
936
|
+
const prefixes = ["Confluent"];
|
|
821
937
|
for (const prefix of prefixes) {
|
|
822
938
|
if (serviceName.startsWith(prefix)) {
|
|
823
939
|
convertedName = serviceName.slice(prefix.length).trim();
|
|
@@ -825,183 +941,304 @@ class GCPClient extends InfraWalletClient {
|
|
|
825
941
|
}
|
|
826
942
|
return `${this.provider}/${convertedName}`;
|
|
827
943
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const projectId = subAccountConfig.getString("projectId");
|
|
831
|
-
const options = {
|
|
832
|
-
keyFilename: keyFilePath,
|
|
833
|
-
projectId
|
|
834
|
-
};
|
|
835
|
-
const bigqueryClient = new bigquery.BigQuery(options);
|
|
836
|
-
return bigqueryClient;
|
|
944
|
+
capitalizeWords(str) {
|
|
945
|
+
return str.toLowerCase().split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
837
946
|
}
|
|
838
|
-
async
|
|
839
|
-
|
|
947
|
+
async fetchEnvDisplayName(client, envId) {
|
|
948
|
+
const url = `https://api.confluent.cloud/org/v2/environments/${envId}`;
|
|
949
|
+
const response = await fetch(url, {
|
|
950
|
+
method: "GET",
|
|
951
|
+
headers: client.headers
|
|
952
|
+
});
|
|
953
|
+
if (!response.ok) {
|
|
954
|
+
throw new Error(`Failed to fetch environment name for ${envId}: ${response.statusText}`);
|
|
955
|
+
}
|
|
956
|
+
const jsonResponse = await response.json();
|
|
957
|
+
return jsonResponse.display_name;
|
|
840
958
|
}
|
|
841
|
-
async
|
|
842
|
-
|
|
959
|
+
async initCloudClient(subAccountConfig) {
|
|
960
|
+
const apiKey = subAccountConfig.getString("apiKey");
|
|
961
|
+
const apiSecret = subAccountConfig.getString("apiSecret");
|
|
962
|
+
const auth = `${apiKey}:${apiSecret}`;
|
|
963
|
+
const client = {
|
|
964
|
+
headers: {
|
|
965
|
+
Authorization: `Basic ${Buffer.from(auth).toString("base64")}`,
|
|
966
|
+
"Content-Type": "application/json"
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
return client;
|
|
843
970
|
}
|
|
844
|
-
async fetchCosts(
|
|
845
|
-
const
|
|
846
|
-
const
|
|
847
|
-
const
|
|
971
|
+
async fetchCosts(_subAccountConfig, client, query) {
|
|
972
|
+
const startDate = moment__default.default(parseInt(query.startTime, 10));
|
|
973
|
+
const endDate = moment__default.default(parseInt(query.endTime, 10));
|
|
974
|
+
const currentStartDate = startDate.clone();
|
|
975
|
+
let aggregatedData = [];
|
|
848
976
|
try {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
977
|
+
while (currentStartDate.isBefore(endDate) || currentStartDate.isSame(endDate, "month")) {
|
|
978
|
+
const currentEndDate = moment__default.default.min(currentStartDate.clone().endOf("month"), endDate);
|
|
979
|
+
const url = `https://api.confluent.cloud/billing/v1/costs?start_date=${currentStartDate.format(
|
|
980
|
+
"YYYY-MM-DD"
|
|
981
|
+
)}&end_date=${currentEndDate.add("1", "d").format("YYYY-MM-DD")}`;
|
|
982
|
+
const response = await fetch(url, {
|
|
983
|
+
method: "GET",
|
|
984
|
+
headers: client.headers
|
|
985
|
+
});
|
|
986
|
+
if (!response.ok) {
|
|
987
|
+
throw new Error(`Failed to fetch costs: ${response.statusText}`);
|
|
988
|
+
}
|
|
989
|
+
const jsonResponse = await response.json();
|
|
990
|
+
const envIds = [...new Set(jsonResponse.data.map((item) => item.resource.environment.id))];
|
|
991
|
+
const envNamePromises = envIds.map((envId) => this.fetchEnvDisplayName(client, envId));
|
|
992
|
+
const envNames = await Promise.all(envNamePromises);
|
|
993
|
+
const envIdToName = {};
|
|
994
|
+
envIds.forEach((envId, index) => {
|
|
995
|
+
envIdToName[envId] = envNames[index];
|
|
996
|
+
});
|
|
997
|
+
const dataWithEnvNames = jsonResponse.data.map((item) => {
|
|
998
|
+
const envId = item.resource.environment.id;
|
|
999
|
+
return {
|
|
1000
|
+
...item,
|
|
1001
|
+
envDisplayName: envIdToName[envId] || "Unknown"
|
|
1002
|
+
};
|
|
1003
|
+
});
|
|
1004
|
+
aggregatedData = aggregatedData.concat(dataWithEnvNames);
|
|
1005
|
+
currentStartDate.add(1, "month").startOf("month");
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
data: aggregatedData
|
|
1009
|
+
};
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
this.logger.error(`Error fetching costs from Confluent: ${error.message}`);
|
|
1012
|
+
throw error;
|
|
874
1013
|
}
|
|
875
1014
|
}
|
|
876
|
-
async transformCostsData(subAccountConfig,
|
|
1015
|
+
async transformCostsData(subAccountConfig, query, costResponse) {
|
|
877
1016
|
const categoryMappingService = CategoryMappingService.getInstance();
|
|
878
1017
|
const accountName = subAccountConfig.getString("name");
|
|
879
1018
|
const tags = subAccountConfig.getOptionalStringArray("tags");
|
|
880
1019
|
const tagKeyValues = {};
|
|
881
|
-
tags
|
|
1020
|
+
tags?.forEach((tag) => {
|
|
882
1021
|
const [k, v] = tag.split(":");
|
|
883
1022
|
tagKeyValues[k.trim()] = v.trim();
|
|
884
1023
|
});
|
|
885
|
-
const transformedData =
|
|
886
|
-
|
|
887
|
-
(
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
}
|
|
907
|
-
{
|
|
908
|
-
|
|
1024
|
+
const transformedData = costResponse.data.reduce((accumulator, line) => {
|
|
1025
|
+
const amount = parseFloat(line.amount) || 0;
|
|
1026
|
+
if (amount === 0) {
|
|
1027
|
+
return accumulator;
|
|
1028
|
+
}
|
|
1029
|
+
const parsedStartDate = moment__default.default(line.start_date);
|
|
1030
|
+
if (!parsedStartDate.isValid()) {
|
|
1031
|
+
return accumulator;
|
|
1032
|
+
}
|
|
1033
|
+
let billingPeriod = void 0;
|
|
1034
|
+
if (query.granularity.toUpperCase() === "MONTHLY") {
|
|
1035
|
+
billingPeriod = parsedStartDate.format("YYYY-MM");
|
|
1036
|
+
} else {
|
|
1037
|
+
billingPeriod = parsedStartDate.format("YYYY-MM-DD");
|
|
1038
|
+
}
|
|
1039
|
+
const serviceName = this.capitalizeWords(line.line_type);
|
|
1040
|
+
const resourceName = line.resource.display_name || "Unknown";
|
|
1041
|
+
const envDisplayName = line.envDisplayName;
|
|
1042
|
+
const keyName = `${accountName}->${categoryMappingService.getCategoryByServiceName(
|
|
1043
|
+
this.provider,
|
|
1044
|
+
serviceName
|
|
1045
|
+
)}->${resourceName}`;
|
|
1046
|
+
if (!accumulator[keyName]) {
|
|
1047
|
+
accumulator[keyName] = {
|
|
1048
|
+
id: keyName,
|
|
1049
|
+
account: `${this.provider}/${accountName}`,
|
|
1050
|
+
service: this.convertServiceName(serviceName),
|
|
1051
|
+
category: categoryMappingService.getCategoryByServiceName(this.provider, serviceName),
|
|
1052
|
+
provider: this.provider,
|
|
1053
|
+
providerType: PROVIDER_TYPE.INTEGRATION,
|
|
1054
|
+
reports: {},
|
|
1055
|
+
...{ project: envDisplayName },
|
|
1056
|
+
...{ cluster: resourceName },
|
|
1057
|
+
...tagKeyValues
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
if (!moment__default.default(billingPeriod).isBefore(moment__default.default(parseInt(query.startTime, 10)))) {
|
|
1061
|
+
accumulator[keyName].reports[billingPeriod] = (accumulator[keyName].reports[billingPeriod] || 0) + amount;
|
|
1062
|
+
}
|
|
1063
|
+
return accumulator;
|
|
1064
|
+
}, {});
|
|
909
1065
|
return Object.values(transformedData);
|
|
910
1066
|
}
|
|
911
1067
|
}
|
|
912
1068
|
|
|
913
|
-
class
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
this.config = config;
|
|
917
|
-
this.database = database;
|
|
918
|
-
this.cache = cache;
|
|
919
|
-
this.logger = logger;
|
|
1069
|
+
class CustomProviderClient extends InfraWalletClient {
|
|
1070
|
+
static create(config, database, cache, logger) {
|
|
1071
|
+
return new CustomProviderClient(CLOUD_PROVIDER.CUSTOM, config, database, cache, logger);
|
|
920
1072
|
}
|
|
921
|
-
async
|
|
922
|
-
|
|
923
|
-
|
|
1073
|
+
async initCloudClient(_config) {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
async fetchCosts(_integrationConfig, _client, query) {
|
|
1077
|
+
const records = getCustomCostsByDateRange(
|
|
1078
|
+
this.database,
|
|
1079
|
+
moment__default.default(parseInt(query.startTime, 10)).toDate(),
|
|
1080
|
+
moment__default.default(parseInt(query.endTime, 10)).toDate()
|
|
924
1081
|
);
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
"business_metrics.metric_provider": this.providerName.toLowerCase(),
|
|
938
|
-
"business_metrics.config_name": configName
|
|
939
|
-
}).select("business_metrics.*").from("business_metrics").join("wallets", "business_metrics.wallet_id", "=", "wallets.id");
|
|
940
|
-
for (const metric of metricSettings || []) {
|
|
941
|
-
const promise = (async () => {
|
|
1082
|
+
return records;
|
|
1083
|
+
}
|
|
1084
|
+
async transformCostsData(_subAccountConfig, query, costResponse) {
|
|
1085
|
+
const transformedData = lodash.reduce(
|
|
1086
|
+
costResponse,
|
|
1087
|
+
(accumulator, record) => {
|
|
1088
|
+
let periodFormat = "YYYY-MM";
|
|
1089
|
+
if (query.granularity === GRANULARITY.DAILY) {
|
|
1090
|
+
periodFormat = "YYYY-MM-DD";
|
|
1091
|
+
}
|
|
1092
|
+
const keyName = `${record.provider}-${record.account}-${record.service}`;
|
|
1093
|
+
if (typeof record.tags === "string") {
|
|
942
1094
|
try {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
)
|
|
970
|
-
|
|
971
|
-
results.push({
|
|
972
|
-
group: metric.group,
|
|
973
|
-
// add group info to the metric
|
|
974
|
-
...value
|
|
975
|
-
});
|
|
976
|
-
});
|
|
977
|
-
} catch (e) {
|
|
978
|
-
this.logger.error(e);
|
|
979
|
-
errors.push({
|
|
980
|
-
provider: this.providerName,
|
|
981
|
-
name: `${this.providerName}/${configName}/${metric.getString("metricName")}`,
|
|
982
|
-
error: e.message
|
|
1095
|
+
record.tags = JSON.parse(record.tags);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
this.logger.error(`Failed to parse tags for custom cost ${keyName}, tags: ${record.tags}`);
|
|
1098
|
+
record.tags = {};
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (!accumulator[keyName]) {
|
|
1102
|
+
accumulator[keyName] = {
|
|
1103
|
+
id: keyName,
|
|
1104
|
+
account: record.account,
|
|
1105
|
+
service: record.service,
|
|
1106
|
+
category: record.category,
|
|
1107
|
+
provider: record.provider,
|
|
1108
|
+
providerType: PROVIDER_TYPE.CUSTOM,
|
|
1109
|
+
reports: {}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
accumulator[keyName] = { ...accumulator[keyName], ...record.tags };
|
|
1113
|
+
const cost = parseFloat(record.cost);
|
|
1114
|
+
if (query.granularity === GRANULARITY.MONTHLY) {
|
|
1115
|
+
const period = moment__default.default(record.usage_month.toString(), "YYYYMM").format(periodFormat);
|
|
1116
|
+
accumulator[keyName].reports[period] = cost;
|
|
1117
|
+
} else {
|
|
1118
|
+
if (record.amortization_mode === "average") {
|
|
1119
|
+
const periods = getDailyPeriodStringsForOneMonth(record.usage_month);
|
|
1120
|
+
const averageCost = parseFloat(record.cost) / periods.length;
|
|
1121
|
+
periods.forEach((period) => {
|
|
1122
|
+
accumulator[keyName].reports[period] = averageCost;
|
|
983
1123
|
});
|
|
1124
|
+
} else if (record.amortization_mode === "start_day") {
|
|
1125
|
+
const period = moment__default.default(record.usage_month.toString(), "YYYYMM").startOf("month").format(periodFormat);
|
|
1126
|
+
accumulator[keyName].reports[period] = cost;
|
|
1127
|
+
} else {
|
|
1128
|
+
const period = moment__default.default(record.usage_month.toString(), "YYYYMM").endOf("month").format(periodFormat);
|
|
1129
|
+
accumulator[keyName].reports[period] = cost;
|
|
984
1130
|
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
}
|
|
1131
|
+
}
|
|
1132
|
+
return accumulator;
|
|
1133
|
+
},
|
|
1134
|
+
{}
|
|
1135
|
+
);
|
|
1136
|
+
return Object.values(transformedData);
|
|
1137
|
+
}
|
|
1138
|
+
// override this method so that we do not read from the config file
|
|
1139
|
+
async getCostReports(query) {
|
|
1140
|
+
const results = [];
|
|
1141
|
+
const errors = [];
|
|
1142
|
+
const cachedCosts = await getReportsFromCache(this.cache, this.provider, "custom", query);
|
|
1143
|
+
if (cachedCosts) {
|
|
1144
|
+
this.logger.debug(`${this.provider} costs from cache`);
|
|
1145
|
+
cachedCosts.forEach((cost) => {
|
|
1146
|
+
results.push(cost);
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
const costResponse = await this.fetchCosts(null, null, query);
|
|
1151
|
+
const transformedReports = await this.transformCostsData(null, query, costResponse);
|
|
1152
|
+
await setReportsToCache(
|
|
1153
|
+
this.cache,
|
|
1154
|
+
transformedReports,
|
|
1155
|
+
this.provider,
|
|
1156
|
+
"custom",
|
|
1157
|
+
query,
|
|
1158
|
+
getDefaultCacheTTL(CACHE_CATEGORY.COSTS, this.provider)
|
|
1159
|
+
);
|
|
1160
|
+
transformedReports.forEach((value) => {
|
|
1161
|
+
results.push(value);
|
|
1162
|
+
});
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
this.logger.error(e);
|
|
1165
|
+
errors.push({
|
|
1166
|
+
provider: this.provider,
|
|
1167
|
+
name: this.provider,
|
|
1168
|
+
error: e.message
|
|
1169
|
+
});
|
|
988
1170
|
}
|
|
989
|
-
await Promise.all(promises);
|
|
990
1171
|
return {
|
|
991
|
-
|
|
1172
|
+
reports: results,
|
|
992
1173
|
errors
|
|
993
1174
|
};
|
|
994
1175
|
}
|
|
995
1176
|
}
|
|
996
1177
|
|
|
997
|
-
class
|
|
1178
|
+
class DatadogClient extends InfraWalletClient {
|
|
998
1179
|
static create(config, database, cache, logger) {
|
|
999
|
-
return new
|
|
1180
|
+
return new DatadogClient(CLOUD_PROVIDER.DATADOG, config, database, cache, logger);
|
|
1000
1181
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1182
|
+
convertServiceName(serviceName) {
|
|
1183
|
+
let convertedName = serviceName;
|
|
1184
|
+
const aliases = /* @__PURE__ */ new Map([
|
|
1185
|
+
["apm_host", "APM Hosts"],
|
|
1186
|
+
["apm_host_enterprise", "APM Enterprise Hosts"],
|
|
1187
|
+
["application_vulnerability_management_oss_host", "Application Security - SCA Host"],
|
|
1188
|
+
["application_security_host", "ASM - Threat Management Hosts"],
|
|
1189
|
+
["audit_trail", "Audit Trail"],
|
|
1190
|
+
["ci_pipeline", "CI Visibility Committers"],
|
|
1191
|
+
["ci_pipeline_indexed_spans", "CI Visibility Spans"],
|
|
1192
|
+
["cloud_cost_management", "Cloud Cost Hosts"],
|
|
1193
|
+
["cspm_container", "Cloud Security Management Containers Pro"],
|
|
1194
|
+
["cspm_host", "Cloud Security Management Hosts Pro"],
|
|
1195
|
+
["csm_host_pro", "Cloud Security Management Hosts Pro"],
|
|
1196
|
+
["cws_host", "Cloud Workload Security Hosts"],
|
|
1197
|
+
["infra_container", "Containers"],
|
|
1198
|
+
["infra_container_excl_agent", "Containers"],
|
|
1199
|
+
["timeseries", "Custom Metrics"],
|
|
1200
|
+
["error_tracking", "Error Tracking"],
|
|
1201
|
+
["incident_management", "Incident Management"],
|
|
1202
|
+
["logs_indexed_15day", "Indexed Logs (15-day Retention)"],
|
|
1203
|
+
["logs_indexed_180day", "Indexed Logs (180-day Retention)"],
|
|
1204
|
+
["logs_indexed_1day", "Indexed Logs (1-day Retention)"],
|
|
1205
|
+
["logs_indexed_30day", "Indexed Logs (30-day Retention)"],
|
|
1206
|
+
["logs_indexed_360day", "Indexed Logs (360-day Retention)"],
|
|
1207
|
+
["logs_indexed_3day", "Indexed Logs (3-day Retention)"],
|
|
1208
|
+
["logs_indexed_45day", "Indexed Logs (45-day Retention)"],
|
|
1209
|
+
["logs_indexed_60day", "Indexed Logs (60-day Retention)"],
|
|
1210
|
+
["logs_indexed_7day", "Indexed Logs (7-day Retention)"],
|
|
1211
|
+
["logs_indexed_90day", "Indexed Logs (90-day Retention)"],
|
|
1212
|
+
["apm_trace_search", "Indexed Spans"],
|
|
1213
|
+
["infra_host", "Infra Hosts"],
|
|
1214
|
+
["logs_ingested", "Ingested Logs"],
|
|
1215
|
+
["ingested_spans", "Ingested Spans"],
|
|
1216
|
+
["iot", "IoT Devices"],
|
|
1217
|
+
["npm_host", "Network Hosts"],
|
|
1218
|
+
["prof_container", "Profiled Containers"],
|
|
1219
|
+
["prof_host", "Profiled Hosts"],
|
|
1220
|
+
["rum_lite", "RUM Sessions"],
|
|
1221
|
+
["rum_replay", "RUM with Session Replay Sessions"],
|
|
1222
|
+
["siem_indexed", "Security Analyzed and Indexed Logs"],
|
|
1223
|
+
["sensitive_data_scanner", "Sensitive Data Scanner"],
|
|
1224
|
+
["serverless_apps", "Serverless App Instances"],
|
|
1225
|
+
["serverless_apm", "Serverless Traced Invocations"],
|
|
1226
|
+
["serverless_infra", "Serverless Workload Functions"],
|
|
1227
|
+
["siem", "SIEM - Analyzed Logs"],
|
|
1228
|
+
["synthetics_api_tests", "Synthetics API Test Runs"],
|
|
1229
|
+
["synthetics_browser_checks", "Synthetics Browser Test Runs"],
|
|
1230
|
+
["ci_testing", "Test Visibility Committers"],
|
|
1231
|
+
["ci_test_indexed_spans", "Test Visibility Spans"]
|
|
1232
|
+
]);
|
|
1233
|
+
if (aliases.has(convertedName)) {
|
|
1234
|
+
convertedName = aliases.get(convertedName) || convertedName;
|
|
1235
|
+
}
|
|
1236
|
+
return `${this.provider}/${convertedName}`;
|
|
1237
|
+
}
|
|
1238
|
+
async initCloudClient(integrationConfig) {
|
|
1239
|
+
const apiKey = integrationConfig.getString("apiKey");
|
|
1240
|
+
const applicationKey = integrationConfig.getString("applicationKey");
|
|
1241
|
+
const ddSite = integrationConfig.getString("ddSite");
|
|
1005
1242
|
const configuration = datadogApiClient.client.createConfiguration({
|
|
1006
1243
|
baseServer: new datadogApiClient.client.BaseServerConfiguration(ddSite, {}),
|
|
1007
1244
|
authMethods: {
|
|
@@ -1009,138 +1246,218 @@ class DatadogProvider extends MetricProvider {
|
|
|
1009
1246
|
appKeyAuth: applicationKey
|
|
1010
1247
|
}
|
|
1011
1248
|
});
|
|
1012
|
-
const client = new datadogApiClient.
|
|
1249
|
+
const client = new datadogApiClient.v2.UsageMeteringApi(configuration);
|
|
1013
1250
|
return client;
|
|
1014
1251
|
}
|
|
1015
|
-
async
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1252
|
+
async fetchCosts(integrationConfig, client, query) {
|
|
1253
|
+
const costData = [];
|
|
1254
|
+
const startTime = moment__default.default(parseInt(query.startTime, 10));
|
|
1255
|
+
const endTime = moment__default.default(parseInt(query.endTime, 10));
|
|
1256
|
+
const firstDayOfLastMonth = moment__default.default().subtract(1, "M").startOf("M");
|
|
1257
|
+
if (startTime.isBefore(firstDayOfLastMonth)) {
|
|
1258
|
+
const historicalCost = await client.getHistoricalCostByOrg({
|
|
1259
|
+
startMonth: startTime,
|
|
1260
|
+
endMonth: firstDayOfLastMonth.subtract(1, "d"),
|
|
1261
|
+
view: "sub-org"
|
|
1262
|
+
});
|
|
1263
|
+
if (historicalCost.data) {
|
|
1264
|
+
costData.push(...historicalCost.data);
|
|
1025
1265
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
};
|
|
1040
|
-
for (const point of series.pointlist) {
|
|
1041
|
-
const period = moment__default.default(point[0]).format(query.granularity === "daily" ? "YYYY-MM-DD" : "YYYY-MM");
|
|
1042
|
-
const value = point[1];
|
|
1043
|
-
metric.reports[period] = value;
|
|
1266
|
+
}
|
|
1267
|
+
if (endTime.isSameOrAfter(firstDayOfLastMonth)) {
|
|
1268
|
+
let estimatedCostStartTime = startTime;
|
|
1269
|
+
if (startTime.isBefore(firstDayOfLastMonth)) {
|
|
1270
|
+
estimatedCostStartTime = firstDayOfLastMonth;
|
|
1271
|
+
}
|
|
1272
|
+
const estimatedCost = await client.getEstimatedCostByOrg({
|
|
1273
|
+
startMonth: estimatedCostStartTime,
|
|
1274
|
+
endMonth: endTime,
|
|
1275
|
+
view: "sub-org"
|
|
1276
|
+
});
|
|
1277
|
+
if (estimatedCost.data) {
|
|
1278
|
+
costData.push(...estimatedCost.data);
|
|
1044
1279
|
}
|
|
1045
|
-
transformedData.push(metric);
|
|
1046
1280
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
return new GrafanaCloudProvider("GrafanaCloud", config, database, cache, logger);
|
|
1054
|
-
}
|
|
1055
|
-
async initProviderClient(_config) {
|
|
1056
|
-
return null;
|
|
1057
|
-
}
|
|
1058
|
-
async fetchMetrics(metricProviderConfig, _client, query) {
|
|
1059
|
-
var _a;
|
|
1060
|
-
const url = metricProviderConfig.getString("url");
|
|
1061
|
-
const datasourceUid = metricProviderConfig.getString("datasourceUid");
|
|
1062
|
-
const token = metricProviderConfig.getString("token");
|
|
1063
|
-
const headers = {
|
|
1064
|
-
"Content-Type": "application/json",
|
|
1065
|
-
Authorization: `Bearer ${token}`
|
|
1066
|
-
};
|
|
1067
|
-
const payload = {
|
|
1068
|
-
queries: [
|
|
1069
|
-
{
|
|
1070
|
-
datasource: {
|
|
1071
|
-
uid: datasourceUid
|
|
1072
|
-
},
|
|
1073
|
-
expr: (_a = query.query) == null ? void 0 : _a.replaceAll("IW_INTERVAL", query.granularity === "daily" ? "1d" : "30d"),
|
|
1074
|
-
refId: "A"
|
|
1281
|
+
const costs = [];
|
|
1282
|
+
if (query.granularity === GRANULARITY.MONTHLY) {
|
|
1283
|
+
costData.forEach((costByOrg) => {
|
|
1284
|
+
const orgName = costByOrg.attributes?.orgName;
|
|
1285
|
+
if (!this.evaluateIntegrationFilters(orgName, integrationConfig)) {
|
|
1286
|
+
return;
|
|
1075
1287
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1288
|
+
costs.push({
|
|
1289
|
+
orgName,
|
|
1290
|
+
date: costByOrg.attributes?.date,
|
|
1291
|
+
// only keep cost breakdown
|
|
1292
|
+
charges: costByOrg.attributes?.charges?.filter((charge) => charge.chargeType !== "total")
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
} else {
|
|
1296
|
+
costData.forEach((costByOrg) => {
|
|
1297
|
+
const orgName = costByOrg.attributes?.orgName;
|
|
1298
|
+
if (!this.evaluateIntegrationFilters(orgName, integrationConfig)) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const daysInMonth = moment__default.default(costByOrg.attributes?.date).daysInMonth();
|
|
1302
|
+
costByOrg.attributes?.charges?.forEach((charge) => {
|
|
1303
|
+
if (charge.chargeType === "total") {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
for (let i = 0; i < daysInMonth; i++) {
|
|
1307
|
+
const dailyCost = {
|
|
1308
|
+
orgName,
|
|
1309
|
+
date: moment__default.default(costByOrg.attributes?.date).add(i, "d"),
|
|
1310
|
+
charges: [
|
|
1311
|
+
{
|
|
1312
|
+
productName: charge.productName,
|
|
1313
|
+
cost: (charge.cost || 0) / daysInMonth,
|
|
1314
|
+
chargeType: charge.chargeType
|
|
1315
|
+
}
|
|
1316
|
+
]
|
|
1317
|
+
};
|
|
1318
|
+
costs.push(dailyCost);
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
});
|
|
1103
1322
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1323
|
+
return costs;
|
|
1324
|
+
}
|
|
1325
|
+
async transformCostsData(subAccountConfig, query, costResponse) {
|
|
1326
|
+
const tags = subAccountConfig.getOptionalStringArray("tags");
|
|
1327
|
+
const tagKeyValues = {};
|
|
1328
|
+
tags?.forEach((tag) => {
|
|
1329
|
+
const [k, v] = tag.split(":");
|
|
1330
|
+
tagKeyValues[k.trim()] = v.trim();
|
|
1331
|
+
});
|
|
1332
|
+
const transformedData = lodash.reduce(
|
|
1333
|
+
costResponse,
|
|
1334
|
+
(accumulator, costByOrg) => {
|
|
1335
|
+
const account = costByOrg.orgName;
|
|
1336
|
+
const charges = costByOrg.charges;
|
|
1337
|
+
let periodFormat = "YYYY-MM";
|
|
1338
|
+
if (query.granularity === GRANULARITY.DAILY) {
|
|
1339
|
+
periodFormat = "YYYY-MM-DD";
|
|
1340
|
+
}
|
|
1341
|
+
const period = moment__default.default(costByOrg.date).format(periodFormat);
|
|
1342
|
+
if (charges) {
|
|
1343
|
+
charges.forEach((charge) => {
|
|
1344
|
+
const productName = charge.productName;
|
|
1345
|
+
const cost = charge.cost;
|
|
1346
|
+
const keyName = `${account}->${productName} (${charge.chargeType})`;
|
|
1347
|
+
if (!accumulator[keyName]) {
|
|
1348
|
+
accumulator[keyName] = {
|
|
1349
|
+
id: keyName,
|
|
1350
|
+
account: `${this.provider}/${account}`,
|
|
1351
|
+
service: `${this.convertServiceName(productName)} (${charge.chargeType})`,
|
|
1352
|
+
category: "Observability",
|
|
1353
|
+
provider: this.provider,
|
|
1354
|
+
providerType: PROVIDER_TYPE.INTEGRATION,
|
|
1355
|
+
reports: {},
|
|
1356
|
+
...tagKeyValues
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
accumulator[keyName].reports[period] = cost || 0;
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
return accumulator;
|
|
1363
|
+
},
|
|
1364
|
+
{}
|
|
1365
|
+
);
|
|
1366
|
+
return Object.values(transformedData);
|
|
1106
1367
|
}
|
|
1107
1368
|
}
|
|
1108
1369
|
|
|
1109
|
-
class
|
|
1370
|
+
class GCPClient extends InfraWalletClient {
|
|
1110
1371
|
static create(config, database, cache, logger) {
|
|
1111
|
-
return new
|
|
1372
|
+
return new GCPClient(CLOUD_PROVIDER.GCP, config, database, cache, logger);
|
|
1112
1373
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1374
|
+
convertServiceName(serviceName) {
|
|
1375
|
+
let convertedName = serviceName;
|
|
1376
|
+
const prefixes = ["Google Cloud"];
|
|
1377
|
+
for (const prefix of prefixes) {
|
|
1378
|
+
if (serviceName.startsWith(prefix)) {
|
|
1379
|
+
convertedName = serviceName.slice(prefix.length).trim();
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return `${this.provider}/${convertedName}`;
|
|
1115
1383
|
}
|
|
1116
|
-
async
|
|
1117
|
-
|
|
1384
|
+
async initCloudClient(subAccountConfig) {
|
|
1385
|
+
const keyFilePath = subAccountConfig.getString("keyFilePath");
|
|
1386
|
+
const projectId = subAccountConfig.getString("projectId");
|
|
1387
|
+
const options = {
|
|
1388
|
+
keyFilename: keyFilePath,
|
|
1389
|
+
projectId
|
|
1390
|
+
};
|
|
1391
|
+
const bigqueryClient = new bigquery.BigQuery(options);
|
|
1392
|
+
return bigqueryClient;
|
|
1118
1393
|
}
|
|
1119
|
-
async
|
|
1120
|
-
|
|
1121
|
-
const
|
|
1122
|
-
const
|
|
1123
|
-
let mockSettings = {};
|
|
1394
|
+
async fetchCosts(subAccountConfig, client, query) {
|
|
1395
|
+
const projectId = subAccountConfig.getString("projectId");
|
|
1396
|
+
const datasetId = subAccountConfig.getString("datasetId");
|
|
1397
|
+
const tableId = subAccountConfig.getString("tableId");
|
|
1124
1398
|
try {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1399
|
+
const periodFormat = query.granularity.toUpperCase() === "MONTHLY" ? "%Y-%m" : "%Y-%m-%d";
|
|
1400
|
+
const sql = `
|
|
1401
|
+
SELECT
|
|
1402
|
+
project.name AS project,
|
|
1403
|
+
service.description AS service,
|
|
1404
|
+
FORMAT_TIMESTAMP('${periodFormat}', usage_start_time) AS period,
|
|
1405
|
+
(SUM(CAST(cost AS NUMERIC)) + SUM(IFNULL((SELECT SUM(CAST(c.amount AS NUMERIC)) FROM UNNEST(credits) AS c), 0))) AS total_cost
|
|
1406
|
+
FROM
|
|
1407
|
+
\`${projectId}.${datasetId}.${tableId}\`
|
|
1408
|
+
WHERE
|
|
1409
|
+
project.name IS NOT NULL
|
|
1410
|
+
AND cost > 0
|
|
1411
|
+
AND usage_start_time >= TIMESTAMP_MILLIS(${query.startTime})
|
|
1412
|
+
AND usage_start_time <= TIMESTAMP_MILLIS(${query.endTime})
|
|
1413
|
+
GROUP BY
|
|
1414
|
+
project, service, period
|
|
1415
|
+
ORDER BY
|
|
1416
|
+
project, period, total_cost DESC`;
|
|
1417
|
+
const [job] = await client.createQueryJob({
|
|
1418
|
+
query: sql
|
|
1419
|
+
});
|
|
1420
|
+
const [rows] = await job.getQueryResults();
|
|
1421
|
+
return rows;
|
|
1422
|
+
} catch (err) {
|
|
1423
|
+
throw new Error(err.message);
|
|
1141
1424
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1425
|
+
}
|
|
1426
|
+
async transformCostsData(subAccountConfig, _query, costResponse) {
|
|
1427
|
+
const categoryMappingService = CategoryMappingService.getInstance();
|
|
1428
|
+
const accountName = subAccountConfig.getString("name");
|
|
1429
|
+
const tags = subAccountConfig.getOptionalStringArray("tags");
|
|
1430
|
+
const tagKeyValues = {};
|
|
1431
|
+
tags?.forEach((tag) => {
|
|
1432
|
+
const [k, v] = tag.split(":");
|
|
1433
|
+
tagKeyValues[k.trim()] = v.trim();
|
|
1434
|
+
});
|
|
1435
|
+
const transformedData = lodash.reduce(
|
|
1436
|
+
costResponse,
|
|
1437
|
+
(acc, row) => {
|
|
1438
|
+
const period = row.period;
|
|
1439
|
+
const keyName = `${accountName}_${row.project}_${row.service}`;
|
|
1440
|
+
if (!acc[keyName]) {
|
|
1441
|
+
acc[keyName] = {
|
|
1442
|
+
id: keyName,
|
|
1443
|
+
account: `${this.provider}/${accountName}`,
|
|
1444
|
+
service: this.convertServiceName(row.service),
|
|
1445
|
+
category: categoryMappingService.getCategoryByServiceName(this.provider, row.service),
|
|
1446
|
+
provider: this.provider,
|
|
1447
|
+
providerType: PROVIDER_TYPE.INTEGRATION,
|
|
1448
|
+
reports: {},
|
|
1449
|
+
...{ project: row.project },
|
|
1450
|
+
// TODO: how should we handle the project field? for now, we add project name as a field in the report
|
|
1451
|
+
...tagKeyValues
|
|
1452
|
+
// note that if there is a tag `project:foo` in config, it overrides the project field set above
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
acc[keyName].reports[period] = parseFloat(row.total_cost);
|
|
1456
|
+
return acc;
|
|
1457
|
+
},
|
|
1458
|
+
{}
|
|
1459
|
+
);
|
|
1460
|
+
return Object.values(transformedData);
|
|
1144
1461
|
}
|
|
1145
1462
|
}
|
|
1146
1463
|
|
|
@@ -1152,12 +1469,6 @@ class MockClient extends InfraWalletClient {
|
|
|
1152
1469
|
this.logger.debug(`MockClient.initCloudClient called with config: ${JSON.stringify(config)}`);
|
|
1153
1470
|
return null;
|
|
1154
1471
|
}
|
|
1155
|
-
async fetchTagKeys(_subAccountConfig, _client, _query) {
|
|
1156
|
-
return { tagKeys: [], provider: CLOUD_PROVIDER.MOCK };
|
|
1157
|
-
}
|
|
1158
|
-
async fetchTagValues(_subAccountConfig, _client, _query, _tagKey) {
|
|
1159
|
-
return { tagValues: [], provider: CLOUD_PROVIDER.MOCK };
|
|
1160
|
-
}
|
|
1161
1472
|
async fetchCosts(_subAccountConfig, _client, _query) {
|
|
1162
1473
|
return null;
|
|
1163
1474
|
}
|
|
@@ -1176,14 +1487,12 @@ class MockClient extends InfraWalletClient {
|
|
|
1176
1487
|
}
|
|
1177
1488
|
const processedData = await Promise.all(
|
|
1178
1489
|
jsonData.map(async (item) => {
|
|
1490
|
+
item.providerType = PROVIDER_TYPE.INTEGRATION;
|
|
1179
1491
|
item.reports = {};
|
|
1180
1492
|
const StartDate = moment__default.default(startD);
|
|
1181
|
-
let step;
|
|
1493
|
+
let step = "months";
|
|
1182
1494
|
let dateFormat = "YYYY-MM";
|
|
1183
|
-
if (query.granularity.toLowerCase() === "
|
|
1184
|
-
step = "months";
|
|
1185
|
-
dateFormat = "YYYY-MM";
|
|
1186
|
-
} else if (query.granularity.toLowerCase() === "daily") {
|
|
1495
|
+
if (query.granularity.toLowerCase() === "daily") {
|
|
1187
1496
|
step = "days";
|
|
1188
1497
|
dateFormat = "YYYY-MM-DD";
|
|
1189
1498
|
}
|
|
@@ -1212,150 +1521,6 @@ class MockClient extends InfraWalletClient {
|
|
|
1212
1521
|
}
|
|
1213
1522
|
}
|
|
1214
1523
|
|
|
1215
|
-
class ConfluentClient extends InfraWalletClient {
|
|
1216
|
-
static create(config, database, cache, logger) {
|
|
1217
|
-
return new ConfluentClient(CLOUD_PROVIDER.CONFLUENT, config, database, cache, logger);
|
|
1218
|
-
}
|
|
1219
|
-
convertServiceName(serviceName) {
|
|
1220
|
-
let convertedName = serviceName;
|
|
1221
|
-
const prefixes = ["Confluent"];
|
|
1222
|
-
for (const prefix of prefixes) {
|
|
1223
|
-
if (serviceName.startsWith(prefix)) {
|
|
1224
|
-
convertedName = serviceName.slice(prefix.length).trim();
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
return `${this.provider}/${convertedName}`;
|
|
1228
|
-
}
|
|
1229
|
-
capitalizeWords(str) {
|
|
1230
|
-
return str.toLowerCase().split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1231
|
-
}
|
|
1232
|
-
async fetchEnvDisplayName(client, envId) {
|
|
1233
|
-
const url = `https://api.confluent.cloud/org/v2/environments/${envId}`;
|
|
1234
|
-
const response = await fetch(url, {
|
|
1235
|
-
method: "GET",
|
|
1236
|
-
headers: client.headers
|
|
1237
|
-
});
|
|
1238
|
-
if (!response.ok) {
|
|
1239
|
-
throw new Error(`Failed to fetch environment name for ${envId}: ${response.statusText}`);
|
|
1240
|
-
}
|
|
1241
|
-
const jsonResponse = await response.json();
|
|
1242
|
-
return jsonResponse.display_name;
|
|
1243
|
-
}
|
|
1244
|
-
async initCloudClient(subAccountConfig) {
|
|
1245
|
-
const apiKey = subAccountConfig.getString("apiKey");
|
|
1246
|
-
const apiSecret = subAccountConfig.getString("apiSecret");
|
|
1247
|
-
const auth = `Basic ${Buffer.from(`${apiKey}:${apiSecret}`).toString("base64")}`;
|
|
1248
|
-
const client = {
|
|
1249
|
-
headers: {
|
|
1250
|
-
Authorization: auth,
|
|
1251
|
-
"Content-Type": "application/json"
|
|
1252
|
-
}
|
|
1253
|
-
};
|
|
1254
|
-
return client;
|
|
1255
|
-
}
|
|
1256
|
-
async fetchTagKeys(_subAccountConfig, _client, _query) {
|
|
1257
|
-
return { tagKeys: [], provider: CLOUD_PROVIDER.CONFLUENT };
|
|
1258
|
-
}
|
|
1259
|
-
async fetchTagValues(_subAccountConfig, _client, _query, _tagKey) {
|
|
1260
|
-
return { tagValues: [], provider: CLOUD_PROVIDER.CONFLUENT };
|
|
1261
|
-
}
|
|
1262
|
-
async fetchCosts(_subAccountConfig, client, query) {
|
|
1263
|
-
const startDate = moment__default.default(parseInt(query.startTime, 10));
|
|
1264
|
-
const endDate = moment__default.default(parseInt(query.endTime, 10));
|
|
1265
|
-
const currentStartDate = startDate.clone();
|
|
1266
|
-
let aggregatedData = [];
|
|
1267
|
-
try {
|
|
1268
|
-
while (currentStartDate.isBefore(endDate) || currentStartDate.isSame(endDate, "month")) {
|
|
1269
|
-
const currentEndDate = moment__default.default.min(currentStartDate.clone().endOf("month"), endDate);
|
|
1270
|
-
const url = `https://api.confluent.cloud/billing/v1/costs?start_date=${currentStartDate.format(
|
|
1271
|
-
"YYYY-MM-DD"
|
|
1272
|
-
)}&end_date=${currentEndDate.add("1", "d").format("YYYY-MM-DD")}`;
|
|
1273
|
-
const response = await fetch(url, {
|
|
1274
|
-
method: "GET",
|
|
1275
|
-
headers: client.headers
|
|
1276
|
-
});
|
|
1277
|
-
if (!response.ok) {
|
|
1278
|
-
throw new Error(`Failed to fetch costs: ${response.statusText}`);
|
|
1279
|
-
}
|
|
1280
|
-
const jsonResponse = await response.json();
|
|
1281
|
-
const envIds = [...new Set(jsonResponse.data.map((item) => item.resource.environment.id))];
|
|
1282
|
-
const envNamePromises = envIds.map((envId) => this.fetchEnvDisplayName(client, envId));
|
|
1283
|
-
const envNames = await Promise.all(envNamePromises);
|
|
1284
|
-
const envIdToName = {};
|
|
1285
|
-
envIds.forEach((envId, index) => {
|
|
1286
|
-
envIdToName[envId] = envNames[index];
|
|
1287
|
-
});
|
|
1288
|
-
const dataWithEnvNames = jsonResponse.data.map((item) => {
|
|
1289
|
-
const envId = item.resource.environment.id;
|
|
1290
|
-
return {
|
|
1291
|
-
...item,
|
|
1292
|
-
envDisplayName: envIdToName[envId] || "Unknown"
|
|
1293
|
-
};
|
|
1294
|
-
});
|
|
1295
|
-
aggregatedData = aggregatedData.concat(dataWithEnvNames);
|
|
1296
|
-
currentStartDate.add(1, "month").startOf("month");
|
|
1297
|
-
}
|
|
1298
|
-
return {
|
|
1299
|
-
data: aggregatedData
|
|
1300
|
-
};
|
|
1301
|
-
} catch (error) {
|
|
1302
|
-
this.logger.error(`Error fetching costs from Confluent: ${error.message}`);
|
|
1303
|
-
throw error;
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
async transformCostsData(subAccountConfig, query, costResponse) {
|
|
1307
|
-
const categoryMappingService = CategoryMappingService.getInstance();
|
|
1308
|
-
const accountName = subAccountConfig.getString("name");
|
|
1309
|
-
const tags = subAccountConfig.getOptionalStringArray("tags");
|
|
1310
|
-
const tagKeyValues = {};
|
|
1311
|
-
tags == null ? void 0 : tags.forEach((tag) => {
|
|
1312
|
-
const [k, v] = tag.split(":");
|
|
1313
|
-
tagKeyValues[k.trim()] = v.trim();
|
|
1314
|
-
});
|
|
1315
|
-
const transformedData = costResponse.data.reduce((accumulator, line) => {
|
|
1316
|
-
const amount = parseFloat(line.amount) || 0;
|
|
1317
|
-
let billingPeriod = "unknown";
|
|
1318
|
-
if (amount === 0) {
|
|
1319
|
-
return accumulator;
|
|
1320
|
-
}
|
|
1321
|
-
const parsedStartDate = moment__default.default(line.start_date);
|
|
1322
|
-
if (!parsedStartDate.isValid()) {
|
|
1323
|
-
return accumulator;
|
|
1324
|
-
}
|
|
1325
|
-
if (query.granularity.toUpperCase() === "MONTHLY") {
|
|
1326
|
-
billingPeriod = parsedStartDate.format("YYYY-MM");
|
|
1327
|
-
} else {
|
|
1328
|
-
billingPeriod = parsedStartDate.format("YYYY-MM-DD");
|
|
1329
|
-
}
|
|
1330
|
-
const serviceName = this.capitalizeWords(line.line_type);
|
|
1331
|
-
const resourceName = line.resource.display_name || "Unknown";
|
|
1332
|
-
const envDisplayName = line.envDisplayName;
|
|
1333
|
-
const keyName = `${accountName}->${categoryMappingService.getCategoryByServiceName(
|
|
1334
|
-
this.provider,
|
|
1335
|
-
serviceName
|
|
1336
|
-
)}->${resourceName}`;
|
|
1337
|
-
if (!accumulator[keyName]) {
|
|
1338
|
-
accumulator[keyName] = {
|
|
1339
|
-
id: keyName,
|
|
1340
|
-
account: `${this.provider}/${accountName}`,
|
|
1341
|
-
service: this.convertServiceName(serviceName),
|
|
1342
|
-
category: categoryMappingService.getCategoryByServiceName(this.provider, serviceName),
|
|
1343
|
-
provider: this.provider,
|
|
1344
|
-
reports: {},
|
|
1345
|
-
...{ project: envDisplayName },
|
|
1346
|
-
...{ cluster: resourceName },
|
|
1347
|
-
...tagKeyValues
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
if (!moment__default.default(billingPeriod).isBefore(moment__default.default(parseInt(query.startTime, 10)))) {
|
|
1351
|
-
accumulator[keyName].reports[billingPeriod] = (accumulator[keyName].reports[billingPeriod] || 0) + amount;
|
|
1352
|
-
}
|
|
1353
|
-
return accumulator;
|
|
1354
|
-
}, {});
|
|
1355
|
-
return Object.values(transformedData);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
1524
|
class MongoAtlasClient extends InfraWalletClient {
|
|
1360
1525
|
static create(config, database, cache, logger) {
|
|
1361
1526
|
return new MongoAtlasClient(CLOUD_PROVIDER.MONGODB_ATLAS, config, database, cache, logger);
|
|
@@ -1376,13 +1541,7 @@ class MongoAtlasClient extends InfraWalletClient {
|
|
|
1376
1541
|
const client = {
|
|
1377
1542
|
digestAuth: `${publicKey}:${privateKey}`
|
|
1378
1543
|
};
|
|
1379
|
-
return client;
|
|
1380
|
-
}
|
|
1381
|
-
async fetchTagKeys(_subAccountConfig, _client, _query) {
|
|
1382
|
-
return { tagKeys: [], provider: CLOUD_PROVIDER.MONGODB_ATLAS };
|
|
1383
|
-
}
|
|
1384
|
-
async fetchTagValues(_subAccountConfig, _client, _query, _tagKey) {
|
|
1385
|
-
return { tagValues: [], provider: CLOUD_PROVIDER.MONGODB_ATLAS };
|
|
1544
|
+
return client;
|
|
1386
1545
|
}
|
|
1387
1546
|
async fetchCosts(subAccountConfig, client, query) {
|
|
1388
1547
|
const orgId = subAccountConfig.getString("orgId");
|
|
@@ -1446,7 +1605,7 @@ class MongoAtlasClient extends InfraWalletClient {
|
|
|
1446
1605
|
const accountName = subAccountConfig.getString("name");
|
|
1447
1606
|
const tags = subAccountConfig.getOptionalStringArray("tags");
|
|
1448
1607
|
const tagKeyValues = {};
|
|
1449
|
-
tags
|
|
1608
|
+
tags?.forEach((tag) => {
|
|
1450
1609
|
const [k, v] = tag.split(":");
|
|
1451
1610
|
tagKeyValues[k.trim()] = v.trim();
|
|
1452
1611
|
});
|
|
@@ -1462,13 +1621,13 @@ class MongoAtlasClient extends InfraWalletClient {
|
|
|
1462
1621
|
rowData[columnName] = columns[index];
|
|
1463
1622
|
});
|
|
1464
1623
|
const amount = parseFloat(rowData.Amount) || 0;
|
|
1465
|
-
let billingPeriod = "unknown";
|
|
1466
1624
|
const dateFormat = "MM/DD/YYYY";
|
|
1467
1625
|
const date = rowData.Date;
|
|
1468
1626
|
const parsedDate = moment__default.default(date, dateFormat, true);
|
|
1469
1627
|
if (!parsedDate.isValid()) {
|
|
1470
1628
|
return accumulator;
|
|
1471
1629
|
}
|
|
1630
|
+
let billingPeriod = void 0;
|
|
1472
1631
|
if (query.granularity.toUpperCase() === "MONTHLY") {
|
|
1473
1632
|
billingPeriod = parsedDate.format("YYYY-MM");
|
|
1474
1633
|
} else {
|
|
@@ -1488,6 +1647,7 @@ class MongoAtlasClient extends InfraWalletClient {
|
|
|
1488
1647
|
service: this.convertServiceName(serviceName),
|
|
1489
1648
|
category: categoryMappingService.getCategoryByServiceName(this.provider, serviceName),
|
|
1490
1649
|
provider: this.provider,
|
|
1650
|
+
providerType: PROVIDER_TYPE.INTEGRATION,
|
|
1491
1651
|
reports: {},
|
|
1492
1652
|
...{ project },
|
|
1493
1653
|
...{ cluster },
|
|
@@ -1505,12 +1665,245 @@ class MongoAtlasClient extends InfraWalletClient {
|
|
|
1505
1665
|
}
|
|
1506
1666
|
}
|
|
1507
1667
|
|
|
1668
|
+
class MetricProvider {
|
|
1669
|
+
constructor(providerName, config, database, cache, logger) {
|
|
1670
|
+
this.providerName = providerName;
|
|
1671
|
+
this.config = config;
|
|
1672
|
+
this.database = database;
|
|
1673
|
+
this.cache = cache;
|
|
1674
|
+
this.logger = logger;
|
|
1675
|
+
}
|
|
1676
|
+
async getMetrics(query) {
|
|
1677
|
+
const conf = this.config.getOptionalConfigArray(
|
|
1678
|
+
`backend.infraWallet.metricProviders.${this.providerName.toLowerCase()}`
|
|
1679
|
+
);
|
|
1680
|
+
if (!conf) {
|
|
1681
|
+
return { metrics: [], errors: [] };
|
|
1682
|
+
}
|
|
1683
|
+
const promises = [];
|
|
1684
|
+
const results = [];
|
|
1685
|
+
const errors = [];
|
|
1686
|
+
for (const c of conf) {
|
|
1687
|
+
const configName = c.getString("name");
|
|
1688
|
+
const client = await this.initProviderClient(c);
|
|
1689
|
+
const dbClient = await this.database.getClient();
|
|
1690
|
+
const metricSettings = await dbClient.where({
|
|
1691
|
+
"wallets.name": query.walletName,
|
|
1692
|
+
"business_metrics.metric_provider": this.providerName.toLowerCase(),
|
|
1693
|
+
"business_metrics.config_name": configName
|
|
1694
|
+
}).select("business_metrics.*").from("business_metrics").join("wallets", "business_metrics.wallet_id", "=", "wallets.id");
|
|
1695
|
+
for (const metric of metricSettings || []) {
|
|
1696
|
+
const promise = (async () => {
|
|
1697
|
+
try {
|
|
1698
|
+
const fullQuery = {
|
|
1699
|
+
name: metric.metric_name,
|
|
1700
|
+
query: metric.query,
|
|
1701
|
+
...query
|
|
1702
|
+
};
|
|
1703
|
+
const cachedMetrics = await getMetricsFromCache(this.cache, this.providerName, configName, fullQuery);
|
|
1704
|
+
if (cachedMetrics) {
|
|
1705
|
+
this.logger.debug(`${this.providerName}/${configName}/${fullQuery.name} metrics from cache`);
|
|
1706
|
+
cachedMetrics.forEach((m) => {
|
|
1707
|
+
results.push({
|
|
1708
|
+
group: metric.group,
|
|
1709
|
+
// add group info to the metric
|
|
1710
|
+
...m
|
|
1711
|
+
});
|
|
1712
|
+
});
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
const metricResponse = await this.fetchMetrics(c, client, fullQuery);
|
|
1716
|
+
const transformedMetrics = await this.transformMetricData(c, fullQuery, metricResponse);
|
|
1717
|
+
await setMetricsToCache(
|
|
1718
|
+
this.cache,
|
|
1719
|
+
transformedMetrics,
|
|
1720
|
+
this.providerName,
|
|
1721
|
+
configName,
|
|
1722
|
+
fullQuery,
|
|
1723
|
+
60 * 60 * 2 * 1e3
|
|
1724
|
+
);
|
|
1725
|
+
transformedMetrics.forEach((value) => {
|
|
1726
|
+
results.push({
|
|
1727
|
+
group: metric.group,
|
|
1728
|
+
// add group info to the metric
|
|
1729
|
+
...value
|
|
1730
|
+
});
|
|
1731
|
+
});
|
|
1732
|
+
} catch (e) {
|
|
1733
|
+
this.logger.error(e);
|
|
1734
|
+
errors.push({
|
|
1735
|
+
provider: this.providerName,
|
|
1736
|
+
name: `${this.providerName}/${configName}/${metric.getString("metricName")}`,
|
|
1737
|
+
error: e.message
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
})();
|
|
1741
|
+
promises.push(promise);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
await Promise.all(promises);
|
|
1745
|
+
return {
|
|
1746
|
+
metrics: results,
|
|
1747
|
+
errors
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
class DatadogProvider extends MetricProvider {
|
|
1753
|
+
static create(config, database, cache, logger) {
|
|
1754
|
+
return new DatadogProvider("Datadog", config, database, cache, logger);
|
|
1755
|
+
}
|
|
1756
|
+
async initProviderClient(config) {
|
|
1757
|
+
const apiKey = config.getString("apiKey");
|
|
1758
|
+
const applicationKey = config.getString("applicationKey");
|
|
1759
|
+
const ddSite = config.getString("ddSite");
|
|
1760
|
+
const configuration = datadogApiClient.client.createConfiguration({
|
|
1761
|
+
baseServer: new datadogApiClient.client.BaseServerConfiguration(ddSite, {}),
|
|
1762
|
+
authMethods: {
|
|
1763
|
+
apiKeyAuth: apiKey,
|
|
1764
|
+
appKeyAuth: applicationKey
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
const client = new datadogApiClient.v1.MetricsApi(configuration);
|
|
1768
|
+
return client;
|
|
1769
|
+
}
|
|
1770
|
+
async fetchMetrics(_metricProviderConfig, client, query) {
|
|
1771
|
+
const params = {
|
|
1772
|
+
from: parseInt(query.startTime, 10) / 1e3,
|
|
1773
|
+
to: parseInt(query.endTime, 10) / 1e3,
|
|
1774
|
+
query: query.query?.replaceAll("IW_INTERVAL", query.granularity === "daily" ? "86400" : "2592000")
|
|
1775
|
+
};
|
|
1776
|
+
return client.queryMetrics(params).then((data) => {
|
|
1777
|
+
if (data.status === "ok") {
|
|
1778
|
+
return data;
|
|
1779
|
+
}
|
|
1780
|
+
throw new Error(data.error);
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
async transformMetricData(_metricProviderConfig, query, metricResponse) {
|
|
1784
|
+
const transformedData = [];
|
|
1785
|
+
for (const series of metricResponse.series) {
|
|
1786
|
+
const metricName = query.name;
|
|
1787
|
+
const tagSet = series.tagSet;
|
|
1788
|
+
const metric = {
|
|
1789
|
+
id: `${metricName} ${tagSet.length === 0 ? "" : tagSet}`,
|
|
1790
|
+
provider: this.providerName,
|
|
1791
|
+
name: metricName,
|
|
1792
|
+
reports: {}
|
|
1793
|
+
};
|
|
1794
|
+
for (const point of series.pointlist) {
|
|
1795
|
+
const period = moment__default.default(point[0]).format(query.granularity === "daily" ? "YYYY-MM-DD" : "YYYY-MM");
|
|
1796
|
+
const value = point[1];
|
|
1797
|
+
metric.reports[period] = value;
|
|
1798
|
+
}
|
|
1799
|
+
transformedData.push(metric);
|
|
1800
|
+
}
|
|
1801
|
+
return transformedData;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
class GrafanaCloudProvider extends MetricProvider {
|
|
1806
|
+
static create(config, database, cache, logger) {
|
|
1807
|
+
return new GrafanaCloudProvider("GrafanaCloud", config, database, cache, logger);
|
|
1808
|
+
}
|
|
1809
|
+
async initProviderClient(_config) {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
async fetchMetrics(metricProviderConfig, _client, query) {
|
|
1813
|
+
const url = metricProviderConfig.getString("url");
|
|
1814
|
+
const datasourceUid = metricProviderConfig.getString("datasourceUid");
|
|
1815
|
+
const token = metricProviderConfig.getString("token");
|
|
1816
|
+
const headers = {
|
|
1817
|
+
"Content-Type": "application/json",
|
|
1818
|
+
Authorization: `Bearer ${token}`
|
|
1819
|
+
};
|
|
1820
|
+
const payload = {
|
|
1821
|
+
queries: [
|
|
1822
|
+
{
|
|
1823
|
+
datasource: {
|
|
1824
|
+
uid: datasourceUid
|
|
1825
|
+
},
|
|
1826
|
+
expr: query.query?.replaceAll("IW_INTERVAL", query.granularity === "daily" ? "1d" : "30d"),
|
|
1827
|
+
refId: "A"
|
|
1828
|
+
}
|
|
1829
|
+
],
|
|
1830
|
+
from: query.startTime,
|
|
1831
|
+
to: query.endTime
|
|
1832
|
+
};
|
|
1833
|
+
const response = await fetch__default.default(`${url}/api/ds/query`, {
|
|
1834
|
+
method: "post",
|
|
1835
|
+
body: JSON.stringify(payload),
|
|
1836
|
+
headers
|
|
1837
|
+
});
|
|
1838
|
+
const data = await response.json();
|
|
1839
|
+
return data;
|
|
1840
|
+
}
|
|
1841
|
+
async transformMetricData(_metricProviderConfig, query, metricResponse) {
|
|
1842
|
+
const transformedData = [];
|
|
1843
|
+
const metricName = query.name;
|
|
1844
|
+
const metric = {
|
|
1845
|
+
id: metricName,
|
|
1846
|
+
provider: this.providerName,
|
|
1847
|
+
name: metricName,
|
|
1848
|
+
reports: {}
|
|
1849
|
+
};
|
|
1850
|
+
const periods = metricResponse.results.A.frames[0].data.values[0];
|
|
1851
|
+
const values = metricResponse.results.A.frames[0].data.values[1];
|
|
1852
|
+
for (let i = 0; i < periods.length; i++) {
|
|
1853
|
+
const period = moment__default.default(periods[i]).format(query.granularity === "daily" ? "YYYY-MM-DD" : "YYYY-MM");
|
|
1854
|
+
const value = values[i];
|
|
1855
|
+
metric.reports[period] = value;
|
|
1856
|
+
}
|
|
1857
|
+
transformedData.push(metric);
|
|
1858
|
+
return transformedData;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
class MockProvider extends MetricProvider {
|
|
1863
|
+
static create(config, database, cache, logger) {
|
|
1864
|
+
return new MockProvider("Mock", config, database, cache, logger);
|
|
1865
|
+
}
|
|
1866
|
+
async initProviderClient(_config) {
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
async fetchMetrics(_metricProviderConfig, _client, _query) {
|
|
1870
|
+
return null;
|
|
1871
|
+
}
|
|
1872
|
+
async transformMetricData(_metricProviderConfig, query, _metricResponse) {
|
|
1873
|
+
const transformedData = [];
|
|
1874
|
+
const metricName = query.name;
|
|
1875
|
+
let mockSettings = {};
|
|
1876
|
+
try {
|
|
1877
|
+
mockSettings = JSON.parse(query.query);
|
|
1878
|
+
} catch (e) {
|
|
1879
|
+
}
|
|
1880
|
+
const minValue = mockSettings.min ?? 0;
|
|
1881
|
+
const maxValue = mockSettings.max ?? 1e3;
|
|
1882
|
+
const metric = {
|
|
1883
|
+
id: metricName,
|
|
1884
|
+
provider: this.providerName,
|
|
1885
|
+
name: metricName,
|
|
1886
|
+
reports: {}
|
|
1887
|
+
};
|
|
1888
|
+
let cursor = moment__default.default(parseInt(query.startTime, 10));
|
|
1889
|
+
while (cursor <= moment__default.default(parseInt(query.endTime, 10))) {
|
|
1890
|
+
const period = cursor.format(query.granularity === "daily" ? "YYYY-MM-DD" : "YYYY-MM");
|
|
1891
|
+
metric.reports[period] = Math.floor(Math.random() * (maxValue - minValue) + minValue);
|
|
1892
|
+
cursor = cursor.add(1, query.granularity === "daily" ? "days" : "months");
|
|
1893
|
+
}
|
|
1894
|
+
transformedData.push(metric);
|
|
1895
|
+
return transformedData;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1508
1899
|
var CLOUD_PROVIDER = /* @__PURE__ */ ((CLOUD_PROVIDER2) => {
|
|
1509
1900
|
CLOUD_PROVIDER2["AWS"] = "AWS";
|
|
1510
1901
|
CLOUD_PROVIDER2["GCP"] = "GCP";
|
|
1511
1902
|
CLOUD_PROVIDER2["AZURE"] = "Azure";
|
|
1512
1903
|
CLOUD_PROVIDER2["MONGODB_ATLAS"] = "MongoAtlas";
|
|
1513
1904
|
CLOUD_PROVIDER2["CONFLUENT"] = "Confluent";
|
|
1905
|
+
CLOUD_PROVIDER2["DATADOG"] = "Datadog";
|
|
1906
|
+
CLOUD_PROVIDER2["CUSTOM"] = "Custom";
|
|
1514
1907
|
CLOUD_PROVIDER2["MOCK"] = "Mock";
|
|
1515
1908
|
return CLOUD_PROVIDER2;
|
|
1516
1909
|
})(CLOUD_PROVIDER || {});
|
|
@@ -1520,6 +1913,8 @@ const COST_CLIENT_MAPPINGS = {
|
|
|
1520
1913
|
gcp: GCPClient,
|
|
1521
1914
|
confluent: ConfluentClient,
|
|
1522
1915
|
mongoatlas: MongoAtlasClient,
|
|
1916
|
+
datadog: DatadogClient,
|
|
1917
|
+
custom: CustomProviderClient,
|
|
1523
1918
|
mock: MockClient
|
|
1524
1919
|
};
|
|
1525
1920
|
const METRIC_PROVIDER_MAPPINGS = {
|
|
@@ -1527,6 +1922,11 @@ const METRIC_PROVIDER_MAPPINGS = {
|
|
|
1527
1922
|
grafanacloud: GrafanaCloudProvider,
|
|
1528
1923
|
mock: MockProvider
|
|
1529
1924
|
};
|
|
1925
|
+
var GRANULARITY = /* @__PURE__ */ ((GRANULARITY2) => {
|
|
1926
|
+
GRANULARITY2["DAILY"] = "daily";
|
|
1927
|
+
GRANULARITY2["MONTHLY"] = "monthly";
|
|
1928
|
+
return GRANULARITY2;
|
|
1929
|
+
})(GRANULARITY || {});
|
|
1530
1930
|
var CACHE_CATEGORY = /* @__PURE__ */ ((CACHE_CATEGORY2) => {
|
|
1531
1931
|
CACHE_CATEGORY2["COSTS"] = "costs";
|
|
1532
1932
|
CACHE_CATEGORY2["TAGS"] = "tags";
|
|
@@ -1542,7 +1942,10 @@ const DEFAULT_TAGS_CACHE_TTL = {
|
|
|
1542
1942
|
["GCP" /* GCP */]: 1 * 60 * 60 * 1e3,
|
|
1543
1943
|
["MongoAtlas" /* MONGODB_ATLAS */]: 1 * 60 * 60 * 1e3,
|
|
1544
1944
|
["Confluent" /* CONFLUENT */]: 1 * 60 * 60 * 1e3,
|
|
1945
|
+
["Datadog" /* DATADOG */]: 1 * 60 * 60 * 1e3,
|
|
1946
|
+
["Custom" /* CUSTOM */]: 1,
|
|
1545
1947
|
["Mock" /* MOCK */]: 0
|
|
1948
|
+
// NOTE: 0 means never expired!
|
|
1546
1949
|
};
|
|
1547
1950
|
const DEFAULT_COSTS_CACHE_TTL = {
|
|
1548
1951
|
["AWS" /* AWS */]: 2 * 60 * 60 * 1e3,
|
|
@@ -1551,33 +1954,37 @@ const DEFAULT_COSTS_CACHE_TTL = {
|
|
|
1551
1954
|
["GCP" /* GCP */]: 2 * 60 * 60 * 1e3,
|
|
1552
1955
|
["MongoAtlas" /* MONGODB_ATLAS */]: 2 * 60 * 60 * 1e3,
|
|
1553
1956
|
["Confluent" /* CONFLUENT */]: 2 * 60 * 60 * 1e3,
|
|
1957
|
+
["Datadog" /* DATADOG */]: 2 * 60 * 60 * 1e3,
|
|
1958
|
+
["Custom" /* CUSTOM */]: 1,
|
|
1959
|
+
// do not cache custom costs since they are in the plugin database
|
|
1554
1960
|
["Mock" /* MOCK */]: 0
|
|
1961
|
+
// NOTE: 0 means never expired!
|
|
1555
1962
|
};
|
|
1963
|
+
var PROVIDER_TYPE = /* @__PURE__ */ ((PROVIDER_TYPE2) => {
|
|
1964
|
+
PROVIDER_TYPE2["INTEGRATION"] = "Integration";
|
|
1965
|
+
PROVIDER_TYPE2["CUSTOM"] = "Custom";
|
|
1966
|
+
return PROVIDER_TYPE2;
|
|
1967
|
+
})(PROVIDER_TYPE || {});
|
|
1556
1968
|
|
|
1557
|
-
|
|
1558
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1559
|
-
var __publicField = (obj, key, value) => {
|
|
1560
|
-
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1561
|
-
return value;
|
|
1562
|
-
};
|
|
1563
|
-
const _CategoryMappingService = class _CategoryMappingService {
|
|
1969
|
+
class CategoryMappingService {
|
|
1564
1970
|
constructor(cache, logger) {
|
|
1565
1971
|
this.cache = cache;
|
|
1566
1972
|
this.logger = logger;
|
|
1567
|
-
__publicField(this, "categoryMappings", {});
|
|
1568
|
-
__publicField(this, "serviceToCategory", {});
|
|
1569
1973
|
}
|
|
1974
|
+
static instance;
|
|
1570
1975
|
static initInstance(cache, logger) {
|
|
1571
|
-
if (!
|
|
1572
|
-
|
|
1976
|
+
if (!CategoryMappingService.instance) {
|
|
1977
|
+
CategoryMappingService.instance = new CategoryMappingService(cache, logger);
|
|
1573
1978
|
}
|
|
1574
1979
|
}
|
|
1575
1980
|
static getInstance() {
|
|
1576
|
-
if (!
|
|
1981
|
+
if (!CategoryMappingService.instance) {
|
|
1577
1982
|
throw new Error("CategoryMappingService needs to be initialized first");
|
|
1578
1983
|
}
|
|
1579
|
-
return
|
|
1984
|
+
return CategoryMappingService.instance;
|
|
1580
1985
|
}
|
|
1986
|
+
categoryMappings = {};
|
|
1987
|
+
serviceToCategory = {};
|
|
1581
1988
|
generateServiceToCategoryMappings(categoryMappings) {
|
|
1582
1989
|
const result = {};
|
|
1583
1990
|
for (const [category, mappings] of Object.entries(categoryMappings)) {
|
|
@@ -1641,15 +2048,12 @@ const _CategoryMappingService = class _CategoryMappingService {
|
|
|
1641
2048
|
this.logger.debug(`serviceToCategoryMappings updated: ${providerLowerCase}/${serviceName} -> ${result}`);
|
|
1642
2049
|
return result;
|
|
1643
2050
|
}
|
|
1644
|
-
}
|
|
1645
|
-
__publicField(_CategoryMappingService, "instance");
|
|
1646
|
-
let CategoryMappingService = _CategoryMappingService;
|
|
2051
|
+
}
|
|
1647
2052
|
|
|
1648
2053
|
async function setUpDatabase(database) {
|
|
1649
|
-
var _a;
|
|
1650
2054
|
const client = await database.getClient();
|
|
1651
2055
|
const migrationsDir = backendPluginApi.resolvePackagePath("@electrolux-oss/plugin-infrawallet-backend", "migrations");
|
|
1652
|
-
if (!
|
|
2056
|
+
if (!database.migrations?.skip) {
|
|
1653
2057
|
await client.migrate.latest({
|
|
1654
2058
|
directory: migrationsDir
|
|
1655
2059
|
});
|
|
@@ -1688,7 +2092,7 @@ async function createRouter(options) {
|
|
|
1688
2092
|
const categoryMappingService = CategoryMappingService.getInstance();
|
|
1689
2093
|
await categoryMappingService.refreshCategoryMappings();
|
|
1690
2094
|
const conf = config.getConfig("backend.infraWallet.integrations");
|
|
1691
|
-
conf.keys().forEach((provider) => {
|
|
2095
|
+
conf.keys().concat(["custom"]).forEach((provider) => {
|
|
1692
2096
|
if (provider in COST_CLIENT_MAPPINGS) {
|
|
1693
2097
|
const client = COST_CLIENT_MAPPINGS[provider].create(config, database, cache, logger);
|
|
1694
2098
|
const fetchCloudCosts = (async () => {
|
|
@@ -1824,6 +2228,62 @@ async function createRouter(options) {
|
|
|
1824
2228
|
response.json({ data: tags, errors, status: 200 });
|
|
1825
2229
|
}
|
|
1826
2230
|
});
|
|
2231
|
+
router.get("/:walletName/budgets", async (request, response) => {
|
|
2232
|
+
const walletName = request.params.walletName;
|
|
2233
|
+
const provider = request.query.provider;
|
|
2234
|
+
let budgets;
|
|
2235
|
+
if (provider) {
|
|
2236
|
+
budgets = await getBudget(database, walletName, provider);
|
|
2237
|
+
} else {
|
|
2238
|
+
budgets = await getBudgets(database, walletName);
|
|
2239
|
+
}
|
|
2240
|
+
response.json({ data: budgets, status: 200 });
|
|
2241
|
+
});
|
|
2242
|
+
router.put("/:walletName/budgets", async (request, response) => {
|
|
2243
|
+
const walletName = request.params.walletName;
|
|
2244
|
+
const result = await upsertBudget(database, walletName, request.body);
|
|
2245
|
+
response.json({ updated: result, status: 200 });
|
|
2246
|
+
});
|
|
2247
|
+
router.get("/custom-costs", async (_request, response) => {
|
|
2248
|
+
const customCosts = await getCustomCosts(database);
|
|
2249
|
+
for (const cost of customCosts) {
|
|
2250
|
+
if (typeof cost.tags === "string") {
|
|
2251
|
+
try {
|
|
2252
|
+
cost.tags = JSON.parse(cost.tags);
|
|
2253
|
+
} catch (error) {
|
|
2254
|
+
cost.tags = {};
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
response.json({ data: customCosts, status: 200 });
|
|
2259
|
+
});
|
|
2260
|
+
router.post("/custom-costs", async (request, response) => {
|
|
2261
|
+
const readOnly = config.getOptionalBoolean("infraWallet.settings.readOnly") ?? false;
|
|
2262
|
+
if (readOnly) {
|
|
2263
|
+
response.status(403).json({ error: "API not enabled in read-only mode", status: 403 });
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
const updatedCustomCost = await createCustomCosts(database, request.body);
|
|
2267
|
+
response.json({ created: updatedCustomCost, status: 200 });
|
|
2268
|
+
});
|
|
2269
|
+
router.put("/custom-cost", async (request, response) => {
|
|
2270
|
+
const readOnly = config.getOptionalBoolean("infraWallet.settings.readOnly") ?? false;
|
|
2271
|
+
if (readOnly) {
|
|
2272
|
+
response.status(403).json({ error: "API not enabled in read-only mode", status: 403 });
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
const updatedCustomCost = await updateOrInsertCustomCost(database, request.body);
|
|
2276
|
+
response.json({ updated: updatedCustomCost, status: 200 });
|
|
2277
|
+
});
|
|
2278
|
+
router.delete("/custom-cost", async (request, response) => {
|
|
2279
|
+
const readOnly = config.getOptionalBoolean("infraWallet.settings.readOnly") ?? false;
|
|
2280
|
+
if (readOnly) {
|
|
2281
|
+
response.status(403).json({ error: "API not enabled in read-only mode", status: 403 });
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
const deletedCustomCost = await deleteCustomCost(database, request.body);
|
|
2285
|
+
response.json({ deleted: deletedCustomCost, status: 200 });
|
|
2286
|
+
});
|
|
1827
2287
|
router.get("/:walletName/metrics", async (request, response) => {
|
|
1828
2288
|
const walletName = request.params.walletName;
|
|
1829
2289
|
const granularity = request.query.granularity;
|
|
@@ -1878,12 +2338,12 @@ async function createRouter(options) {
|
|
|
1878
2338
|
}
|
|
1879
2339
|
response.json({ data: wallet, status: 200 });
|
|
1880
2340
|
});
|
|
1881
|
-
router.get("/:walletName/
|
|
2341
|
+
router.get("/:walletName/metrics-setting", async (request, response) => {
|
|
1882
2342
|
const walletName = request.params.walletName;
|
|
1883
2343
|
const metricSettings = await getWalletMetricSettings(database, walletName);
|
|
1884
2344
|
response.json({ data: metricSettings, status: 200 });
|
|
1885
2345
|
});
|
|
1886
|
-
router.get("/metric/
|
|
2346
|
+
router.get("/metric/metric-configs", async (_request, response) => {
|
|
1887
2347
|
const conf = config.getConfig("backend.infraWallet.metricProviders");
|
|
1888
2348
|
const configNames = [];
|
|
1889
2349
|
conf.keys().forEach((provider) => {
|
|
@@ -1896,9 +2356,8 @@ async function createRouter(options) {
|
|
|
1896
2356
|
});
|
|
1897
2357
|
response.json({ data: configNames, status: 200 });
|
|
1898
2358
|
});
|
|
1899
|
-
router.put("/:walletName/
|
|
1900
|
-
|
|
1901
|
-
const readOnly = (_a = config.getOptionalBoolean("infraWallet.settings.readOnly")) != null ? _a : false;
|
|
2359
|
+
router.put("/:walletName/metrics-setting", async (request, response) => {
|
|
2360
|
+
const readOnly = config.getOptionalBoolean("infraWallet.settings.readOnly") ?? false;
|
|
1902
2361
|
if (readOnly) {
|
|
1903
2362
|
response.status(403).json({ error: "API not enabled in read-only mode", status: 403 });
|
|
1904
2363
|
return;
|
|
@@ -1906,9 +2365,8 @@ async function createRouter(options) {
|
|
|
1906
2365
|
const updatedMetricSetting = await updateOrInsertWalletMetricSetting(database, request.body);
|
|
1907
2366
|
response.json({ updated: updatedMetricSetting, status: 200 });
|
|
1908
2367
|
});
|
|
1909
|
-
router.delete("/:walletName/
|
|
1910
|
-
|
|
1911
|
-
const readOnly = (_a = config.getOptionalBoolean("infraWallet.settings.readOnly")) != null ? _a : false;
|
|
2368
|
+
router.delete("/:walletName/metrics-setting", async (request, response) => {
|
|
2369
|
+
const readOnly = config.getOptionalBoolean("infraWallet.settings.readOnly") ?? false;
|
|
1912
2370
|
if (readOnly) {
|
|
1913
2371
|
response.status(403).json({ error: "API not enabled in read-only mode", status: 403 });
|
|
1914
2372
|
return;
|