@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/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 fetch$1 = require('node-fetch');
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 || tags[0] !== "(" || tags[tags.length - 1] !== ")") {
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 != null ? ttl : 60 * 60 * 2 * 1e3
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 || filters[0] !== "(" || filters[filters.length - 1] !== ")") {
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 accounts = this.config.getOptionalConfigArray(
352
+ const integrationConfigs = this.config.getOptionalConfigArray(
248
353
  `backend.infraWallet.integrations.${this.provider.toLowerCase()}`
249
354
  );
250
- if (!accounts) {
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 account of accounts) {
257
- const accountName = account.getString("name");
258
- const cachedTagKeys = await getTagKeysFromCache(this.cache, this.provider, accountName, query);
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}/${accountName} tag keys from cache`);
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(account);
271
- const response = await this.fetchTagKeys(account, client, query);
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, accountName, query);
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}/${accountName}`,
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.sort((a, b) => `${a.provider}/${a.key}`.localeCompare(`${b.provider}/${b.key}`)),
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 accounts = this.config.getOptionalConfigArray(
406
+ const integrationConfigs = this.config.getOptionalConfigArray(
301
407
  `backend.infraWallet.integrations.${this.provider.toLowerCase()}`
302
408
  );
303
- if (!accounts) {
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 account of accounts) {
310
- const accountName = account.getString("name");
311
- const cachedTagValues = await getTagValuesFromCache(this.cache, this.provider, accountName, tagKey, query);
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}/${accountName}/${tagKey} tag values from cache`);
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(account);
324
- const response = await this.fetchTagValues(account, client, query, tagKey);
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, accountName, tagKey, query);
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}/${accountName}`,
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.sort(
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 accounts = this.config.getOptionalConfigArray(
461
+ const integrationConfigs = this.config.getOptionalConfigArray(
355
462
  `backend.infraWallet.integrations.${this.provider.toLowerCase()}`
356
463
  );
357
- if (!accounts) {
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 account of accounts) {
364
- const accountName = account.getString("name");
365
- const cachedCosts = await getReportsFromCache(this.cache, this.provider, accountName, query);
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}/${accountName} costs from cache`);
368
- cachedCosts.map((cost) => {
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(account);
376
- const costResponse = await this.fetchCosts(account, client, query);
377
- const transformedReports = await this.transformCostsData(account, query, costResponse);
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
- accountName,
489
+ integrationName,
383
490
  query,
384
491
  getDefaultCacheTTL(CACHE_CATEGORY.COSTS, this.provider)
385
492
  );
386
- transformedReports.map((value) => {
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}/${accountName}`,
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
- constructor() {
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(subAccountConfig) {
449
- var _a, _b, _c;
450
- const accountId = subAccountConfig.getString("accountId");
451
- const assumedRoleName = subAccountConfig.getString("assumedRoleName");
452
- const accessKeyId = subAccountConfig.getOptionalString("accessKeyId");
453
- const accessKeySecret = subAccountConfig.getOptionalString("accessKeySecret");
454
- let stsParams = {};
455
- if (accessKeyId && accessKeySecret) {
456
- stsParams = {
457
- region: "us-east-1",
458
- credentials: {
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
- } else {
464
- stsParams = {
465
- region: "us-east-1"
466
- };
561
+ };
562
+ } else {
563
+ throw new Error("Both accessKeyId and accessKeySecret must be provided");
564
+ }
467
565
  }
468
- const client = new clientSts.STSClient(stsParams);
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: "AssumeRoleSession1"
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: "us-east-1",
584
+ region,
478
585
  credentials: {
479
- accessKeyId: (_a = assumeRoleResponse.Credentials) == null ? void 0 : _a.AccessKeyId,
480
- secretAccessKey: (_b = assumeRoleResponse.Credentials) == null ? void 0 : _b.SecretAccessKey,
481
- sessionToken: (_c = assumeRoleResponse.Credentials) == null ? void 0 : _c.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 results = [];
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
- results.push(tag);
608
+ tags.push(tag);
502
609
  }
503
610
  }
504
611
  nextPageToken = response.NextPageToken;
505
612
  } while (nextPageToken);
506
- results.sort();
507
- return results;
613
+ tags.sort((a, b) => a.localeCompare(b));
614
+ return tags;
508
615
  }
509
- async fetchTagKeys(_subAccountConfig, client, query) {
616
+ async fetchTagKeys(_integrationConfig, client, query) {
510
617
  const tagKeys = await this._fetchTags(client, query);
511
- return { tagKeys, provider: CLOUD_PROVIDER.AWS };
618
+ return { tagKeys, provider: this.provider };
512
619
  }
513
- async fetchTagValues(_subAccountConfig, client, query, tagKey) {
620
+ async fetchTagValues(_integrationConfig, client, query, tagKey) {
514
621
  const tagValues = await this._fetchTags(client, query, tagKey);
515
- return { tagValues, provider: CLOUD_PROVIDER.AWS };
622
+ return { tagValues, provider: this.provider };
516
623
  }
517
- async fetchCosts(_subAccountConfig, client, query) {
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
- tagsExpression = { Tags: { Key: tags[0].key, Values: [tags[0].value] } };
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
- tagList.push({ Tags: { Key: tag.key, Values: [tag.value] } });
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(subAccountConfig, query, costResponse) {
673
+ async transformCostsData(integrationConfig, query, costResponse) {
563
674
  const categoryMappingService = CategoryMappingService.getInstance();
564
- const tags = subAccountConfig.getOptionalStringArray("tags");
675
+ const tags = integrationConfig.getOptionalStringArray("tags");
565
676
  const tagKeyValues = {};
566
- tags == null ? void 0 : tags.forEach((tag) => {
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
- var _a;
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((_a2 = groupMetrics.UnblendedCost.Amount) != null ? _a2 : "0.0");
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
- if (row[1]) {
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: CLOUD_PROVIDER.AZURE };
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: CLOUD_PROVIDER.AZURE };
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
- filter = {
731
- tags: { name: tags[0].key, operator: "In", values: [tags[0].value] }
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
- tagList.push({ tags: { name: tag.key, operator: "In", values: [tag.value] } });
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 == null ? void 0 : tags.forEach((tag) => {
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 GCPClient extends InfraWalletClient {
930
+ class ConfluentClient extends InfraWalletClient {
815
931
  static create(config, database, cache, logger) {
816
- return new GCPClient(CLOUD_PROVIDER.GCP, config, database, cache, logger);
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 = ["Google Cloud"];
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
- async initCloudClient(subAccountConfig) {
829
- const keyFilePath = subAccountConfig.getString("keyFilePath");
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 fetchTagKeys(_subAccountConfig, _client, _query) {
839
- return { tagKeys: [], provider: CLOUD_PROVIDER.GCP };
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 fetchTagValues(_subAccountConfig, _client, _query, _tagKey) {
842
- return { tagValues: [], provider: CLOUD_PROVIDER.GCP };
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(subAccountConfig, client, query) {
845
- const projectId = subAccountConfig.getString("projectId");
846
- const datasetId = subAccountConfig.getString("datasetId");
847
- const tableId = subAccountConfig.getString("tableId");
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
- const periodFormat = query.granularity.toUpperCase() === "MONTHLY" ? "%Y-%m" : "%Y-%m-%d";
850
- const sql = `
851
- SELECT
852
- project.name AS project,
853
- service.description AS service,
854
- FORMAT_TIMESTAMP('${periodFormat}', usage_start_time) AS period,
855
- (SUM(CAST(cost AS NUMERIC)) + SUM(IFNULL((SELECT SUM(CAST(c.amount AS NUMERIC)) FROM UNNEST(credits) AS c), 0))) AS total_cost
856
- FROM
857
- \`${projectId}.${datasetId}.${tableId}\`
858
- WHERE
859
- project.name IS NOT NULL
860
- AND cost > 0
861
- AND usage_start_time >= TIMESTAMP_MILLIS(${query.startTime})
862
- AND usage_start_time <= TIMESTAMP_MILLIS(${query.endTime})
863
- GROUP BY
864
- project, service, period
865
- ORDER BY
866
- project, period, total_cost DESC`;
867
- const [job] = await client.createQueryJob({
868
- query: sql
869
- });
870
- const [rows] = await job.getQueryResults();
871
- return rows;
872
- } catch (err) {
873
- throw new Error(err.message);
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, _query, costResponse) {
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 == null ? void 0 : tags.forEach((tag) => {
1020
+ tags?.forEach((tag) => {
882
1021
  const [k, v] = tag.split(":");
883
1022
  tagKeyValues[k.trim()] = v.trim();
884
1023
  });
885
- const transformedData = lodash.reduce(
886
- costResponse,
887
- (acc, row) => {
888
- const period = row.period;
889
- const keyName = `${accountName}_${row.project}_${row.service}`;
890
- if (!acc[keyName]) {
891
- acc[keyName] = {
892
- id: keyName,
893
- account: `${this.provider}/${accountName}`,
894
- service: this.convertServiceName(row.service),
895
- category: categoryMappingService.getCategoryByServiceName(this.provider, row.service),
896
- provider: this.provider,
897
- reports: {},
898
- ...{ project: row.project },
899
- // TODO: how should we handle the project field? for now, we add project name as a field in the report
900
- ...tagKeyValues
901
- // note that if there is a tag `project:foo` in config, it overrides the project field set above
902
- };
903
- }
904
- acc[keyName].reports[period] = parseFloat(row.total_cost);
905
- return acc;
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 MetricProvider {
914
- constructor(providerName, config, database, cache, logger) {
915
- this.providerName = providerName;
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 getMetrics(query) {
922
- const conf = this.config.getOptionalConfigArray(
923
- `backend.infraWallet.metricProviders.${this.providerName.toLowerCase()}`
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
- if (!conf) {
926
- return { metrics: [], errors: [] };
927
- }
928
- const promises = [];
929
- const results = [];
930
- const errors = [];
931
- for (const c of conf) {
932
- const configName = c.getString("name");
933
- const client = await this.initProviderClient(c);
934
- const dbClient = await this.database.getClient();
935
- const metricSettings = await dbClient.where({
936
- "wallets.name": query.walletName,
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
- const fullQuery = {
944
- name: metric.metric_name,
945
- query: metric.query,
946
- ...query
947
- };
948
- const cachedMetrics = await getMetricsFromCache(this.cache, this.providerName, configName, fullQuery);
949
- if (cachedMetrics) {
950
- this.logger.debug(`${this.providerName}/${configName}/${fullQuery.name} metrics from cache`);
951
- cachedMetrics.map((m) => {
952
- results.push({
953
- group: metric.group,
954
- // add group info to the metric
955
- ...m
956
- });
957
- });
958
- return;
959
- }
960
- const metricResponse = await this.fetchMetrics(c, client, fullQuery);
961
- const transformedMetrics = await this.transformMetricData(c, fullQuery, metricResponse);
962
- await setMetricsToCache(
963
- this.cache,
964
- transformedMetrics,
965
- this.providerName,
966
- configName,
967
- fullQuery,
968
- 60 * 60 * 2 * 1e3
969
- );
970
- transformedMetrics.map((value) => {
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
- promises.push(promise);
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
- metrics: results,
1172
+ reports: results,
992
1173
  errors
993
1174
  };
994
1175
  }
995
1176
  }
996
1177
 
997
- class DatadogProvider extends MetricProvider {
1178
+ class DatadogClient extends InfraWalletClient {
998
1179
  static create(config, database, cache, logger) {
999
- return new DatadogProvider("Datadog", config, database, cache, logger);
1180
+ return new DatadogClient(CLOUD_PROVIDER.DATADOG, config, database, cache, logger);
1000
1181
  }
1001
- async initProviderClient(config) {
1002
- const apiKey = config.getString("apiKey");
1003
- const applicationKey = config.getString("applicationKey");
1004
- const ddSite = config.getString("ddSite");
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.v1.MetricsApi(configuration);
1249
+ const client = new datadogApiClient.v2.UsageMeteringApi(configuration);
1013
1250
  return client;
1014
1251
  }
1015
- async fetchMetrics(_metricProviderConfig, client, query) {
1016
- var _a;
1017
- const params = {
1018
- from: parseInt(query.startTime, 10) / 1e3,
1019
- to: parseInt(query.endTime, 10) / 1e3,
1020
- query: (_a = query.query) == null ? void 0 : _a.replaceAll("IW_INTERVAL", query.granularity === "daily" ? "86400" : "2592000")
1021
- };
1022
- return client.queryMetrics(params).then((data) => {
1023
- if (data.status === "ok") {
1024
- return data;
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
- throw new Error(data.error);
1027
- });
1028
- }
1029
- async transformMetricData(_metricProviderConfig, query, metricResponse) {
1030
- const transformedData = [];
1031
- for (const series of metricResponse.series) {
1032
- const metricName = query.name;
1033
- const tagSet = series.tagSet;
1034
- const metric = {
1035
- id: `${metricName} ${tagSet.length === 0 ? "" : tagSet}`,
1036
- provider: this.providerName,
1037
- name: metricName,
1038
- reports: {}
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
- return transformedData;
1048
- }
1049
- }
1050
-
1051
- class GrafanaCloudProvider extends MetricProvider {
1052
- static create(config, database, cache, logger) {
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
- from: query.startTime,
1078
- to: query.endTime
1079
- };
1080
- const response = await fetch__default.default(`${url}/api/ds/query`, {
1081
- method: "post",
1082
- body: JSON.stringify(payload),
1083
- headers
1084
- });
1085
- const data = await response.json();
1086
- return data;
1087
- }
1088
- async transformMetricData(_metricProviderConfig, query, metricResponse) {
1089
- const transformedData = [];
1090
- const metricName = query.name;
1091
- const metric = {
1092
- id: metricName,
1093
- provider: this.providerName,
1094
- name: metricName,
1095
- reports: {}
1096
- };
1097
- const periods = metricResponse.results.A.frames[0].data.values[0];
1098
- const values = metricResponse.results.A.frames[0].data.values[1];
1099
- for (let i = 0; i < periods.length; i++) {
1100
- const period = moment__default.default(periods[i]).format(query.granularity === "daily" ? "YYYY-MM-DD" : "YYYY-MM");
1101
- const value = values[i];
1102
- metric.reports[period] = value;
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
- transformedData.push(metric);
1105
- return transformedData;
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 MockProvider extends MetricProvider {
1370
+ class GCPClient extends InfraWalletClient {
1110
1371
  static create(config, database, cache, logger) {
1111
- return new MockProvider("Mock", config, database, cache, logger);
1372
+ return new GCPClient(CLOUD_PROVIDER.GCP, config, database, cache, logger);
1112
1373
  }
1113
- async initProviderClient(_config) {
1114
- return null;
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 fetchMetrics(_metricProviderConfig, _client, _query) {
1117
- return null;
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 transformMetricData(_metricProviderConfig, query, _metricResponse) {
1120
- var _a, _b;
1121
- const transformedData = [];
1122
- const metricName = query.name;
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
- mockSettings = JSON.parse(query.query);
1126
- } catch (e) {
1127
- }
1128
- const minValue = (_a = mockSettings.min) != null ? _a : 0;
1129
- const maxValue = (_b = mockSettings.max) != null ? _b : 1e3;
1130
- const metric = {
1131
- id: metricName,
1132
- provider: this.providerName,
1133
- name: metricName,
1134
- reports: {}
1135
- };
1136
- let cursor = moment__default.default(parseInt(query.startTime, 10));
1137
- while (cursor <= moment__default.default(parseInt(query.endTime, 10))) {
1138
- const period = cursor.format(query.granularity === "daily" ? "YYYY-MM-DD" : "YYYY-MM");
1139
- metric.reports[period] = Math.floor(Math.random() * (maxValue - minValue) + minValue);
1140
- cursor = cursor.add(1, query.granularity === "daily" ? "days" : "months");
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
- transformedData.push(metric);
1143
- return transformedData;
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() === "monthly") {
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 == null ? void 0 : tags.forEach((tag) => {
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
- var __defProp = Object.defineProperty;
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 (!_CategoryMappingService.instance) {
1572
- _CategoryMappingService.instance = new _CategoryMappingService(cache, logger);
1976
+ if (!CategoryMappingService.instance) {
1977
+ CategoryMappingService.instance = new CategoryMappingService(cache, logger);
1573
1978
  }
1574
1979
  }
1575
1980
  static getInstance() {
1576
- if (!_CategoryMappingService.instance) {
1981
+ if (!CategoryMappingService.instance) {
1577
1982
  throw new Error("CategoryMappingService needs to be initialized first");
1578
1983
  }
1579
- return _CategoryMappingService.instance;
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 (!((_a = database.migrations) == null ? void 0 : _a.skip)) {
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/metrics_setting", async (request, response) => {
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/metric_configs", async (_request, response) => {
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/metrics_setting", async (request, response) => {
1900
- var _a;
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/metrics_setting", async (request, response) => {
1910
- var _a;
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;