@meyicloud/meyi-cost-server 1.8.6 → 1.8.7
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/package.json
CHANGED
|
@@ -112,27 +112,37 @@ function rangeWhere(range) {
|
|
|
112
112
|
AND CAST(line_item_usage_start_date AS TIMESTAMP) < TIMESTAMP ${sqlString(`${range.End} 00:00:00`)}`;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function inCondition(column, values = []) {
|
|
115
|
+
function inCondition(column, values = []) {
|
|
116
116
|
if (!values.length) return "";
|
|
117
117
|
return `AND CAST("${column}" AS VARCHAR) IN (${values.map(sqlString).join(", ")})`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function cur2TagExpression(tagKey) {
|
|
121
|
-
const key = String(tagKey || "
|
|
122
|
-
if (!key) throw new Error("Invalid CUR tag key");
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cur2TagExpression(tagKey) {
|
|
121
|
+
const key = String(tagKey || "").trim();
|
|
122
|
+
if (!key) throw new Error("Invalid CUR tag key");
|
|
123
|
+
const plainKey = key.replace(/^(?:user:|resourceTags\/)/, "");
|
|
124
|
+
const candidates = key.includes("/")
|
|
125
|
+
? [key]
|
|
126
|
+
: [`resourceTags/${plainKey}`, `user:${plainKey}`, plainKey];
|
|
127
|
+
return `COALESCE(${[...new Set(candidates)].map((candidate) => `element_at(resource_tags, ${sqlString(candidate)})`).join(", ")})`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const regionExpression = `COALESCE(
|
|
131
|
+
NULLIF(TRIM(CAST(product_region_code AS VARCHAR)), ''),
|
|
132
|
+
NULLIF(regexp_extract(COALESCE(line_item_resource_id, ''), '^arn:[^:]+:[^:]+:([^:]+):', 1), ''),
|
|
133
|
+
'Region unavailable in CUR'
|
|
134
|
+
)`;
|
|
125
135
|
|
|
126
136
|
function inExpression(expression, values = []) {
|
|
127
137
|
if (!values.length) return "";
|
|
128
138
|
return `AND CAST(${expression} AS VARCHAR) IN (${values.map(sqlString).join(", ")})`;
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
function filterWhere(filters = {}) {
|
|
132
|
-
const conditions = [
|
|
133
|
-
inCondition("product_product_name", filters.services),
|
|
134
|
-
|
|
135
|
-
inCondition("line_item_usage_account_id", filters.accountIds),
|
|
141
|
+
function filterWhere(filters = {}) {
|
|
142
|
+
const conditions = [
|
|
143
|
+
inCondition("product_product_name", filters.services),
|
|
144
|
+
inExpression(regionExpression, filters.regions),
|
|
145
|
+
inCondition("line_item_usage_account_id", filters.accountIds),
|
|
136
146
|
];
|
|
137
147
|
if (filters.tagKey && filters.tagValues?.length) {
|
|
138
148
|
conditions.push(inExpression(cur2TagExpression(filters.tagKey), filters.tagValues));
|
|
@@ -141,14 +151,15 @@ function filterWhere(filters = {}) {
|
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
function dimensionExpression(groupBy, tagKey) {
|
|
144
|
-
const dimensions = {
|
|
145
|
-
service: "product_product_name",
|
|
146
|
-
account: "line_item_usage_account_id",
|
|
147
|
-
region:
|
|
148
|
-
resource: "line_item_resource_id",
|
|
149
|
-
};
|
|
150
|
-
if (groupBy === "tag") return cur2TagExpression(tagKey);
|
|
151
|
-
|
|
154
|
+
const dimensions = {
|
|
155
|
+
service: "product_product_name",
|
|
156
|
+
account: "line_item_usage_account_id",
|
|
157
|
+
region: regionExpression,
|
|
158
|
+
resource: "line_item_resource_id",
|
|
159
|
+
};
|
|
160
|
+
if (groupBy === "tag") return cur2TagExpression(tagKey);
|
|
161
|
+
if (groupBy === "region") return dimensions.region;
|
|
162
|
+
return dimensions[groupBy] ? `"${dimensions[groupBy]}"` : undefined;
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
function summarize(periods, accountNames = new Map()) {
|
|
@@ -166,7 +177,7 @@ function summarize(periods, accountNames = new Map()) {
|
|
|
166
177
|
.sort((a, b) => b.amount - a.amount);
|
|
167
178
|
}
|
|
168
179
|
|
|
169
|
-
export async function getCurReport({ client, config, tenant, range, groupBy, granularity = "MONTHLY", tagKey, accountNames = new Map(), filters = {}, includeBreakdown = false }) {
|
|
180
|
+
export async function getCurReport({ client, config, tenant, range, groupBy, granularity = "MONTHLY", tagKey, accountNames = new Map(), filters = {}, includeBreakdown = false }) {
|
|
170
181
|
const column = dimensionExpression(groupBy, tagKey);
|
|
171
182
|
if (!column) throw new Error("Unsupported CUR report group");
|
|
172
183
|
const bucket = granularity === "DAILY" ? "day" : "month";
|
|
@@ -178,9 +189,10 @@ export async function getCurReport({ client, config, tenant, range, groupBy, gra
|
|
|
178
189
|
CAST(SUM(line_item_unblended_cost) AS DOUBLE) AS amount
|
|
179
190
|
FROM ${table(config)}
|
|
180
191
|
WHERE ${sourceWhere(config, tenant)}
|
|
181
|
-
AND ${rangeWhere(range)}
|
|
182
|
-
AND line_item_unblended_cost > 0
|
|
183
|
-
${
|
|
192
|
+
AND ${rangeWhere(range)}
|
|
193
|
+
AND line_item_unblended_cost > 0
|
|
194
|
+
${groupBy === "tag" ? `AND NULLIF(TRIM(CAST(${column} AS VARCHAR)), '') IS NOT NULL` : ""}
|
|
195
|
+
${filterWhere(filters)}
|
|
184
196
|
GROUP BY 1, 2
|
|
185
197
|
ORDER BY 1, 3 DESC
|
|
186
198
|
${limit}
|
|
@@ -188,18 +200,19 @@ export async function getCurReport({ client, config, tenant, range, groupBy, gra
|
|
|
188
200
|
const rows = await execute(client, config, query);
|
|
189
201
|
const periods = rows.map((row) => ({ start: String(row.period_start).slice(0, 10), key: row.item_key || "Unallocated", amount: amount(row.amount) }));
|
|
190
202
|
let summary = summarize(periods, accountNames);
|
|
191
|
-
if (includeBreakdown
|
|
203
|
+
if (includeBreakdown) {
|
|
192
204
|
const relationQuery = `
|
|
193
205
|
SELECT
|
|
194
206
|
COALESCE(NULLIF(CAST(${column} AS VARCHAR), ''), 'Unallocated') AS item_key,
|
|
195
207
|
COALESCE(NULLIF(CAST(line_item_usage_account_id AS VARCHAR), ''), 'Unallocated') AS account_key,
|
|
196
|
-
|
|
208
|
+
${regionExpression} AS region_key,
|
|
197
209
|
CAST(SUM(line_item_unblended_cost) AS DOUBLE) AS amount
|
|
198
210
|
FROM ${table(config)}
|
|
199
211
|
WHERE ${sourceWhere(config, tenant)}
|
|
200
|
-
AND ${rangeWhere(range)}
|
|
201
|
-
AND line_item_unblended_cost > 0
|
|
202
|
-
${
|
|
212
|
+
AND ${rangeWhere(range)}
|
|
213
|
+
AND line_item_unblended_cost > 0
|
|
214
|
+
${groupBy === "tag" ? `AND NULLIF(TRIM(CAST(${column} AS VARCHAR)), '') IS NOT NULL` : ""}
|
|
215
|
+
${filterWhere(filters)}
|
|
203
216
|
GROUP BY 1, 2, 3
|
|
204
217
|
`;
|
|
205
218
|
const relationRows = await execute(client, config, relationQuery);
|
|
@@ -213,7 +226,7 @@ export async function getCurReport({ client, config, tenant, range, groupBy, gra
|
|
|
213
226
|
values.set(relationKey, (values.get(relationKey) || 0) + amount(row.amount));
|
|
214
227
|
};
|
|
215
228
|
add(accountsByItem, row.account_key || "Unallocated");
|
|
216
|
-
add(regionsByItem, row.region_key || "
|
|
229
|
+
add(regionsByItem, row.region_key || "Region unavailable in CUR");
|
|
217
230
|
}
|
|
218
231
|
const relations = (target, key, type) => [...(target.get(key) || new Map()).entries()].map(([value, cost]) => ({ key: value, label: type === "account" ? accountNames.get(value) || value : value, amount: round(cost) })).sort((a, b) => b.amount - a.amount);
|
|
219
232
|
summary = summary.map((item) => ({ ...item, accounts: relations(accountsByItem, item.key, "account"), regions: relations(regionsByItem, item.key, "region") }));
|
|
@@ -302,7 +315,7 @@ export async function getCurOverview({ client, config, tenant, range, trendRange
|
|
|
302
315
|
};
|
|
303
316
|
}
|
|
304
317
|
|
|
305
|
-
export async function getCurTags({ client, config, tenant }) {
|
|
318
|
+
export async function getCurTags({ client, config, tenant }) {
|
|
306
319
|
if (!tenant) throw new Error("A tenant is required to list CUR tags");
|
|
307
320
|
const query = `
|
|
308
321
|
SELECT DISTINCT tag_key
|
|
@@ -314,8 +327,10 @@ export async function getCurTags({ client, config, tenant }) {
|
|
|
314
327
|
ORDER BY tag_key
|
|
315
328
|
`;
|
|
316
329
|
const rows = await execute(client, config, query);
|
|
317
|
-
return rows
|
|
318
|
-
|
|
330
|
+
return rows
|
|
331
|
+
.map((row) => String(row.tag_key || "").replace(/^(?:user:|resourceTags\/)/, ""))
|
|
332
|
+
.filter(Boolean);
|
|
333
|
+
}
|
|
319
334
|
|
|
320
335
|
export async function getCurFilterOptions({ client, config, tenant, range, tagKey, accountNames = new Map() }) {
|
|
321
336
|
const distinct = async (column) => execute(client, config, `
|
|
@@ -328,9 +343,16 @@ export async function getCurFilterOptions({ client, config, tenant, range, tagKe
|
|
|
328
343
|
AND TRIM(CAST("${column}" AS VARCHAR)) <> ''
|
|
329
344
|
ORDER BY 1
|
|
330
345
|
`);
|
|
331
|
-
const [services, regions, accounts, tags] = await Promise.all([
|
|
332
|
-
distinct("product_product_name"),
|
|
333
|
-
|
|
346
|
+
const [services, regions, accounts, tags] = await Promise.all([
|
|
347
|
+
distinct("product_product_name"),
|
|
348
|
+
execute(client, config, `
|
|
349
|
+
SELECT DISTINCT CAST(${regionExpression} AS VARCHAR) AS value
|
|
350
|
+
FROM ${table(config)}
|
|
351
|
+
WHERE ${sourceWhere(config, tenant)}
|
|
352
|
+
AND ${rangeWhere(range)}
|
|
353
|
+
AND line_item_unblended_cost > 0
|
|
354
|
+
ORDER BY 1
|
|
355
|
+
`),
|
|
334
356
|
distinct("line_item_usage_account_id"),
|
|
335
357
|
getCurTags({ client, config, tenant }),
|
|
336
358
|
]);
|
|
@@ -7,7 +7,13 @@ export class CostAnalysisReportWorker {
|
|
|
7
7
|
this.logger = logger;
|
|
8
8
|
this.enabled = String(env.COST_AI_ENABLED || "").toLowerCase() === "true";
|
|
9
9
|
this.pollMs = Math.max(Number(env.COST_AI_REPORT_POLL_INTERVAL_MS || 60_000), 10_000);
|
|
10
|
-
|
|
10
|
+
// Must exceed COST_AI_ANALYSER_TIMEOUT_MS (45 min). claimDue holds a
|
|
11
|
+
// schedule by pushing next_run_at forward by the lease; run() releases it
|
|
12
|
+
// on every exit path, so the value only matters when the process dies
|
|
13
|
+
// mid-run. A lease shorter than the analyser timeout lets a second worker
|
|
14
|
+
// re-claim a schedule whose Fargate task is still running - a duplicate
|
|
15
|
+
// report row and a duplicate billed model run.
|
|
16
|
+
this.leaseMs = Math.max(Number(env.COST_AI_REPORT_LEASE_MS || 60 * 60_000), 60_000);
|
|
11
17
|
this.batchSize = Math.min(Math.max(Number(env.COST_AI_REPORT_BATCH_SIZE || 5), 1), 25);
|
|
12
18
|
this.running = false;
|
|
13
19
|
this.timer = null;
|