@letthem/backstage-plugin-aws-cost-insights-backend 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/README.md ADDED
@@ -0,0 +1,299 @@
1
+ # Cost Insights Backend Plugin
2
+
3
+ Backstage backend plugin that serves EC2 cost insights from S3 CUR-derived data.
4
+
5
+ ## Overview
6
+
7
+ This plugin:
8
+
9
+ - reads daily cost data files from S3
10
+ - aggregates costs by resource
11
+ - provides monthly rollups
12
+ - exposes REST endpoints consumed by the frontend plugin
13
+
14
+ ## Configuration
15
+
16
+ ```yaml
17
+ costInsights:
18
+ environments:
19
+ - dev
20
+ - stg
21
+ - prod
22
+ defaultEnvironment: prod
23
+ auth:
24
+ allowUnauthenticated: false
25
+ s3:
26
+ region: ap-northeast-2
27
+ bucket: your-cur-result-bucket
28
+ # Optional: local SSO profile
29
+ # profile: your-aws-profile
30
+ # Optional: prefix template
31
+ # Tokens: {environment}, {yearMonth}, {year}, {month}
32
+ dailyPrefixTemplate: '{environment}/daily/{yearMonth}/'
33
+ ```
34
+
35
+ ## Auth Behavior
36
+
37
+ - `auth.allowUnauthenticated: false` -> `user-cookie`
38
+ - `auth.allowUnauthenticated: true` -> `unauthenticated`
39
+
40
+ Default is `false`.
41
+
42
+ ## AWS Credentials
43
+
44
+ - If `s3.profile` is set, plugin uses that profile (local SSO use case).
45
+ - If omitted, plugin uses AWS default credential chain:
46
+ - IRSA (EKS)
47
+ - EC2 Instance Profile
48
+ - environment variables
49
+ - shared credentials/default profile
50
+
51
+ ## API Endpoints
52
+
53
+ - `GET /health`
54
+ - `GET /config`
55
+ - `GET /last-complete-date?environment=<env>`
56
+ - `GET /product/ec2/insights?intervals=<ISO_START>/<ISO_END>&environment=<env>`
57
+
58
+ ## Query Rules
59
+
60
+ - `environment` must exist in `costInsights.environments`.
61
+ - `intervals` must be valid ISO interval (`start/end`).
62
+ - `start <= end`.
63
+ - max interval span is 24 months.
64
+
65
+ ## Data Loading Notes
66
+
67
+ - The plugin resolves the latest `run=` prefix under month prefix.
68
+ - It reads all data files in the run prefix (not just one file).
69
+ - Non-numeric cost/usage rows are skipped to avoid `NaN` aggregation.
70
+
71
+ ## Input Data Contract
72
+
73
+ This plugin is data-source agnostic. It does not require Athena specifically.
74
+ Any upstream pipeline is valid as long as it writes data in the contract below.
75
+
76
+ ### Storage Layout
77
+
78
+ - Base location is defined by `costInsights.s3.bucket`.
79
+ - Prefix is resolved by `costInsights.s3.dailyPrefixTemplate`.
80
+ - Default template:
81
+ - `'{environment}/daily/{yearMonth}/'`
82
+ - Under each month prefix, plugin expects one or more `run=` folders:
83
+ - Example: `prd/daily/2026-01/run=2026-01-16T11-14-00/`
84
+
85
+ ### File Discovery Rules
86
+
87
+ - All files under selected `run=` prefix are scanned.
88
+ - Files ending with `-manifest.csv` are ignored.
89
+ - Files ending with `.metadata` are ignored.
90
+ - Directory markers (`.../`) are ignored.
91
+
92
+ ### File Format
93
+
94
+ - NDJSON (JSON Lines): one JSON object per line.
95
+ - UTF-8 text.
96
+
97
+ ### Required Fields (per line)
98
+
99
+ - `usage_date`: string, ISO date (for example `2026-01-15`)
100
+ - `resource_id`: string
101
+ - `resource_type`: string
102
+ - `total_cost`: number-like value
103
+ - `usage_amount`: number-like value
104
+
105
+ ### Optional Fields
106
+
107
+ - `product_instance_type`: string
108
+ - `product_volume_type`: string
109
+
110
+ ### Validation Behavior
111
+
112
+ - If `total_cost` or `usage_amount` is not numeric, that row is skipped.
113
+ - Resource costs are aggregated by `resource_id`.
114
+ - Daily points are sorted by `usage_date` before response.
115
+
116
+ ### Example NDJSON Line
117
+
118
+ ```json
119
+ {"usage_date":"2026-01-15","resource_id":"i-0123456789abcdef0","resource_type":"instance","product_instance_type":"m6i.large","product_volume_type":"","total_cost":12.34,"usage_amount":24}
120
+ ```
121
+
122
+ ## Appendix: Example Athena UNLOAD Query
123
+
124
+ Use this only as a reference implementation. Any pipeline is valid if it produces
125
+ data that matches the Input Data Contract above.
126
+
127
+ Replace placeholders before use:
128
+
129
+ - `<cur_database>`, `<cur_table>`
130
+ - `<year>`, `<month>`
131
+ - `<bucket>`, `<environment>`, `<year-month>`, `<run-timestamp>`
132
+
133
+ ```sql
134
+ UNLOAD (
135
+ SELECT
136
+ usage_date,
137
+ resource_id,
138
+ resource_type,
139
+ product_instance_type,
140
+ product_volume_type,
141
+ SUM(total_cost) AS total_cost,
142
+ SUM(usage_amount) AS usage_amount
143
+ FROM (
144
+ SELECT
145
+ DATE(line_item_usage_start_date) AS usage_date,
146
+ line_item_resource_id AS resource_id,
147
+ 'instance' AS resource_type,
148
+ product_instance_type AS product_instance_type,
149
+ '' AS product_volume_type,
150
+ SUM(line_item_unblended_cost) AS total_cost,
151
+ SUM(line_item_usage_amount) AS usage_amount
152
+ FROM <cur_database>.<cur_table>
153
+ WHERE
154
+ line_item_product_code = 'AmazonEC2'
155
+ AND product_product_family = 'Compute Instance'
156
+ AND line_item_line_item_type IN ('Usage', 'DiscountedUsage', 'SavingsPlanCoveredUsage')
157
+ AND year = '<year>'
158
+ AND month = '<month>'
159
+ AND line_item_resource_id IS NOT NULL
160
+ GROUP BY
161
+ DATE(line_item_usage_start_date),
162
+ line_item_resource_id,
163
+ product_instance_type
164
+
165
+ UNION ALL
166
+
167
+ SELECT
168
+ DATE(line_item_usage_start_date) AS usage_date,
169
+ line_item_resource_id AS resource_id,
170
+ CASE
171
+ WHEN product_product_family = 'Storage Snapshot' THEN 'snapshot'
172
+ ELSE 'volume'
173
+ END AS resource_type,
174
+ '' AS product_instance_type,
175
+ product_volume_type AS product_volume_type,
176
+ SUM(line_item_unblended_cost) AS total_cost,
177
+ SUM(line_item_usage_amount) AS usage_amount
178
+ FROM <cur_database>.<cur_table>
179
+ WHERE
180
+ line_item_product_code = 'AmazonEC2'
181
+ AND line_item_line_item_type = 'Usage'
182
+ AND year = '<year>'
183
+ AND month = '<month>'
184
+ AND product_product_family IN ('Storage', 'Storage Snapshot')
185
+ AND line_item_resource_id IS NOT NULL
186
+ GROUP BY
187
+ DATE(line_item_usage_start_date),
188
+ line_item_resource_id,
189
+ product_product_family,
190
+ product_volume_type
191
+
192
+ UNION ALL
193
+
194
+ SELECT
195
+ DATE(line_item_usage_start_date) AS usage_date,
196
+ COALESCE(line_item_resource_id, 'Unattached-IP') AS resource_id,
197
+ 'elastic-ip' AS resource_type,
198
+ '' AS product_instance_type,
199
+ line_item_usage_type AS product_volume_type,
200
+ SUM(line_item_unblended_cost) AS total_cost,
201
+ SUM(line_item_usage_amount) AS usage_amount
202
+ FROM <cur_database>.<cur_table>
203
+ WHERE
204
+ line_item_product_code = 'AmazonEC2'
205
+ AND line_item_line_item_type = 'Usage'
206
+ AND year = '<year>'
207
+ AND month = '<month>'
208
+ AND product_product_family = 'IP Address'
209
+ GROUP BY
210
+ DATE(line_item_usage_start_date),
211
+ line_item_resource_id,
212
+ line_item_usage_type
213
+
214
+ UNION ALL
215
+
216
+ SELECT
217
+ DATE(line_item_usage_start_date) AS usage_date,
218
+ line_item_resource_id AS resource_id,
219
+ 'nat-gateway' AS resource_type,
220
+ '' AS product_instance_type,
221
+ '' AS product_volume_type,
222
+ SUM(line_item_unblended_cost) AS total_cost,
223
+ SUM(line_item_usage_amount) AS usage_amount
224
+ FROM <cur_database>.<cur_table>
225
+ WHERE
226
+ line_item_product_code = 'AmazonEC2'
227
+ AND line_item_line_item_type = 'Usage'
228
+ AND year = '<year>'
229
+ AND month = '<month>'
230
+ AND (line_item_resource_id LIKE 'nat-%' OR product_product_family = 'NAT Gateway')
231
+ AND line_item_resource_id IS NOT NULL
232
+ GROUP BY
233
+ DATE(line_item_usage_start_date),
234
+ line_item_resource_id
235
+
236
+ UNION ALL
237
+
238
+ SELECT
239
+ DATE(line_item_usage_start_date) AS usage_date,
240
+ COALESCE(line_item_resource_id, 'Data-Transfer') AS resource_id,
241
+ 'data-transfer' AS resource_type,
242
+ '' AS product_instance_type,
243
+ CASE
244
+ WHEN COUNT(DISTINCT product_transfer_type) > 1 THEN 'Mixed'
245
+ ELSE MAX(product_transfer_type)
246
+ END AS product_volume_type,
247
+ SUM(line_item_unblended_cost) AS total_cost,
248
+ SUM(line_item_usage_amount) AS usage_amount
249
+ FROM <cur_database>.<cur_table>
250
+ WHERE
251
+ line_item_product_code = 'AmazonEC2'
252
+ AND line_item_line_item_type = 'Usage'
253
+ AND year = '<year>'
254
+ AND month = '<month>'
255
+ AND product_product_family = 'Data Transfer'
256
+ GROUP BY
257
+ DATE(line_item_usage_start_date),
258
+ line_item_resource_id
259
+
260
+ UNION ALL
261
+
262
+ SELECT
263
+ DATE(line_item_usage_start_date) AS usage_date,
264
+ COALESCE(line_item_resource_id, 'VPC-Peering') AS resource_id,
265
+ 'vpc' AS resource_type,
266
+ '' AS product_instance_type,
267
+ CASE
268
+ WHEN COUNT(DISTINCT product_product_family) > 1 THEN 'Mixed'
269
+ ELSE MAX(product_product_family)
270
+ END AS product_volume_type,
271
+ SUM(line_item_unblended_cost) AS total_cost,
272
+ SUM(line_item_usage_amount) AS usage_amount
273
+ FROM <cur_database>.<cur_table>
274
+ WHERE
275
+ line_item_product_code = 'AmazonEC2'
276
+ AND line_item_line_item_type = 'Usage'
277
+ AND year = '<year>'
278
+ AND month = '<month>'
279
+ AND (
280
+ line_item_resource_id LIKE 'vpce-%'
281
+ OR product_product_family IN ('VPC Peering', 'VpcEndpoint')
282
+ )
283
+ GROUP BY
284
+ DATE(line_item_usage_start_date),
285
+ line_item_resource_id
286
+ )
287
+ GROUP BY
288
+ usage_date,
289
+ resource_id,
290
+ resource_type,
291
+ product_instance_type,
292
+ product_volume_type
293
+ )
294
+ TO 's3://<bucket>/<environment>/daily/<year-month>/run=<run-timestamp>/'
295
+ WITH (
296
+ format = 'JSON',
297
+ compression = 'NONE'
298
+ );
299
+ ```
package/config.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ export interface Config {
2
+ costInsights?: {
3
+ /**
4
+ * Environment ids exposed to the UI and API.
5
+ * @example ['dev', 'stg', 'prod']
6
+ */
7
+ environments?: string[];
8
+
9
+ /**
10
+ * Default environment used when query param is omitted.
11
+ * Must exist in `costInsights.environments`.
12
+ * @example 'prod'
13
+ */
14
+ defaultEnvironment?: string;
15
+
16
+ /**
17
+ * HTTP auth behavior for cost-insights backend routes.
18
+ */
19
+ auth?: {
20
+ /**
21
+ * Allow unauthenticated access.
22
+ * Keep false by default for safer deployments.
23
+ * @default false
24
+ */
25
+ allowUnauthenticated?: boolean;
26
+ };
27
+
28
+ s3?: {
29
+ /**
30
+ * AWS region.
31
+ * @example 'ap-northeast-2'
32
+ */
33
+ region: string;
34
+
35
+ /**
36
+ * CUR result bucket name.
37
+ */
38
+ bucket: string;
39
+
40
+ /**
41
+ * Optional AWS profile for local/SSO usage.
42
+ * If omitted, default credential chain is used.
43
+ */
44
+ profile?: string;
45
+
46
+ /**
47
+ * Prefix template for daily files.
48
+ * Supported tokens: {environment}, {yearMonth}, {year}, {month}
49
+ * @default '{environment}/daily/{yearMonth}/'
50
+ */
51
+ dailyPrefixTemplate?: string;
52
+ };
53
+ };
54
+ }
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var plugin = require('./plugin.cjs.js');
6
+
7
+
8
+
9
+ exports.default = plugin.costInsightsPlugin;
10
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -0,0 +1,5 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+
3
+ declare const costInsightsPlugin: _backstage_backend_plugin_api.BackendFeature;
4
+
5
+ export { costInsightsPlugin as default };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var router = require('./service/router.cjs.js');
5
+
6
+ const costInsightsPlugin = backendPluginApi.createBackendPlugin({
7
+ pluginId: "cost-insights",
8
+ register(env) {
9
+ env.registerInit({
10
+ deps: {
11
+ logger: backendPluginApi.coreServices.logger,
12
+ config: backendPluginApi.coreServices.rootConfig,
13
+ httpRouter: backendPluginApi.coreServices.httpRouter
14
+ },
15
+ async init({ logger, config, httpRouter }) {
16
+ const router$1 = await router.createRouter({ logger, config });
17
+ httpRouter.use(router$1);
18
+ const costInsightsConfig = config.getOptionalConfig("costInsights");
19
+ const allowUnauthenticated = costInsightsConfig?.getOptionalBoolean("auth.allowUnauthenticated") ?? false;
20
+ httpRouter.addAuthPolicy({
21
+ path: "/",
22
+ allow: allowUnauthenticated ? "unauthenticated" : "user-cookie"
23
+ });
24
+ }
25
+ });
26
+ }
27
+ });
28
+
29
+ exports.costInsightsPlugin = costInsightsPlugin;
30
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\n\nexport const costInsightsPlugin = createBackendPlugin({\n pluginId: 'cost-insights',\n register(env) {\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n httpRouter: coreServices.httpRouter,\n },\n async init({ logger, config, httpRouter }) {\n const router = await createRouter({ logger, config });\n httpRouter.use(router);\n\n const costInsightsConfig = config.getOptionalConfig('costInsights');\n const allowUnauthenticated =\n costInsightsConfig?.getOptionalBoolean('auth.allowUnauthenticated') ??\n false;\n\n httpRouter.addAuthPolicy({\n path: '/',\n allow: allowUnauthenticated ? 'unauthenticated' : 'user-cookie',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","router","createRouter"],"mappings":";;;;;AAMO,MAAM,qBAAqBA,oCAAA,CAAoB;AAAA,EACpD,QAAA,EAAU,eAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,QAAQC,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,YAAYA,6BAAA,CAAa;AAAA,OAC3B;AAAA,MACA,MAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,MAAA,EAAQ,YAAW,EAAG;AACzC,QAAA,MAAMC,WAAS,MAAMC,mBAAA,CAAa,EAAE,MAAA,EAAQ,QAAQ,CAAA;AACpD,QAAA,UAAA,CAAW,IAAID,QAAM,CAAA;AAErB,QAAA,MAAM,kBAAA,GAAqB,MAAA,CAAO,iBAAA,CAAkB,cAAc,CAAA;AAClE,QAAA,MAAM,oBAAA,GACJ,kBAAA,EAAoB,kBAAA,CAAmB,2BAA2B,CAAA,IAClE,KAAA;AAEF,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,GAAA;AAAA,UACN,KAAA,EAAO,uBAAuB,iBAAA,GAAoB;AAAA,SACnD,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
@@ -0,0 +1,285 @@
1
+ 'use strict';
2
+
3
+ var clientS3 = require('@aws-sdk/client-s3');
4
+ var credentialProviders = require('@aws-sdk/credential-providers');
5
+
6
+ function streamToString(stream) {
7
+ return new Promise((resolve, reject) => {
8
+ const chunks = [];
9
+ stream.on("data", (chunk) => chunks.push(chunk));
10
+ stream.on("error", reject);
11
+ stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
12
+ });
13
+ }
14
+ async function findLatestRunPrefix(params) {
15
+ const { s3, bucket, monthPrefix } = params;
16
+ const res = await s3.send(
17
+ new clientS3.ListObjectsV2Command({
18
+ Bucket: bucket,
19
+ Prefix: monthPrefix,
20
+ Delimiter: "/"
21
+ // List "folders" as CommonPrefixes
22
+ })
23
+ );
24
+ const runs = (res.CommonPrefixes ?? []).map((p) => p.Prefix).filter((p) => !!p).filter((p) => p.includes("run="));
25
+ if (runs.length === 0) {
26
+ throw new Error(
27
+ `No run prefixes found under: s3://${bucket}/${monthPrefix}`
28
+ );
29
+ }
30
+ runs.sort();
31
+ return runs[runs.length - 1];
32
+ }
33
+ async function findDataFileKeys(params) {
34
+ const { s3, bucket, runPrefix } = params;
35
+ const res = await s3.send(
36
+ new clientS3.ListObjectsV2Command({
37
+ Bucket: bucket,
38
+ Prefix: runPrefix
39
+ })
40
+ );
41
+ const dataFiles = (res.Contents ?? []).map((o) => o.Key).filter((k) => !!k).filter((k) => !k.endsWith("-manifest.csv")).filter((k) => !k.endsWith(".metadata")).filter((k) => !k.endsWith("/")).sort();
42
+ if (dataFiles.length === 0) {
43
+ throw new Error(
44
+ `No data files found under: s3://${bucket}/${runPrefix}`
45
+ );
46
+ }
47
+ return dataFiles;
48
+ }
49
+ function parseNdjsonDailyCostRows(ndjson) {
50
+ const rows = [];
51
+ for (const line of ndjson.split(/\r?\n/)) {
52
+ const trimmed = line.trim();
53
+ if (!trimmed) continue;
54
+ const obj = JSON.parse(trimmed);
55
+ const totalCost = Number(obj.total_cost);
56
+ const usageAmount = Number(obj.usage_amount);
57
+ if (!Number.isFinite(totalCost) || !Number.isFinite(usageAmount)) {
58
+ continue;
59
+ }
60
+ rows.push({
61
+ usage_date: obj.usage_date,
62
+ resource_id: obj.resource_id,
63
+ resource_type: obj.resource_type,
64
+ product_instance_type: obj.product_instance_type ?? "",
65
+ product_volume_type: obj.product_volume_type ?? "",
66
+ total_cost: totalCost,
67
+ usage_amount: usageAmount
68
+ });
69
+ }
70
+ return rows;
71
+ }
72
+ function mapResourceType(s3Type) {
73
+ const normalized = s3Type.toLowerCase();
74
+ if (normalized.includes("instance") || normalized.includes("compute"))
75
+ return "instance";
76
+ if (normalized.includes("elastic-ip") || normalized.includes("ip address"))
77
+ return "elastic-ip";
78
+ if (normalized.includes("snapshot")) return "snapshot";
79
+ if (normalized.includes("volume") || normalized.includes("storage"))
80
+ return "volume";
81
+ if (normalized.includes("nat")) return "nat-gateway";
82
+ if (normalized.includes("transfer")) return "data-transfer";
83
+ if (normalized.includes("vpc") || normalized.includes("endpoint"))
84
+ return "vpc";
85
+ return "other";
86
+ }
87
+ class S3CostDataClient {
88
+ constructor(logger, config) {
89
+ this.logger = logger;
90
+ const credentialConfig = config.profile ? {
91
+ region: config.region,
92
+ credentials: credentialProviders.fromNodeProviderChain({
93
+ profile: config.profile
94
+ })
95
+ } : {
96
+ region: config.region
97
+ };
98
+ this.client = new clientS3.S3Client(credentialConfig);
99
+ this.bucket = config.bucket;
100
+ this.dailyPrefixTemplate = config.dailyPrefixTemplate || "{environment}/daily/{yearMonth}/";
101
+ const authMethod = config.profile ? `SSO Profile: ${config.profile}` : "Default credential chain (IRSA/EC2 instance profile)";
102
+ this.logger.info(
103
+ `[CostInsights] S3CostDataClient initialized successfully - Auth: ${authMethod}, Bucket: ${this.bucket}, Region: ${config.region}`
104
+ );
105
+ }
106
+ client;
107
+ bucket;
108
+ dailyPrefixTemplate;
109
+ buildDailyPrefix(params) {
110
+ const { environment, year, month } = params;
111
+ const paddedMonth = month.padStart(2, "0");
112
+ const yearMonth = `${year}-${paddedMonth}`;
113
+ return this.dailyPrefixTemplate.replace("{environment}", environment).replace("{yearMonth}", yearMonth).replace("{year}", year).replace("{month}", paddedMonth);
114
+ }
115
+ async queryEC2Resources(environment, year, month) {
116
+ const paddedMonth = month.padStart(2, "0");
117
+ const yearMonth = `${year}-${paddedMonth}`;
118
+ const monthPrefix = this.buildDailyPrefix({ environment, year, month });
119
+ this.logger.info(
120
+ `[CostInsights] Loading daily EC2 data from S3: ${environment}/${yearMonth}`
121
+ );
122
+ const runPrefix = await findLatestRunPrefix({
123
+ s3: this.client,
124
+ bucket: this.bucket,
125
+ monthPrefix
126
+ });
127
+ this.logger.debug(`[CostInsights] Found run prefix: ${runPrefix}`);
128
+ const dataFileKeys = await findDataFileKeys({
129
+ s3: this.client,
130
+ bucket: this.bucket,
131
+ runPrefix
132
+ });
133
+ this.logger.debug(
134
+ `[CostInsights] Found ${dataFileKeys.length} data files under ${runPrefix}`
135
+ );
136
+ const rowsByFile = await Promise.all(
137
+ dataFileKeys.map(async (dataFileKey) => {
138
+ const dataObj = await this.client.send(
139
+ new clientS3.GetObjectCommand({
140
+ Bucket: this.bucket,
141
+ Key: dataFileKey
142
+ })
143
+ );
144
+ if (!dataObj.Body) {
145
+ throw new Error(
146
+ `Data file has empty body: s3://${this.bucket}/${dataFileKey}`
147
+ );
148
+ }
149
+ const ndjson = await streamToString(dataObj.Body);
150
+ return parseNdjsonDailyCostRows(ndjson);
151
+ })
152
+ );
153
+ const dailyRows = rowsByFile.flat();
154
+ this.logger.info(
155
+ `[CostInsights] Loaded ${dailyRows.length} daily records from S3 for ${environment}/${yearMonth}`
156
+ );
157
+ const resourceMap = /* @__PURE__ */ new Map();
158
+ dailyRows.forEach((row) => {
159
+ const key = row.resource_id;
160
+ if (!resourceMap.has(key)) {
161
+ resourceMap.set(key, {
162
+ resourceType: row.resource_type,
163
+ instanceType: row.product_instance_type,
164
+ volumeType: row.product_volume_type,
165
+ totalCost: 0,
166
+ usageAmount: 0,
167
+ dailyCosts: []
168
+ });
169
+ }
170
+ const resource = resourceMap.get(key);
171
+ resource.totalCost += row.total_cost;
172
+ resource.usageAmount += row.usage_amount;
173
+ resource.dailyCosts.push({
174
+ date: row.usage_date,
175
+ cost: row.total_cost
176
+ });
177
+ });
178
+ const result = [];
179
+ resourceMap.forEach((data, resourceId) => {
180
+ result.push({
181
+ resourceId: resourceId || "Unknown",
182
+ resourceType: mapResourceType(data.resourceType),
183
+ instanceType: data.instanceType || void 0,
184
+ volumeType: data.volumeType || void 0,
185
+ totalCost: data.totalCost,
186
+ usageAmount: data.usageAmount,
187
+ dailyCosts: data.dailyCosts.sort((a, b) => a.date.localeCompare(b.date))
188
+ });
189
+ });
190
+ result.sort((a, b) => b.totalCost - a.totalCost);
191
+ this.logger.info(
192
+ `[CostInsights] Aggregated ${result.length} resources from ${dailyRows.length} daily records`
193
+ );
194
+ return result;
195
+ }
196
+ /**
197
+ * Query monthly EC2 costs for a specific environment
198
+ * Reads daily data for each month and aggregates by resource type
199
+ * @param environment - legacy, dev, or prd
200
+ * @param numMonths - number of months to look back (default: 6)
201
+ */
202
+ async queryMonthlyEC2Costs(environment, numMonths = 6) {
203
+ const now = /* @__PURE__ */ new Date();
204
+ const monthlyData = [];
205
+ this.logger.info(
206
+ `[CostInsights] Loading monthly costs for ${environment}, last ${numMonths} months`
207
+ );
208
+ for (let i = numMonths - 1; i >= 0; i--) {
209
+ const targetDate = new Date(
210
+ now.getFullYear(),
211
+ now.getMonth() - i,
212
+ 1
213
+ );
214
+ const year = targetDate.getFullYear().toString();
215
+ const month = (targetDate.getMonth() + 1).toString();
216
+ const paddedMonth = month.padStart(2, "0");
217
+ const monthKey = `${year}-${paddedMonth}`;
218
+ try {
219
+ const resources = await this.queryEC2Resources(
220
+ environment,
221
+ year,
222
+ month
223
+ );
224
+ const monthData = {
225
+ month: monthKey,
226
+ instances: 0,
227
+ volume: 0,
228
+ elasticIp: 0,
229
+ natGateway: 0,
230
+ dataTransfer: 0,
231
+ vpc: 0,
232
+ total: 0
233
+ };
234
+ resources.forEach((resource) => {
235
+ switch (resource.resourceType) {
236
+ case "instance":
237
+ monthData.instances += resource.totalCost;
238
+ break;
239
+ case "volume":
240
+ case "snapshot":
241
+ monthData.volume += resource.totalCost;
242
+ break;
243
+ case "elastic-ip":
244
+ monthData.elasticIp += resource.totalCost;
245
+ break;
246
+ case "nat-gateway":
247
+ monthData.natGateway += resource.totalCost;
248
+ break;
249
+ case "data-transfer":
250
+ monthData.dataTransfer += resource.totalCost;
251
+ break;
252
+ case "vpc":
253
+ monthData.vpc += resource.totalCost;
254
+ break;
255
+ default:
256
+ break;
257
+ }
258
+ monthData.total += resource.totalCost;
259
+ });
260
+ monthlyData.push(monthData);
261
+ this.logger.debug(
262
+ `[CostInsights] Monthly data for ${monthKey}: $${monthData.total.toFixed(2)}`
263
+ );
264
+ } catch (error) {
265
+ this.logger.warn(
266
+ `[CostInsights] Failed to load data for ${environment}/${monthKey}: ${error}`
267
+ );
268
+ monthlyData.push({
269
+ month: monthKey,
270
+ instances: 0,
271
+ volume: 0,
272
+ elasticIp: 0,
273
+ natGateway: 0,
274
+ dataTransfer: 0,
275
+ vpc: 0,
276
+ total: 0
277
+ });
278
+ }
279
+ }
280
+ return monthlyData;
281
+ }
282
+ }
283
+
284
+ exports.S3CostDataClient = S3CostDataClient;
285
+ //# sourceMappingURL=S3CostDataClient.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3CostDataClient.cjs.js","sources":["../../src/service/S3CostDataClient.ts"],"sourcesContent":["import {\n S3Client,\n ListObjectsV2Command,\n GetObjectCommand,\n} from '@aws-sdk/client-s3';\nimport { fromNodeProviderChain } from '@aws-sdk/credential-providers';\nimport type { LoggerService } from '@backstage/backend-plugin-api';\nimport { Readable } from 'stream';\n\nexport interface EC2ResourceData {\n resourceId: string;\n resourceType:\n | 'instance'\n | 'elastic-ip'\n | 'other'\n | 'snapshot'\n | 'volume'\n | 'nat-gateway'\n | 'data-transfer'\n | 'vpc';\n instanceType?: string;\n volumeType?: string;\n totalCost: number;\n usageAmount: number;\n dailyCosts: Array<{\n date: string;\n cost: number;\n }>;\n}\n\nexport interface MonthlyCostData {\n month: string;\n instances: number;\n volume: number;\n elasticIp: number;\n natGateway: number;\n dataTransfer: number;\n vpc: number;\n total: number;\n}\n\ninterface S3DailyCostRow {\n usage_date: string;\n resource_id: string;\n resource_type: string;\n product_instance_type: string;\n product_volume_type: string;\n total_cost: number;\n usage_amount: number;\n}\n\n/**\n * Convert S3 readable stream to string\n */\nfunction streamToString(stream: Readable): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n stream.on('data', (chunk: Buffer) => chunks.push(chunk));\n stream.on('error', reject);\n stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));\n });\n}\n\n/**\n * Find the latest run prefix under a month prefix\n * Example: legacy/daily/2026-01/ -> legacy/daily/2026-01/run=2026-01-16T11-14-00/\n */\nasync function findLatestRunPrefix(params: {\n s3: S3Client;\n bucket: string;\n monthPrefix: string;\n}): Promise<string> {\n const { s3, bucket, monthPrefix } = params;\n\n const res = await s3.send(\n new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: monthPrefix,\n Delimiter: '/', // List \"folders\" as CommonPrefixes\n }),\n );\n\n const runs = (res.CommonPrefixes ?? [])\n .map(p => p.Prefix)\n .filter((p): p is string => !!p)\n .filter(p => p.includes('run=')); // Only run=YYYY-MM-DDTHH-mm-ss/\n\n if (runs.length === 0) {\n throw new Error(\n `No run prefixes found under: s3://${bucket}/${monthPrefix}`,\n );\n }\n\n runs.sort();\n return runs[runs.length - 1];\n}\n\n/**\n * Find data files under a run prefix (without manifest)\n * Returns all data files found\n */\nasync function findDataFileKeys(params: {\n s3: S3Client;\n bucket: string;\n runPrefix: string;\n}): Promise<string[]> {\n const { s3, bucket, runPrefix } = params;\n\n const res = await s3.send(\n new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: runPrefix,\n }),\n );\n\n const dataFiles = (res.Contents ?? [])\n .map(o => o.Key)\n .filter((k): k is string => !!k)\n .filter(k => !k.endsWith('-manifest.csv'))\n .filter(k => !k.endsWith('.metadata'))\n .filter(k => !k.endsWith('/'))\n .sort();\n\n if (dataFiles.length === 0) {\n throw new Error(\n `No data files found under: s3://${bucket}/${runPrefix}`,\n );\n }\n\n return dataFiles;\n}\n\n/**\n * Parse NDJSON (JSON Lines) to S3DailyCostRow array\n */\nfunction parseNdjsonDailyCostRows(ndjson: string): S3DailyCostRow[] {\n const rows: S3DailyCostRow[] = [];\n\n for (const line of ndjson.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n const obj = JSON.parse(trimmed);\n\n const totalCost = Number(obj.total_cost);\n const usageAmount = Number(obj.usage_amount);\n if (!Number.isFinite(totalCost) || !Number.isFinite(usageAmount)) {\n continue;\n }\n\n rows.push({\n usage_date: obj.usage_date,\n resource_id: obj.resource_id,\n resource_type: obj.resource_type,\n product_instance_type: obj.product_instance_type ?? '',\n product_volume_type: obj.product_volume_type ?? '',\n total_cost: totalCost,\n usage_amount: usageAmount,\n });\n }\n\n return rows;\n}\n\n/**\n * Map S3 resource_type to EC2ResourceData resourceType\n */\nfunction mapResourceType(s3Type: string): EC2ResourceData['resourceType'] {\n const normalized = s3Type.toLowerCase();\n\n if (normalized.includes('instance') || normalized.includes('compute'))\n return 'instance';\n if (normalized.includes('elastic-ip') || normalized.includes('ip address'))\n return 'elastic-ip';\n if (normalized.includes('snapshot')) return 'snapshot';\n if (normalized.includes('volume') || normalized.includes('storage'))\n return 'volume';\n if (normalized.includes('nat')) return 'nat-gateway';\n if (normalized.includes('transfer')) return 'data-transfer';\n if (normalized.includes('vpc') || normalized.includes('endpoint'))\n return 'vpc';\n\n return 'other';\n}\n\nexport class S3CostDataClient {\n private client: S3Client;\n private bucket: string;\n private dailyPrefixTemplate: string;\n\n constructor(\n private readonly logger: LoggerService,\n config: {\n region: string;\n bucket: string;\n profile?: string;\n dailyPrefixTemplate?: string;\n },\n ) {\n const credentialConfig = config.profile\n ? {\n region: config.region,\n credentials: fromNodeProviderChain({\n profile: config.profile,\n }),\n }\n : {\n region: config.region,\n };\n\n this.client = new S3Client(credentialConfig);\n this.bucket = config.bucket;\n this.dailyPrefixTemplate =\n config.dailyPrefixTemplate || '{environment}/daily/{yearMonth}/';\n\n const authMethod = config.profile\n ? `SSO Profile: ${config.profile}`\n : 'Default credential chain (IRSA/EC2 instance profile)';\n\n this.logger.info(\n `[CostInsights] S3CostDataClient initialized successfully - Auth: ${authMethod}, Bucket: ${this.bucket}, Region: ${config.region}`,\n );\n }\n\n private buildDailyPrefix(params: {\n environment: string;\n year: string;\n month: string;\n }): string {\n const { environment, year, month } = params;\n const paddedMonth = month.padStart(2, '0');\n const yearMonth = `${year}-${paddedMonth}`;\n\n return this.dailyPrefixTemplate\n .replace('{environment}', environment)\n .replace('{yearMonth}', yearMonth)\n .replace('{year}', year)\n .replace('{month}', paddedMonth);\n }\n\n async queryEC2Resources(\n environment: string,\n year: string,\n month: string,\n ): Promise<EC2ResourceData[]> {\n const paddedMonth = month.padStart(2, '0');\n const yearMonth = `${year}-${paddedMonth}`;\n const monthPrefix = this.buildDailyPrefix({ environment, year, month });\n\n this.logger.info(\n `[CostInsights] Loading daily EC2 data from S3: ${environment}/${yearMonth}`,\n );\n\n const runPrefix = await findLatestRunPrefix({\n s3: this.client,\n bucket: this.bucket,\n monthPrefix,\n });\n\n this.logger.debug(`[CostInsights] Found run prefix: ${runPrefix}`);\n\n const dataFileKeys = await findDataFileKeys({\n s3: this.client,\n bucket: this.bucket,\n runPrefix,\n });\n\n this.logger.debug(\n `[CostInsights] Found ${dataFileKeys.length} data files under ${runPrefix}`,\n );\n\n const rowsByFile = await Promise.all(\n dataFileKeys.map(async dataFileKey => {\n const dataObj = await this.client.send(\n new GetObjectCommand({\n Bucket: this.bucket,\n Key: dataFileKey,\n }),\n );\n\n if (!dataObj.Body) {\n throw new Error(\n `Data file has empty body: s3://${this.bucket}/${dataFileKey}`,\n );\n }\n\n const ndjson = await streamToString(dataObj.Body as Readable);\n return parseNdjsonDailyCostRows(ndjson);\n }),\n );\n const dailyRows = rowsByFile.flat();\n\n this.logger.info(\n `[CostInsights] Loaded ${dailyRows.length} daily records from S3 for ${environment}/${yearMonth}`,\n );\n\n const resourceMap = new Map<string, {\n resourceType: string;\n instanceType: string;\n volumeType: string;\n totalCost: number;\n usageAmount: number;\n dailyCosts: Array<{ date: string; cost: number }>;\n }>();\n\n dailyRows.forEach(row => {\n const key = row.resource_id;\n\n if (!resourceMap.has(key)) {\n resourceMap.set(key, {\n resourceType: row.resource_type,\n instanceType: row.product_instance_type,\n volumeType: row.product_volume_type,\n totalCost: 0,\n usageAmount: 0,\n dailyCosts: [],\n });\n }\n\n const resource = resourceMap.get(key)!;\n resource.totalCost += row.total_cost;\n resource.usageAmount += row.usage_amount;\n resource.dailyCosts.push({\n date: row.usage_date,\n cost: row.total_cost,\n });\n });\n\n const result: EC2ResourceData[] = [];\n\n resourceMap.forEach((data, resourceId) => {\n result.push({\n resourceId: resourceId || 'Unknown',\n resourceType: mapResourceType(data.resourceType),\n instanceType: data.instanceType || undefined,\n volumeType: data.volumeType || undefined,\n totalCost: data.totalCost,\n usageAmount: data.usageAmount,\n dailyCosts: data.dailyCosts.sort((a, b) => a.date.localeCompare(b.date)),\n });\n });\n\n result.sort((a, b) => b.totalCost - a.totalCost);\n\n this.logger.info(\n `[CostInsights] Aggregated ${result.length} resources from ${dailyRows.length} daily records`,\n );\n\n return result;\n }\n\n /**\n * Query monthly EC2 costs for a specific environment\n * Reads daily data for each month and aggregates by resource type\n * @param environment - legacy, dev, or prd\n * @param numMonths - number of months to look back (default: 6)\n */\n async queryMonthlyEC2Costs(\n environment: string,\n numMonths: number = 6,\n ): Promise<MonthlyCostData[]> {\n const now = new Date();\n const monthlyData: MonthlyCostData[] = [];\n\n this.logger.info(\n `[CostInsights] Loading monthly costs for ${environment}, last ${numMonths} months`,\n );\n\n for (let i = numMonths - 1; i >= 0; i--) {\n const targetDate = new Date(\n now.getFullYear(),\n now.getMonth() - i,\n 1,\n );\n const year = targetDate.getFullYear().toString();\n const month = (targetDate.getMonth() + 1).toString();\n const paddedMonth = month.padStart(2, '0');\n const monthKey = `${year}-${paddedMonth}`;\n\n try {\n const resources = await this.queryEC2Resources(\n environment,\n year,\n month,\n );\n\n const monthData: MonthlyCostData = {\n month: monthKey,\n instances: 0,\n volume: 0,\n elasticIp: 0,\n natGateway: 0,\n dataTransfer: 0,\n vpc: 0,\n total: 0,\n };\n\n resources.forEach(resource => {\n switch (resource.resourceType) {\n case 'instance':\n monthData.instances += resource.totalCost;\n break;\n case 'volume':\n case 'snapshot':\n monthData.volume += resource.totalCost;\n break;\n case 'elastic-ip':\n monthData.elasticIp += resource.totalCost;\n break;\n case 'nat-gateway':\n monthData.natGateway += resource.totalCost;\n break;\n case 'data-transfer':\n monthData.dataTransfer += resource.totalCost;\n break;\n case 'vpc':\n monthData.vpc += resource.totalCost;\n break;\n default:\n break;\n }\n monthData.total += resource.totalCost;\n });\n\n monthlyData.push(monthData);\n\n this.logger.debug(\n `[CostInsights] Monthly data for ${monthKey}: $${monthData.total.toFixed(2)}`,\n );\n } catch (error) {\n this.logger.warn(\n `[CostInsights] Failed to load data for ${environment}/${monthKey}: ${error}`,\n );\n monthlyData.push({\n month: monthKey,\n instances: 0,\n volume: 0,\n elasticIp: 0,\n natGateway: 0,\n dataTransfer: 0,\n vpc: 0,\n total: 0,\n });\n }\n }\n\n return monthlyData;\n }\n}\n"],"names":["ListObjectsV2Command","fromNodeProviderChain","S3Client","GetObjectCommand"],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ var luxon = require('luxon');
4
+
5
+ const DEFAULT_DATE_FORMAT = "yyyy-LL-dd";
6
+ class S3CostInsightsClient {
7
+ constructor(s3Client, logger) {
8
+ this.s3Client = s3Client;
9
+ this.logger = logger;
10
+ }
11
+ async getLastCompleteBillingDate() {
12
+ return luxon.DateTime.now().minus({ days: 1 }).toFormat(DEFAULT_DATE_FORMAT);
13
+ }
14
+ async getEC2Insights(intervals, environment) {
15
+ const [startDateStr, endDateStr] = intervals.split("/");
16
+ const startDate = luxon.DateTime.fromISO(startDateStr ?? "", { setZone: true });
17
+ const endDate = luxon.DateTime.fromISO(endDateStr ?? "", { setZone: true });
18
+ if (!startDate.isValid || !endDate.isValid) {
19
+ throw new Error(`Invalid intervals: ${intervals}`);
20
+ }
21
+ if (startDate > endDate) {
22
+ throw new Error(`Invalid intervals, start > end: ${intervals}`);
23
+ }
24
+ const months = [];
25
+ let cursor = startDate.startOf("month");
26
+ const last = endDate.startOf("month");
27
+ while (cursor <= last) {
28
+ months.push({
29
+ year: cursor.toFormat("yyyy"),
30
+ month: cursor.toFormat("M")
31
+ });
32
+ cursor = cursor.plus({ months: 1 });
33
+ }
34
+ this.logger.info(
35
+ `[CostInsights] Fetching EC2 resources for ${environment}, months=${months.length}, interval=${intervals}`
36
+ );
37
+ const monthlyResources = await Promise.all(
38
+ months.map(
39
+ ({ year, month }) => this.s3Client.queryEC2Resources(environment, year, month)
40
+ )
41
+ );
42
+ const merged = /* @__PURE__ */ new Map();
43
+ for (const resources of monthlyResources) {
44
+ for (const resource of resources) {
45
+ const key = resource.resourceId || "Unknown";
46
+ const existing = merged.get(key);
47
+ if (!existing) {
48
+ merged.set(key, {
49
+ ...resource,
50
+ dailyCosts: [...resource.dailyCosts]
51
+ });
52
+ continue;
53
+ }
54
+ existing.totalCost += resource.totalCost;
55
+ existing.usageAmount += resource.usageAmount;
56
+ existing.dailyCosts.push(...resource.dailyCosts);
57
+ }
58
+ }
59
+ const ec2Resources = Array.from(merged.values()).map((resource) => ({
60
+ ...resource,
61
+ dailyCosts: resource.dailyCosts.sort(
62
+ (a, b) => a.date.localeCompare(b.date)
63
+ )
64
+ })).sort((a, b) => b.totalCost - a.totalCost);
65
+ const totalCost = ec2Resources.reduce((sum, r) => sum + r.totalCost, 0);
66
+ const monthlyData = await this.s3Client.queryMonthlyEC2Costs(
67
+ environment,
68
+ 12
69
+ );
70
+ const entities = ec2Resources.map((resource) => ({
71
+ id: resource.resourceId || "Unknown",
72
+ resourceId: resource.resourceId,
73
+ resourceType: resource.resourceType,
74
+ instanceType: resource.instanceType,
75
+ volumeType: resource.volumeType,
76
+ totalCost: resource.totalCost,
77
+ usageAmount: resource.usageAmount,
78
+ dailyCosts: resource.dailyCosts,
79
+ entities: {},
80
+ aggregation: [resource.totalCost, 0],
81
+ change: { ratio: 0, amount: 0 }
82
+ }));
83
+ return {
84
+ id: "ec2",
85
+ entities: { ec2: entities },
86
+ monthlyData,
87
+ aggregation: [totalCost, 0],
88
+ change: { ratio: 0, amount: 0 }
89
+ };
90
+ }
91
+ }
92
+
93
+ exports.S3CostInsightsClient = S3CostInsightsClient;
94
+ //# sourceMappingURL=S3CostInsightsClient.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3CostInsightsClient.cjs.js","sources":["../../src/service/S3CostInsightsClient.ts"],"sourcesContent":["import type { LoggerService } from '@backstage/backend-plugin-api';\nimport { DateTime } from 'luxon';\nimport type { EC2ResourceData, S3CostDataClient } from './S3CostDataClient';\n\nconst DEFAULT_DATE_FORMAT = 'yyyy-LL-dd';\n\nexport class S3CostInsightsClient {\n constructor(\n private readonly s3Client: S3CostDataClient,\n private readonly logger: LoggerService,\n ) {}\n\n async getLastCompleteBillingDate(): Promise<string> {\n return DateTime.now().minus({ days: 1 }).toFormat(DEFAULT_DATE_FORMAT);\n }\n\n async getEC2Insights(\n intervals: string,\n environment: string,\n ) {\n const [startDateStr, endDateStr] = intervals.split('/');\n const startDate = DateTime.fromISO(startDateStr ?? '', { setZone: true });\n const endDate = DateTime.fromISO(endDateStr ?? '', { setZone: true });\n\n if (!startDate.isValid || !endDate.isValid) {\n throw new Error(`Invalid intervals: ${intervals}`);\n }\n if (startDate > endDate) {\n throw new Error(`Invalid intervals, start > end: ${intervals}`);\n }\n\n const months: Array<{ year: string; month: string }> = [];\n let cursor = startDate.startOf('month');\n const last = endDate.startOf('month');\n while (cursor <= last) {\n months.push({\n year: cursor.toFormat('yyyy'),\n month: cursor.toFormat('M'),\n });\n cursor = cursor.plus({ months: 1 });\n }\n\n this.logger.info(\n `[CostInsights] Fetching EC2 resources for ${environment}, months=${months.length}, interval=${intervals}`,\n );\n\n const monthlyResources = await Promise.all(\n months.map(({ year, month }) =>\n this.s3Client.queryEC2Resources(environment, year, month),\n ),\n );\n\n const merged = new Map<string, EC2ResourceData>();\n for (const resources of monthlyResources) {\n for (const resource of resources) {\n const key = resource.resourceId || 'Unknown';\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, {\n ...resource,\n dailyCosts: [...resource.dailyCosts],\n });\n continue;\n }\n\n existing.totalCost += resource.totalCost;\n existing.usageAmount += resource.usageAmount;\n existing.dailyCosts.push(...resource.dailyCosts);\n }\n }\n\n const ec2Resources = Array.from(merged.values())\n .map(resource => ({\n ...resource,\n dailyCosts: resource.dailyCosts.sort((a, b) =>\n a.date.localeCompare(b.date),\n ),\n }))\n .sort((a, b) => b.totalCost - a.totalCost);\n\n const totalCost = ec2Resources.reduce((sum, r) => sum + r.totalCost, 0);\n\n // Always fetch 12 months for frontend flexibility (6/12 months toggle)\n const monthlyData = await this.s3Client.queryMonthlyEC2Costs(\n environment,\n 12,\n );\n\n const entities = ec2Resources.map(resource => ({\n id: resource.resourceId || 'Unknown',\n resourceId: resource.resourceId,\n resourceType: resource.resourceType,\n instanceType: resource.instanceType,\n volumeType: resource.volumeType,\n totalCost: resource.totalCost,\n usageAmount: resource.usageAmount,\n dailyCosts: resource.dailyCosts,\n entities: {},\n aggregation: [resource.totalCost, 0] as [number, number],\n change: { ratio: 0, amount: 0 },\n }));\n\n return {\n id: 'ec2',\n entities: { ec2: entities },\n monthlyData: monthlyData,\n aggregation: [totalCost, 0] as [number, number],\n change: { ratio: 0, amount: 0 },\n };\n }\n}\n"],"names":["DateTime"],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ var express = require('express');
4
+ var Router = require('express-promise-router');
5
+ var luxon = require('luxon');
6
+ var S3CostDataClient = require('./S3CostDataClient.cjs.js');
7
+ var S3CostInsightsClient = require('./S3CostInsightsClient.cjs.js');
8
+
9
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
10
+
11
+ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
12
+ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
13
+
14
+ async function createRouter(options) {
15
+ const { logger, config } = options;
16
+ const router = Router__default.default();
17
+ router.use(express__default.default.json());
18
+ const costInsightsConfig = config.getOptionalConfig("costInsights");
19
+ const s3Config = costInsightsConfig?.getOptionalConfig("s3");
20
+ if (!s3Config) {
21
+ logger.warn("[CostInsights] S3 configuration not found");
22
+ router.get("/health", (_, res) => {
23
+ res.json({ status: "ok", message: "S3 not configured" });
24
+ });
25
+ return router;
26
+ }
27
+ const environments = costInsightsConfig?.getOptionalStringArray("environments") ?? ["prd"];
28
+ const configuredDefaultEnvironment = costInsightsConfig?.getOptionalString("defaultEnvironment") ?? environments[0];
29
+ const defaultEnvironment = environments.includes(configuredDefaultEnvironment) ? configuredDefaultEnvironment : environments[0];
30
+ if (!environments.includes(configuredDefaultEnvironment)) {
31
+ logger.warn(
32
+ `[CostInsights] defaultEnvironment (${configuredDefaultEnvironment}) is not in environments list, fallback to ${defaultEnvironment}`
33
+ );
34
+ }
35
+ const s3DataClient = new S3CostDataClient.S3CostDataClient(logger, {
36
+ region: s3Config.getString("region"),
37
+ bucket: s3Config.getString("bucket"),
38
+ profile: s3Config.getOptionalString("profile"),
39
+ dailyPrefixTemplate: s3Config.getOptionalString("dailyPrefixTemplate")
40
+ });
41
+ const costInsightsClient = new S3CostInsightsClient.S3CostInsightsClient(s3DataClient, logger);
42
+ logger.info(`[CostInsights] Initialized S3 client for environments: ${environments.join(", ")}`);
43
+ router.get("/health", (_, res) => {
44
+ res.json({ status: "ok", environments });
45
+ });
46
+ router.get("/config", (_, res) => {
47
+ res.json({
48
+ environments,
49
+ defaultEnvironment
50
+ });
51
+ });
52
+ router.get("/last-complete-date", async (req, res) => {
53
+ const { environment = defaultEnvironment } = req.query;
54
+ if (!environments.includes(environment)) {
55
+ res.status(400).json({ error: `Invalid environment: ${environment}` });
56
+ return;
57
+ }
58
+ const date = await costInsightsClient.getLastCompleteBillingDate();
59
+ res.json({ date, environment });
60
+ });
61
+ router.get("/product/ec2/insights", async (req, res) => {
62
+ const { intervals, environment = defaultEnvironment } = req.query;
63
+ logger.info(`[CostInsights] GET /product/ec2/insights - intervals: ${intervals}, environment: ${environment}`);
64
+ if (!intervals) {
65
+ res.status(400).json({ error: "intervals parameter required" });
66
+ return;
67
+ }
68
+ const [startStr, endStr] = intervals.split("/");
69
+ const start = luxon.DateTime.fromISO(startStr ?? "", { setZone: true });
70
+ const end = luxon.DateTime.fromISO(endStr ?? "", { setZone: true });
71
+ if (!start.isValid || !end.isValid) {
72
+ res.status(400).json({ error: "intervals must be valid ISO interval: start/end" });
73
+ return;
74
+ }
75
+ if (start > end) {
76
+ res.status(400).json({ error: "interval start must be before or equal to end" });
77
+ return;
78
+ }
79
+ const monthDiff = Math.abs(
80
+ end.startOf("month").diff(start.startOf("month"), "months").months
81
+ );
82
+ if (monthDiff > 24) {
83
+ res.status(400).json({ error: "intervals range must be within 24 months" });
84
+ return;
85
+ }
86
+ if (!environments.includes(environment)) {
87
+ res.status(400).json({ error: `Invalid environment: ${environment}` });
88
+ return;
89
+ }
90
+ const insights = await costInsightsClient.getEC2Insights(
91
+ intervals,
92
+ environment
93
+ );
94
+ res.json({ ...insights, environment });
95
+ });
96
+ logger.info(`[CostInsights] Router initialized successfully for environments: ${environments.join(", ")}`);
97
+ return router;
98
+ }
99
+
100
+ exports.createRouter = createRouter;
101
+ //# sourceMappingURL=router.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["import type { LoggerService } from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { DateTime } from 'luxon';\nimport { S3CostDataClient } from './S3CostDataClient';\nimport { S3CostInsightsClient } from './S3CostInsightsClient';\n\nexport interface RouterOptions {\n logger: LoggerService;\n config: Config;\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const { logger, config } = options;\n\n const router = Router();\n router.use(express.json());\n\n const costInsightsConfig = config.getOptionalConfig('costInsights');\n const s3Config = costInsightsConfig?.getOptionalConfig('s3');\n\n if (!s3Config) {\n logger.warn('[CostInsights] S3 configuration not found');\n router.get('/health', (_, res) => {\n res.json({ status: 'ok', message: 'S3 not configured' });\n });\n return router;\n }\n\n const environments =\n costInsightsConfig?.getOptionalStringArray('environments') ?? ['prd'];\n const configuredDefaultEnvironment =\n costInsightsConfig?.getOptionalString('defaultEnvironment') ??\n environments[0];\n const defaultEnvironment = environments.includes(configuredDefaultEnvironment)\n ? configuredDefaultEnvironment\n : environments[0];\n\n if (!environments.includes(configuredDefaultEnvironment)) {\n logger.warn(\n `[CostInsights] defaultEnvironment (${configuredDefaultEnvironment}) is not in environments list, fallback to ${defaultEnvironment}`,\n );\n }\n\n const s3DataClient = new S3CostDataClient(logger, {\n region: s3Config.getString('region'),\n bucket: s3Config.getString('bucket'),\n profile: s3Config.getOptionalString('profile'),\n dailyPrefixTemplate: s3Config.getOptionalString('dailyPrefixTemplate'),\n });\n\n const costInsightsClient = new S3CostInsightsClient(s3DataClient, logger);\n\n logger.info(`[CostInsights] Initialized S3 client for environments: ${environments.join(', ')}`)\n\n router.get('/health', (_, res) => {\n res.json({ status: 'ok', environments });\n });\n\n router.get('/config', (_, res) => {\n res.json({\n environments,\n defaultEnvironment,\n });\n });\n\n router.get('/last-complete-date', async (req, res) => {\n const { environment = defaultEnvironment } = req.query as {\n environment?: string;\n };\n\n if (!environments.includes(environment)) {\n res.status(400).json({ error: `Invalid environment: ${environment}` });\n return;\n }\n\n const date = await costInsightsClient.getLastCompleteBillingDate();\n res.json({ date, environment });\n });\n\n router.get('/product/ec2/insights', async (req, res) => {\n const { intervals, environment = defaultEnvironment } = req.query as {\n intervals: string;\n environment?: string;\n };\n\n logger.info(`[CostInsights] GET /product/ec2/insights - intervals: ${intervals}, environment: ${environment}`);\n\n if (!intervals) {\n res.status(400).json({ error: 'intervals parameter required' });\n return;\n }\n\n const [startStr, endStr] = intervals.split('/');\n const start = DateTime.fromISO(startStr ?? '', { setZone: true });\n const end = DateTime.fromISO(endStr ?? '', { setZone: true });\n if (!start.isValid || !end.isValid) {\n res\n .status(400)\n .json({ error: 'intervals must be valid ISO interval: start/end' });\n return;\n }\n if (start > end) {\n res\n .status(400)\n .json({ error: 'interval start must be before or equal to end' });\n return;\n }\n const monthDiff = Math.abs(\n end.startOf('month').diff(start.startOf('month'), 'months').months,\n );\n if (monthDiff > 24) {\n res\n .status(400)\n .json({ error: 'intervals range must be within 24 months' });\n return;\n }\n\n if (!environments.includes(environment)) {\n res.status(400).json({ error: `Invalid environment: ${environment}` });\n return;\n }\n\n const insights = await costInsightsClient.getEC2Insights(\n intervals,\n environment,\n );\n res.json({ ...insights, environment });\n });\n\n logger.info(`[CostInsights] Router initialized successfully for environments: ${environments.join(', ')}`);\n\n return router;\n}\n"],"names":["Router","express","S3CostDataClient","S3CostInsightsClient","DateTime"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@letthem/backstage-plugin-aws-cost-insights-backend",
3
+ "version": "0.2.0",
4
+ "main": "dist/index.cjs.js",
5
+ "types": "dist/index.d.ts",
6
+ "license": "Apache-2.0",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "main": "dist/index.cjs.js",
10
+ "types": "dist/index.d.ts"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/letthem/backstage-plugin-cost-insights.git",
15
+ "directory": "plugins/cost-insights-backend"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/letthem/backstage-plugin-cost-insights/issues"
19
+ },
20
+ "keywords": [
21
+ "backstage",
22
+ "plugin",
23
+ "backend",
24
+ "cost-insights",
25
+ "aws",
26
+ "s3",
27
+ "athena",
28
+ "finops"
29
+ ],
30
+ "homepage": "https://github.com/letthem/backstage-plugin-cost-insights#readme",
31
+ "backstage": {
32
+ "role": "backend-plugin",
33
+ "pluginId": "aws-cost-insights",
34
+ "pluginPackages": [
35
+ "@letthem/backstage-plugin-aws-cost-insights",
36
+ "@letthem/backstage-plugin-aws-cost-insights-backend"
37
+ ]
38
+ },
39
+ "sideEffects": false,
40
+ "scripts": {
41
+ "build": "backstage-cli package build",
42
+ "lint": "backstage-cli package lint",
43
+ "test": "backstage-cli package test --passWithNoTests",
44
+ "clean": "backstage-cli package clean",
45
+ "prepack": "backstage-cli package prepack",
46
+ "postpack": "backstage-cli package postpack"
47
+ },
48
+ "dependencies": {
49
+ "@aws-sdk/client-athena": "^3.700.0",
50
+ "@aws-sdk/client-s3": "^3.700.0",
51
+ "@aws-sdk/credential-providers": "^3.967.0",
52
+ "@backstage/backend-plugin-api": "^1.5.0",
53
+ "@backstage/config": "^1.3.6",
54
+ "express": "^4.17.1",
55
+ "express-promise-router": "^4.1.0",
56
+ "luxon": "^3.0.0",
57
+ "node-fetch": "2",
58
+ "winston": "^3.2.1"
59
+ },
60
+ "devDependencies": {
61
+ "@backstage/cli": "^0.29.3",
62
+ "@types/express": "^4.17.6",
63
+ "@types/luxon": "^3.4.2",
64
+ "@types/node": "^18"
65
+ },
66
+ "files": [
67
+ "dist",
68
+ "config.d.ts"
69
+ ],
70
+ "typesVersions": {
71
+ "*": {
72
+ "index": [
73
+ "dist/index.d.ts"
74
+ ]
75
+ }
76
+ }
77
+ }