@electrolux-oss/plugin-infrawallet-backend 0.1.2 → 0.1.4

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
@@ -11,7 +11,9 @@ var clientSts = require('@aws-sdk/client-sts');
11
11
  var lodash = require('lodash');
12
12
  var moment = require('moment');
13
13
  var armCostmanagement = require('@azure/arm-costmanagement');
14
+ var coreRestPipeline = require('@azure/core-rest-pipeline');
14
15
  var identity = require('@azure/identity');
16
+ var bigquery = require('@google-cloud/bigquery');
15
17
 
16
18
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
17
19
 
@@ -22,50 +24,126 @@ var moment__default = /*#__PURE__*/_interopDefaultCompat(moment);
22
24
  async function getCategoryMappings(database, provider) {
23
25
  const result = {};
24
26
  const client = await database.getClient();
25
- const mappings = await client.where({ provider }).select().from("category_mappings");
26
- mappings.forEach((mapping) => {
27
+ const default_mappings = await client.where({ provider: provider.toLowerCase() }).select().from("category_mappings_default");
28
+ default_mappings.forEach((mapping) => {
27
29
  if (typeof mapping.cloud_service_names === "string") {
28
- result[mapping.category] = JSON.parse(mapping.cloud_service_names);
30
+ JSON.parse(mapping.cloud_service_names).forEach((service) => {
31
+ result[service] = mapping.category;
32
+ });
33
+ } else {
34
+ mapping.cloud_service_names.forEach((service) => {
35
+ result[service] = mapping.category;
36
+ });
37
+ }
38
+ });
39
+ const override_mappings = await client.where({ provider }).select().from("category_mappings_override");
40
+ override_mappings.forEach((mapping) => {
41
+ if (typeof mapping.cloud_service_names === "string") {
42
+ JSON.parse(mapping.cloud_service_names).forEach((service) => {
43
+ result[service] = mapping.category;
44
+ });
29
45
  } else {
30
- result[mapping.category] = mapping.cloud_service_names;
46
+ mapping.cloud_service_names.forEach((service) => {
47
+ result[service] = mapping.category;
48
+ });
31
49
  }
32
50
  });
33
51
  return result;
34
52
  }
35
53
  function getCategoryByServiceName(serviceName, categoryMappings) {
36
- for (const key of Object.keys(categoryMappings)) {
37
- const serviceNames = categoryMappings[key];
38
- if (serviceNames && serviceNames.includes(serviceName)) {
39
- return key;
40
- }
54
+ if (serviceName in categoryMappings) {
55
+ return categoryMappings[serviceName];
41
56
  }
42
57
  return "Uncategorized";
43
58
  }
59
+ async function getReportsFromCache(cache, provider, configKey, query) {
60
+ const cacheKey = [
61
+ provider,
62
+ configKey,
63
+ query.filters,
64
+ query.groups,
65
+ query.granularity,
66
+ query.startTime,
67
+ query.endTime
68
+ ].join("_");
69
+ const cachedCosts = await cache.get(cacheKey);
70
+ return cachedCosts;
71
+ }
72
+ async function setReportsToCache(cache, reports, provider, configKey, query, ttl) {
73
+ const cacheKey = [
74
+ provider,
75
+ configKey,
76
+ query.filters,
77
+ query.groups,
78
+ query.granularity,
79
+ query.startTime,
80
+ query.endTime
81
+ ].join("_");
82
+ await cache.set(cacheKey, reports, {
83
+ ttl: ttl
84
+ });
85
+ }
44
86
 
45
87
  class AwsClient {
46
- constructor(config, database) {
88
+ constructor(providerName, config, database, cache, logger) {
89
+ this.providerName = providerName;
47
90
  this.config = config;
48
91
  this.database = database;
92
+ this.cache = cache;
93
+ this.logger = logger;
49
94
  }
50
- static create(config, database) {
51
- return new AwsClient(config, database);
95
+ static create(config, database, cache, logger) {
96
+ return new AwsClient("AWS", config, database, cache, logger);
97
+ }
98
+ convertServiceName(serviceName) {
99
+ let convertedName = serviceName;
100
+ const prefixes = ["Amazon", "AWS"];
101
+ const aliases = /* @__PURE__ */ new Map([
102
+ ["Elastic Compute Cloud - Compute", "EC2 - Instances"],
103
+ ["Virtual Private Cloud", "VPC (Virtual Private Cloud)"],
104
+ ["Relational Database Service", "RDS (Relational Database Service)"],
105
+ ["Simple Storage Service", "S3 (Simple Storage Service)"],
106
+ ["Managed Streaming for Apache Kafka", "MSK (Managed Streaming for Apache Kafka)"],
107
+ ["Elastic Container Service for Kubernetes", "EKS (Elastic Container Service for Kubernetes)"],
108
+ ["Elastic Container Service", "ECS (Elastic Container Service)"],
109
+ ["EC2 Container Registry (ECR)", "ECR (Elastic Container Registry)"],
110
+ ["Simple Queue Service", "SQS (Simple Queue Service)"],
111
+ ["Simple Notification Service", "SNS (Simple Notification Service)"],
112
+ ["Database Migration Service", "DMS (Database Migration Service)"]
113
+ ]);
114
+ for (const prefix of prefixes) {
115
+ if (serviceName.startsWith(prefix)) {
116
+ convertedName = serviceName.slice(prefix.length).trim();
117
+ }
118
+ }
119
+ if (aliases.has(convertedName)) {
120
+ convertedName = aliases.get(convertedName) || convertedName;
121
+ }
122
+ return `${this.providerName}/${convertedName}`;
52
123
  }
53
124
  async fetchCostsFromCloud(query) {
54
- const conf = this.config.getOptionalConfigArray(
55
- "backend.infraWallet.integrations.aws"
56
- );
125
+ const conf = this.config.getOptionalConfigArray("backend.infraWallet.integrations.aws");
57
126
  if (!conf) {
58
- return [];
127
+ return { reports: [], errors: [] };
59
128
  }
60
129
  const promises = [];
61
130
  const results = [];
131
+ const errors = [];
62
132
  query.groups.split(",").forEach((group) => {
63
133
  if (group.includes(":")) {
64
134
  group.split(":");
65
135
  }
66
136
  });
67
137
  for (const c of conf) {
68
- const name = c.getString("name");
138
+ const accountName = c.getString("name");
139
+ const cachedCosts = await getReportsFromCache(this.cache, this.providerName, accountName, query);
140
+ if (cachedCosts) {
141
+ this.logger.debug(`${this.providerName}/${accountName} costs from cache`);
142
+ cachedCosts.map((cost) => {
143
+ results.push(cost);
144
+ });
145
+ continue;
146
+ }
69
147
  const accountId = c.getString("accountId");
70
148
  const assumedRoleName = c.getString("assumedRoleName");
71
149
  const accessKeyId = c.getOptionalString("accessKeyId");
@@ -76,7 +154,7 @@ class AwsClient {
76
154
  const [k, v] = tag.split(":");
77
155
  tagKeyValues[k.trim()] = v.trim();
78
156
  });
79
- const categoryMappings = await getCategoryMappings(this.database, "aws");
157
+ const categoryMappings = await getCategoryMappings(this.database, this.providerName);
80
158
  let stsParams = {};
81
159
  if (accessKeyId && accessKeySecret) {
82
160
  stsParams = {
@@ -93,92 +171,172 @@ class AwsClient {
93
171
  }
94
172
  const promise = (async () => {
95
173
  var _a, _b, _c;
96
- const client = new clientSts.STSClient(stsParams);
97
- const commandInput = {
98
- // AssumeRoleRequest
99
- RoleArn: `arn:aws:iam::${accountId}:role/${assumedRoleName}`,
100
- RoleSessionName: "AssumeRoleSession1"
101
- };
102
- const assumeRoleCommand = new clientSts.AssumeRoleCommand(commandInput);
103
- const assumeRoleResponse = await client.send(assumeRoleCommand);
104
- const awsCeClient = new clientCostExplorer.CostExplorerClient({
105
- region: "us-east-1",
106
- credentials: {
107
- accessKeyId: (_a = assumeRoleResponse.Credentials) == null ? void 0 : _a.AccessKeyId,
108
- secretAccessKey: (_b = assumeRoleResponse.Credentials) == null ? void 0 : _b.SecretAccessKey,
109
- sessionToken: (_c = assumeRoleResponse.Credentials) == null ? void 0 : _c.SessionToken
110
- }
111
- });
112
- const input = {
113
- TimePeriod: {
114
- Start: moment__default.default(parseInt(query.startTime, 10)).format("YYYY-MM-DD"),
115
- End: moment__default.default(parseInt(query.endTime, 10)).format("YYYY-MM-DD")
116
- },
117
- Granularity: query.granularity.toUpperCase(),
118
- Filter: { Dimensions: { Key: "RECORD_TYPE", Values: ["Usage"] } },
119
- GroupBy: [{ Type: "DIMENSION", Key: "SERVICE" }],
120
- Metrics: ["UnblendedCost"]
121
- };
122
- const getCostCommand = new clientCostExplorer.GetCostAndUsageCommand(input);
123
- const costAndusageResponse = await awsCeClient.send(getCostCommand);
124
- const transformedData = lodash.reduce(
125
- costAndusageResponse.ResultsByTime,
126
- (acc, row) => {
127
- var _a2;
128
- const rowTime = (_a2 = row.TimePeriod) == null ? void 0 : _a2.Start;
129
- const period = rowTime ? rowTime.substring(0, 7) : "unknown";
130
- if (row.Groups) {
131
- row.Groups.forEach((group) => {
132
- var _a3;
133
- const groupKeys = group.Keys ? group.Keys[0] : "";
134
- const keyName = `${name}_${groupKeys}`;
135
- if (!acc[keyName]) {
136
- acc[keyName] = {
137
- id: keyName,
138
- name,
139
- service: `${groupKeys} (AWS)`,
140
- category: getCategoryByServiceName(
141
- groupKeys,
142
- categoryMappings
143
- ),
144
- provider: "AWS",
145
- reports: {},
146
- ...tagKeyValues
147
- };
148
- }
149
- const groupMetrics = group.Metrics;
150
- if (groupMetrics !== void 0) {
151
- acc[keyName].reports[period] = parseFloat(
152
- (_a3 = groupMetrics.UnblendedCost.Amount) != null ? _a3 : "0.0"
153
- );
154
- }
155
- });
174
+ try {
175
+ const client = new clientSts.STSClient(stsParams);
176
+ const commandInput = {
177
+ // AssumeRoleRequest
178
+ RoleArn: `arn:aws:iam::${accountId}:role/${assumedRoleName}`,
179
+ RoleSessionName: "AssumeRoleSession1"
180
+ };
181
+ const assumeRoleCommand = new clientSts.AssumeRoleCommand(commandInput);
182
+ const assumeRoleResponse = await client.send(assumeRoleCommand);
183
+ const awsCeClient = new clientCostExplorer.CostExplorerClient({
184
+ region: "us-east-1",
185
+ credentials: {
186
+ accessKeyId: (_a = assumeRoleResponse.Credentials) == null ? void 0 : _a.AccessKeyId,
187
+ secretAccessKey: (_b = assumeRoleResponse.Credentials) == null ? void 0 : _b.SecretAccessKey,
188
+ sessionToken: (_c = assumeRoleResponse.Credentials) == null ? void 0 : _c.SessionToken
156
189
  }
157
- return acc;
158
- },
159
- {}
160
- );
161
- Object.values(transformedData).map((value) => {
162
- results.push(value);
163
- });
190
+ });
191
+ let costAndUsageResults = [];
192
+ let nextPageToken = void 0;
193
+ do {
194
+ const input = {
195
+ TimePeriod: {
196
+ Start: moment__default.default(parseInt(query.startTime, 10)).format("YYYY-MM-DD"),
197
+ End: moment__default.default(parseInt(query.endTime, 10)).format("YYYY-MM-DD")
198
+ },
199
+ Granularity: query.granularity.toUpperCase(),
200
+ Filter: { Dimensions: { Key: "RECORD_TYPE", Values: ["Usage"] } },
201
+ GroupBy: [{ Type: "DIMENSION", Key: "SERVICE" }],
202
+ Metrics: ["UnblendedCost"],
203
+ NextPageToken: nextPageToken
204
+ };
205
+ const getCostCommand = new clientCostExplorer.GetCostAndUsageCommand(input);
206
+ const costAndUsageResponse = await awsCeClient.send(getCostCommand);
207
+ costAndUsageResults = costAndUsageResults.concat(costAndUsageResponse.ResultsByTime);
208
+ nextPageToken = costAndUsageResponse.NextPageToken;
209
+ } while (nextPageToken);
210
+ const transformedData = lodash.reduce(
211
+ costAndUsageResults,
212
+ (accumulator, row) => {
213
+ var _a2;
214
+ const rowTime = (_a2 = row.TimePeriod) == null ? void 0 : _a2.Start;
215
+ let period = "unknown";
216
+ if (rowTime) {
217
+ if (query.granularity.toUpperCase() === "MONTHLY") {
218
+ period = rowTime.substring(0, 7);
219
+ } else {
220
+ period = rowTime;
221
+ }
222
+ }
223
+ if (row.Groups) {
224
+ row.Groups.forEach((group) => {
225
+ var _a3;
226
+ const serviceName = group.Keys ? group.Keys[0] : "";
227
+ const keyName = `${accountName}_${serviceName}`;
228
+ if (!accumulator[keyName]) {
229
+ accumulator[keyName] = {
230
+ id: keyName,
231
+ name: `${this.providerName}/${accountName}`,
232
+ service: this.convertServiceName(serviceName),
233
+ category: getCategoryByServiceName(serviceName, categoryMappings),
234
+ provider: this.providerName,
235
+ reports: {},
236
+ ...tagKeyValues
237
+ };
238
+ }
239
+ const groupMetrics = group.Metrics;
240
+ if (groupMetrics !== void 0) {
241
+ accumulator[keyName].reports[period] = parseFloat((_a3 = groupMetrics.UnblendedCost.Amount) != null ? _a3 : "0.0");
242
+ }
243
+ });
244
+ }
245
+ return accumulator;
246
+ },
247
+ {}
248
+ );
249
+ await setReportsToCache(
250
+ this.cache,
251
+ Object.values(transformedData),
252
+ this.providerName,
253
+ accountName,
254
+ query,
255
+ 60 * 60 * 2 * 1e3
256
+ );
257
+ Object.values(transformedData).map((value) => {
258
+ results.push(value);
259
+ });
260
+ } catch (e) {
261
+ this.logger.error(e);
262
+ errors.push({
263
+ provider: this.providerName,
264
+ name: `${this.providerName}/${accountName}`,
265
+ error: e.message
266
+ });
267
+ }
164
268
  })();
165
269
  promises.push(promise);
166
270
  }
167
271
  await Promise.all(promises);
168
- return results;
272
+ return {
273
+ reports: results,
274
+ errors
275
+ };
169
276
  }
170
277
  }
171
278
 
172
279
  class AzureClient {
173
- constructor(config, database) {
280
+ constructor(providerName, config, database, cache, logger) {
281
+ this.providerName = providerName;
174
282
  this.config = config;
175
283
  this.database = database;
284
+ this.cache = cache;
285
+ this.logger = logger;
286
+ }
287
+ static create(config, database, cache, logger) {
288
+ return new AzureClient("Azure", config, database, cache, logger);
289
+ }
290
+ convertServiceName(serviceName) {
291
+ let convertedName = serviceName;
292
+ const prefixes = ["Azure"];
293
+ for (const prefix of prefixes) {
294
+ if (serviceName.startsWith(prefix)) {
295
+ convertedName = serviceName.slice(prefix.length).trim();
296
+ }
297
+ }
298
+ return `${this.providerName}/${convertedName}`;
299
+ }
300
+ formatDate(dateNumber) {
301
+ const dateString = dateNumber.toString();
302
+ if (dateString.length !== 8) {
303
+ return null;
304
+ }
305
+ const year = dateString.slice(0, 4);
306
+ const month = dateString.slice(4, 6);
307
+ const day = dateString.slice(6);
308
+ return `${year}-${month}-${day}`;
176
309
  }
177
- static create(config, database) {
178
- return new AzureClient(config, database);
310
+ async fetchDataWithRetry(client, url, body, maxRetries = 5) {
311
+ let retries = 0;
312
+ while (retries < maxRetries) {
313
+ const request = coreRestPipeline.createPipelineRequest({
314
+ url,
315
+ method: "POST",
316
+ body: JSON.stringify(body),
317
+ headers: coreRestPipeline.createHttpHeaders({
318
+ "Content-Type": "application/json"
319
+ })
320
+ });
321
+ const response = await client.pipeline.sendRequest(client, request);
322
+ if (response.status === 200) {
323
+ return JSON.parse(response.bodyAsText || "{}");
324
+ } else if (response.status === 429) {
325
+ const retryAfter = parseInt(
326
+ response.headers.get("x-ms-ratelimit-microsoft.costmanagement-entity-retry-after") || "60",
327
+ 10
328
+ );
329
+ this.logger.warn(`Hit Azure rate limit, retrying after ${retryAfter} seconds...`);
330
+ await new Promise((resolve) => setTimeout(resolve, retryAfter * 1e3));
331
+ retries++;
332
+ } else {
333
+ throw new Error(response.bodyAsText);
334
+ }
335
+ }
336
+ throw new Error("Max retries exceeded");
179
337
  }
180
338
  async queryAzureCostExplorer(azureClient, subscription, granularity, groups, startDate, endDate) {
181
- const scope = `/subscriptions/${subscription}`;
339
+ const url = `https://management.azure.com/subscriptions/${subscription}/providers/Microsoft.CostManagement/query?api-version=2022-10-01`;
182
340
  const query = {
183
341
  type: "ActualCost",
184
342
  dataset: {
@@ -188,34 +346,43 @@ class AzureClient {
188
346
  },
189
347
  timeframe: "Custom",
190
348
  timePeriod: {
191
- from: startDate.toISOString(),
192
- to: endDate.toISOString()
349
+ from: startDate.toDate(),
350
+ to: endDate.toDate()
193
351
  }
194
352
  };
195
- const result = await azureClient.query.usage(scope, query);
196
- return result;
353
+ let result = await this.fetchDataWithRetry(azureClient, url, query);
354
+ let allResults = result.properties.rows;
355
+ while (result.properties.nextLink) {
356
+ result = await this.fetchDataWithRetry(azureClient, result.properties.nextLink, query);
357
+ allResults = allResults.concat(result.properties.rows);
358
+ }
359
+ return allResults;
197
360
  }
198
361
  async fetchCostsFromCloud(query) {
199
- const conf = this.config.getOptionalConfigArray(
200
- "backend.infraWallet.integrations.azure"
201
- );
362
+ const conf = this.config.getOptionalConfigArray("backend.infraWallet.integrations.azure");
202
363
  if (!conf) {
203
- return [];
364
+ return { reports: [], errors: [] };
204
365
  }
366
+ const categoryMappings = await getCategoryMappings(this.database, this.providerName);
205
367
  const promises = [];
206
368
  const results = [];
369
+ const errors = [];
207
370
  const groupPairs = [{ type: "Dimension", name: "ServiceName" }];
208
371
  for (const c of conf) {
209
- const name = c.getString("name");
372
+ const accountName = c.getString("name");
373
+ const cachedCosts = await getReportsFromCache(this.cache, this.providerName, accountName, query);
374
+ if (cachedCosts) {
375
+ this.logger.debug(`${this.providerName}/${accountName} costs from cache`);
376
+ cachedCosts.map((cost) => {
377
+ results.push(cost);
378
+ });
379
+ continue;
380
+ }
210
381
  const subscriptionId = c.getString("subscriptionId");
211
382
  const tenantId = c.getString("tenantId");
212
383
  const clientId = c.getString("clientId");
213
384
  const clientSecret = c.getString("clientSecret");
214
- const credential = new identity.ClientSecretCredential(
215
- tenantId,
216
- clientId,
217
- clientSecret
218
- );
385
+ const credential = new identity.ClientSecretCredential(tenantId, clientId, clientSecret);
219
386
  const client = new armCostmanagement.CostManagementClient(credential);
220
387
  const tags = c.getOptionalStringArray("tags");
221
388
  const tagKeyValues = {};
@@ -223,10 +390,6 @@ class AzureClient {
223
390
  const [k, v] = tag.split(":");
224
391
  tagKeyValues[k.trim()] = v.trim();
225
392
  });
226
- const categoryMappings = await getCategoryMappings(
227
- this.database,
228
- "azure"
229
- );
230
393
  const promise = (async () => {
231
394
  try {
232
395
  const costResponse = await this.queryAzureCostExplorer(
@@ -238,77 +401,234 @@ class AzureClient {
238
401
  moment__default.default(parseInt(query.endTime, 10))
239
402
  );
240
403
  const transformedData = lodash.reduce(
241
- costResponse.rows,
242
- (acc, row) => {
243
- let keyName = name;
404
+ costResponse,
405
+ (accumulator, row) => {
406
+ const cost = row[0];
407
+ let date = row[1];
408
+ const serviceName = row[2];
409
+ if (query.granularity.toUpperCase() === "DAILY") {
410
+ date = this.formatDate(date);
411
+ }
412
+ let keyName = accountName;
244
413
  for (let i = 0; i < groupPairs.length; i++) {
245
414
  keyName += `->${row[i + 2]}`;
246
415
  }
416
+ if (!accumulator[keyName]) {
417
+ accumulator[keyName] = {
418
+ id: keyName,
419
+ name: `${this.providerName}/${accountName}`,
420
+ service: this.convertServiceName(serviceName),
421
+ category: getCategoryByServiceName(serviceName, categoryMappings),
422
+ provider: this.providerName,
423
+ reports: {},
424
+ ...tagKeyValues
425
+ };
426
+ }
427
+ if (!moment__default.default(date).isBefore(moment__default.default(parseInt(query.startTime, 10)))) {
428
+ if (query.granularity.toUpperCase() === "MONTHLY") {
429
+ const yearMonth = date.substring(0, 7);
430
+ accumulator[keyName].reports[yearMonth] = parseFloat(cost);
431
+ } else {
432
+ accumulator[keyName].reports[date] = parseFloat(cost);
433
+ }
434
+ }
435
+ return accumulator;
436
+ },
437
+ {}
438
+ );
439
+ await setReportsToCache(
440
+ this.cache,
441
+ Object.values(transformedData),
442
+ this.providerName,
443
+ accountName,
444
+ query,
445
+ 60 * 60 * 2 * 1e3
446
+ );
447
+ Object.values(transformedData).map((value) => {
448
+ results.push(value);
449
+ });
450
+ } catch (e) {
451
+ this.logger.error(e);
452
+ errors.push({
453
+ provider: this.providerName,
454
+ name: `${this.providerName}/${accountName}`,
455
+ error: e.message
456
+ });
457
+ }
458
+ })();
459
+ promises.push(promise);
460
+ }
461
+ await Promise.all(promises);
462
+ return {
463
+ reports: results,
464
+ errors
465
+ };
466
+ }
467
+ }
468
+
469
+ class GCPClient {
470
+ constructor(providerName, config, database, cache, logger) {
471
+ this.providerName = providerName;
472
+ this.config = config;
473
+ this.database = database;
474
+ this.cache = cache;
475
+ this.logger = logger;
476
+ }
477
+ static create(config, database, cache, logger) {
478
+ return new GCPClient("GCP", config, database, cache, logger);
479
+ }
480
+ convertServiceName(serviceName) {
481
+ let convertedName = serviceName;
482
+ const prefixes = ["Google Cloud"];
483
+ for (const prefix of prefixes) {
484
+ if (serviceName.startsWith(prefix)) {
485
+ convertedName = serviceName.slice(prefix.length).trim();
486
+ }
487
+ }
488
+ return `${this.providerName}/${convertedName}`;
489
+ }
490
+ async queryBigQuery(keyFilePath, projectId, datasetId, tableId, query) {
491
+ const options = {
492
+ keyFilename: keyFilePath,
493
+ projectId
494
+ };
495
+ const bigquery$1 = new bigquery.BigQuery(options);
496
+ try {
497
+ const periodFormat = query.granularity.toUpperCase() === "MONTHLY" ? "%Y-%m" : "%Y-%m-%d";
498
+ const sql = `
499
+ SELECT
500
+ project.name AS project,
501
+ service.description AS service,
502
+ FORMAT_TIMESTAMP('${periodFormat}', usage_start_time) AS period,
503
+ SUM(cost) AS total_cost
504
+ FROM
505
+ \`${projectId}.${datasetId}.${tableId}\`
506
+ WHERE
507
+ project.name IS NOT NULL
508
+ AND cost > 0
509
+ AND usage_start_time >= TIMESTAMP_MILLIS(${query.startTime})
510
+ AND usage_start_time <= TIMESTAMP_MILLIS(${query.endTime})
511
+ GROUP BY
512
+ project, service, period
513
+ ORDER BY
514
+ project, period, total_cost DESC`;
515
+ const [job] = await bigquery$1.createQueryJob({
516
+ query: sql,
517
+ location: "US"
518
+ });
519
+ const [rows] = await job.getQueryResults();
520
+ return rows;
521
+ } catch (err) {
522
+ throw new Error(err.message);
523
+ }
524
+ }
525
+ async fetchCostsFromCloud(query) {
526
+ const conf = this.config.getOptionalConfigArray("backend.infraWallet.integrations.gcp");
527
+ if (!conf) {
528
+ return { reports: [], errors: [] };
529
+ }
530
+ const promises = [];
531
+ const results = [];
532
+ const errors = [];
533
+ for (const c of conf) {
534
+ const accountName = c.getString("name");
535
+ const cachedCosts = await getReportsFromCache(this.cache, this.providerName, accountName, query);
536
+ if (cachedCosts) {
537
+ this.logger.debug(`${this.providerName}/${accountName} costs from cache`);
538
+ cachedCosts.map((cost) => {
539
+ results.push(cost);
540
+ });
541
+ continue;
542
+ }
543
+ const keyFilePath = c.getString("keyFilePath");
544
+ const projectId = c.getString("projectId");
545
+ const datasetId = c.getString("datasetId");
546
+ const tableId = c.getString("tableId");
547
+ const tags = c.getOptionalStringArray("tags");
548
+ const tagKeyValues = {};
549
+ tags == null ? void 0 : tags.forEach((tag) => {
550
+ const [k, v] = tag.split(":");
551
+ tagKeyValues[k.trim()] = v.trim();
552
+ });
553
+ const categoryMappings = await getCategoryMappings(this.database, this.providerName);
554
+ const promise = (async () => {
555
+ try {
556
+ const costResponse = await this.queryBigQuery(keyFilePath, projectId, datasetId, tableId, query);
557
+ const transformedData = lodash.reduce(
558
+ costResponse,
559
+ (acc, row) => {
560
+ const period = row.period;
561
+ const keyName = `${accountName}_${row.project}_${row.service}`;
247
562
  if (!acc[keyName]) {
248
563
  acc[keyName] = {
249
564
  id: keyName,
250
- name,
251
- service: `${row[2]} (Azure)`,
252
- category: getCategoryByServiceName(row[2], categoryMappings),
253
- provider: "Azure",
565
+ name: `${this.providerName}/${accountName}`,
566
+ service: this.convertServiceName(row.service),
567
+ category: getCategoryByServiceName(row.service, categoryMappings),
568
+ provider: this.providerName,
254
569
  reports: {},
570
+ ...{ project: row.project },
571
+ // TODO: how should we handle the project field? for now, we add project name as a field in the report
255
572
  ...tagKeyValues
573
+ // note that if there is a tag `project:foo` in config, it overrides the project field set above
256
574
  };
257
575
  }
258
- if (!moment__default.default(row[1]).isBefore(moment__default.default(parseInt(query.startTime, 10)))) {
259
- acc[keyName].reports[row[1].substring(0, 7)] = parseFloat(
260
- row[0]
261
- );
262
- }
576
+ acc[keyName].reports[period] = row.total_cost;
263
577
  return acc;
264
578
  },
265
579
  {}
266
580
  );
581
+ await setReportsToCache(
582
+ this.cache,
583
+ Object.values(transformedData),
584
+ this.providerName,
585
+ accountName,
586
+ query,
587
+ 60 * 60 * 2 * 1e3
588
+ );
267
589
  Object.values(transformedData).map((value) => {
268
590
  results.push(value);
269
591
  });
270
592
  } catch (e) {
271
- throw new Error(e.message);
593
+ this.logger.error(e);
594
+ errors.push({
595
+ provider: this.providerName,
596
+ name: `${this.providerName}/${accountName}`,
597
+ error: e.message
598
+ });
272
599
  }
273
600
  })();
274
601
  promises.push(promise);
275
602
  }
276
603
  await Promise.all(promises);
277
- return results;
604
+ return {
605
+ reports: results,
606
+ errors
607
+ };
278
608
  }
279
609
  }
280
610
 
281
611
  async function setUpDatabase(database) {
282
612
  var _a;
283
613
  const client = await database.getClient();
284
- const migrationsDir = backendPluginApi.resolvePackagePath(
285
- "@electrolux-oss/plugin-infrawallet-backend",
286
- "migrations"
287
- );
614
+ const migrationsDir = backendPluginApi.resolvePackagePath("@electrolux-oss/plugin-infrawallet-backend", "migrations");
288
615
  if (!((_a = database.migrations) == null ? void 0 : _a.skip)) {
289
616
  await client.migrate.latest({
290
617
  directory: migrationsDir
291
618
  });
292
619
  }
293
- const category_mappings_count = await client("category_mappings").count(
294
- "id as c"
295
- );
296
- if (category_mappings_count[0].c === 0 || category_mappings_count[0].c === "0") {
297
- const seedsDir = backendPluginApi.resolvePackagePath(
298
- "@electrolux-oss/plugin-infrawallet-backend",
299
- "seeds"
300
- );
301
- await client.seed.run({ directory: seedsDir });
302
- }
620
+ const seedsDir = backendPluginApi.resolvePackagePath("@electrolux-oss/plugin-infrawallet-backend", "seeds");
621
+ await client.seed.run({ directory: seedsDir });
303
622
  }
304
623
  async function createRouter(options) {
305
624
  const { logger, config, cache, database } = options;
306
625
  await setUpDatabase(database);
307
626
  const router = Router__default.default();
308
627
  router.use(express__default.default.json());
309
- const azureClient = AzureClient.create(config, database);
310
- const awsClient = AwsClient.create(config, database);
311
- const cloudClients = [azureClient, awsClient];
628
+ const azureClient = AzureClient.create(config, database, cache, logger);
629
+ const awsClient = AwsClient.create(config, database, cache, logger);
630
+ const gcpClient = GCPClient.create(config, database, cache, logger);
631
+ const cloudClients = [azureClient, awsClient, gcpClient];
312
632
  router.get("/health", (_, response) => {
313
633
  logger.info("PONG!");
314
634
  response.json({ status: "ok" });
@@ -321,42 +641,40 @@ async function createRouter(options) {
321
641
  const endTime = request.query.endTime;
322
642
  const promises = [];
323
643
  const results = [];
644
+ const errors = [];
324
645
  cloudClients.forEach(async (client) => {
325
646
  const fetchCloudCosts = (async () => {
326
- const cacheKey = [
327
- client.constructor.name,
328
- filters,
329
- groups,
330
- granularity,
331
- startTime,
332
- endTime
333
- ].join("_");
334
- const cachedCosts = await cache.get(cacheKey);
335
- if (cachedCosts) {
336
- logger.debug(`${client.constructor.name} costs from cache`);
337
- cachedCosts.forEach((cost) => {
338
- results.push(cost);
339
- });
340
- } else {
341
- const costs = await client.fetchCostsFromCloud({
647
+ try {
648
+ const clientResponse = await client.fetchCostsFromCloud({
342
649
  filters,
343
650
  groups,
344
651
  granularity,
345
652
  startTime,
346
653
  endTime
347
654
  });
348
- await cache.set(cacheKey, costs, {
349
- ttl: 60 * 60 * 2 * 1e3
655
+ clientResponse.errors.forEach((e) => {
656
+ errors.push(e);
350
657
  });
351
- costs.forEach((cost) => {
658
+ clientResponse.reports.forEach((cost) => {
352
659
  results.push(cost);
353
660
  });
661
+ } catch (e) {
662
+ logger.error(e);
663
+ errors.push({
664
+ provider: client.constructor.name,
665
+ name: client.constructor.name,
666
+ error: e.message
667
+ });
354
668
  }
355
669
  })();
356
670
  promises.push(fetchCloudCosts);
357
671
  });
358
672
  await Promise.all(promises);
359
- response.json({ data: results, status: "ok" });
673
+ if (errors.length > 0) {
674
+ response.status(207).json({ data: results, errors, status: 207 });
675
+ } else {
676
+ response.json({ data: results, errors, status: 200 });
677
+ }
360
678
  });
361
679
  router.use(backendCommon.errorHandler());
362
680
  return router;