@meyicloud/meyi-cost-server 1.8.5 → 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/README.md +3 -1
- package/package.json +1 -1
- package/src/cur-discovery/cur-discovery.aws.js +7 -5
- package/src/cur-discovery/cur-discovery.service.js +2 -1
- package/src/services/cost-analyser.service.js +2 -0
- package/src/services/cur.service.js +78 -51
- package/src/services/saas-athena-context.service.js +5 -1
- package/src/workers/cost-analysis-report.worker.js +7 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ flowchart LR
|
|
|
48
48
|
Router --> Controller[Cost controller]
|
|
49
49
|
Controller --> Customer[Tenant CUR metadata]
|
|
50
50
|
Controller --> SaaS[SaaS Athena context]
|
|
51
|
-
SaaS --> Athena[Central tenant-partitioned CUR]
|
|
51
|
+
SaaS --> Athena[Central tenant and connection-partitioned CUR]
|
|
52
52
|
Controller --> Budget[Budget services]
|
|
53
53
|
Budget --> DB[(PostgreSQL)]
|
|
54
54
|
```
|
|
@@ -319,6 +319,7 @@ failed and do not change CUR, standard reports, or onboarding.
|
|
|
319
319
|
| `AWS_REGION` | `us-east-1` | No | Used as the CUR region fallback. |
|
|
320
320
|
| `COST_CUR_TENANT_COLUMN` | `tenant_id` | No | CUR column used to isolate the Hive-style tenant partition. |
|
|
321
321
|
| `COST_CUR_TENANT_PARTITION` | Request tenant ID | No | Overrides the partition value. Avoid a global override in multi-tenant production unless every request is intentionally mapped to that partition. |
|
|
322
|
+
| `COST_CUR_CONNECTION_COLUMN` | `connection_id` | No | CUR column used to isolate the selected AWS source connection. The connection value is resolved from onboarding data and is not a global environment override. |
|
|
322
323
|
| `COST_CUR_MAX_ROWS` | `1000` | No | Resource-report row cap, clamped between 1 and 5000. |
|
|
323
324
|
| `COST_CUR_ROLE_ARN` | Empty | No | Dedicated SaaS-side role assumed only for central Athena/Glue/S3 queries. |
|
|
324
325
|
| `COST_CUR_EXTERNAL_ID` | Empty | No | External ID for the dedicated SaaS CUR role. |
|
|
@@ -344,6 +345,7 @@ COST_CUR_OUTPUT_LOCATION=s3://meyi-saas-athena-results/
|
|
|
344
345
|
COST_CUR_REGION=us-east-1
|
|
345
346
|
COST_CUR_WORKGROUP=meyi-cost
|
|
346
347
|
COST_CUR_TENANT_COLUMN=tenant_id
|
|
348
|
+
COST_CUR_CONNECTION_COLUMN=connection_id
|
|
347
349
|
COST_CUR_MAX_ROWS=1000
|
|
348
350
|
COST_CUR_ROLE_ARN=arn:aws:iam::SAAS_ACCOUNT_ID:role/meyi-cur-query
|
|
349
351
|
COST_CUR_ALLOW_TENANT_CATALOG=false
|
package/package.json
CHANGED
|
@@ -27,8 +27,9 @@ export class CurDiscoveryAws {
|
|
|
27
27
|
this.env = env;
|
|
28
28
|
this.region = String(env.COST_CUR_REGION || env.AWS_REGION || "us-east-1");
|
|
29
29
|
this.database = String(env.COST_CUR_DATABASE || "").trim();
|
|
30
|
-
this.tableOverride = String(env.COST_CUR_TABLE || "").trim();
|
|
31
|
-
this.tenantColumn = String(env.COST_CUR_TENANT_COLUMN || "tenant_id").trim();
|
|
30
|
+
this.tableOverride = String(env.COST_CUR_TABLE || "").trim();
|
|
31
|
+
this.tenantColumn = String(env.COST_CUR_TENANT_COLUMN || "tenant_id").trim();
|
|
32
|
+
this.connectionColumn = String(env.COST_CUR_CONNECTION_COLUMN || "connection_id").trim();
|
|
32
33
|
const credentials = centralCredentials(env, credentialProvider);
|
|
33
34
|
this.glue = glueClient || new GlueClient({ region: this.region, credentials });
|
|
34
35
|
this.s3 = s3Client || new S3Client({ region: this.region, credentials });
|
|
@@ -60,9 +61,10 @@ export class CurDiscoveryAws {
|
|
|
60
61
|
const columns = new Set((table.StorageDescriptor?.Columns || []).map((item) => String(item.Name || "").toLowerCase()));
|
|
61
62
|
const partitions = new Set((table.PartitionKeys || []).map((item) => String(item.Name || "").toLowerCase()));
|
|
62
63
|
const pathMatches = location && (tenantUri.startsWith(location) || location.startsWith(tenantUri));
|
|
63
|
-
const tenantSafe = partitions.has(this.tenantColumn.toLowerCase()) || columns.has(this.tenantColumn.toLowerCase());
|
|
64
|
-
const
|
|
65
|
-
|
|
64
|
+
const tenantSafe = partitions.has(this.tenantColumn.toLowerCase()) || columns.has(this.tenantColumn.toLowerCase());
|
|
65
|
+
const connectionSafe = partitions.has(this.connectionColumn.toLowerCase()) || columns.has(this.connectionColumn.toLowerCase());
|
|
66
|
+
const curSchema = columns.has("line_item_usage_start_date");
|
|
67
|
+
if (pathMatches && tenantSafe && connectionSafe && curSchema) {
|
|
66
68
|
candidates.push({
|
|
67
69
|
database: this.database,
|
|
68
70
|
name: table.Name,
|
|
@@ -61,7 +61,8 @@ export class CurDiscoveryService {
|
|
|
61
61
|
curSourceBucket: job.cur_bucket,
|
|
62
62
|
curSourcePrefix: job.cur_prefix,
|
|
63
63
|
curSourceRegion: job.cur_region,
|
|
64
|
-
curIngestionMode: "central",
|
|
64
|
+
curIngestionMode: "central",
|
|
65
|
+
connectionId: job.connection_id,
|
|
65
66
|
},
|
|
66
67
|
};
|
|
67
68
|
const context = this.athenaContextService.resolve(customerContext);
|
|
@@ -201,6 +201,8 @@ export class CostAnalyserService {
|
|
|
201
201
|
COST_CUR_REGION: athena.region,
|
|
202
202
|
COST_CUR_TENANT_COLUMN: athena.tenantColumn,
|
|
203
203
|
COST_CUR_TENANT_PARTITION: athena.tenantPartition || tenantId,
|
|
204
|
+
COST_CUR_CONNECTION_COLUMN: required(athena.connectionColumn, "COST_CUR_CONNECTION_COLUMN"),
|
|
205
|
+
COST_CUR_CONNECTION_PARTITION: required(athena.connectionPartition, "COST_CUR_CONNECTION_PARTITION"),
|
|
204
206
|
AI_PROVIDER_SECRET_ARN: providerSecretArn,
|
|
205
207
|
AI_PROVIDER_SECRET_REGION: this.region,
|
|
206
208
|
TOP_N_SERVICES: this.topServices,
|
|
@@ -27,9 +27,11 @@ export function getCurConfig(env = process.env, metadata = {}) {
|
|
|
27
27
|
table: tableName ? identifier(tableName, "table", true) : "",
|
|
28
28
|
outputLocation,
|
|
29
29
|
region: String(metadata.curRegion || env.COST_CUR_REGION || env.AWS_REGION || "us-east-1"),
|
|
30
|
-
workgroup: identifier(metadata.curWorkgroup || env.COST_CUR_WORKGROUP || "meyi-cost", "workgroup", true),
|
|
31
|
-
tenantColumn: identifier(metadata.curTenantColumn || env.COST_CUR_TENANT_COLUMN || "tenant_id", "tenant column"),
|
|
32
|
-
tenantPartition: String(metadata.curTenantPartition || env.COST_CUR_TENANT_PARTITION || "").trim(),
|
|
30
|
+
workgroup: identifier(metadata.curWorkgroup || env.COST_CUR_WORKGROUP || "meyi-cost", "workgroup", true),
|
|
31
|
+
tenantColumn: identifier(metadata.curTenantColumn || env.COST_CUR_TENANT_COLUMN || "tenant_id", "tenant column"),
|
|
32
|
+
tenantPartition: String(metadata.curTenantPartition || env.COST_CUR_TENANT_PARTITION || "").trim(),
|
|
33
|
+
connectionColumn: identifier(metadata.curConnectionColumn || env.COST_CUR_CONNECTION_COLUMN || "connection_id", "connection column"),
|
|
34
|
+
connectionPartition: String(metadata.curConnectionPartition || "").trim(),
|
|
33
35
|
maxRows: Math.min(Math.max(Number(env.COST_CUR_MAX_ROWS || 1000), 1), 5000),
|
|
34
36
|
};
|
|
35
37
|
}
|
|
@@ -82,10 +84,13 @@ function table(config) {
|
|
|
82
84
|
return `"${config.database}"."${config.table}"`;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
function
|
|
86
|
-
const partition = config.tenantPartition || tenant;
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
function sourceWhere(config, tenant) {
|
|
88
|
+
const partition = config.tenantPartition || tenant;
|
|
89
|
+
if (!config.connectionPartition) {
|
|
90
|
+
throw new Error("CUR connection partition is required for source-isolated queries");
|
|
91
|
+
}
|
|
92
|
+
return `"${config.tenantColumn}" = ${sqlString(partition)}\n AND "${config.connectionColumn}" = ${sqlString(config.connectionPartition)}`;
|
|
93
|
+
}
|
|
89
94
|
|
|
90
95
|
export async function getCurDataStatus({ client, config, tenant }) {
|
|
91
96
|
const statusRows = await execute(client, config, `
|
|
@@ -93,7 +98,7 @@ export async function getCurDataStatus({ client, config, tenant }) {
|
|
|
93
98
|
CAST(COUNT(*) AS BIGINT) AS record_count,
|
|
94
99
|
CAST(MAX(CAST(line_item_usage_start_date AS TIMESTAMP)) AS VARCHAR) AS last_data_at
|
|
95
100
|
FROM ${table(config)}
|
|
96
|
-
WHERE ${
|
|
101
|
+
WHERE ${sourceWhere(config, tenant)}
|
|
97
102
|
`);
|
|
98
103
|
const status = statusRows[0] || {};
|
|
99
104
|
return {
|
|
@@ -107,27 +112,37 @@ function rangeWhere(range) {
|
|
|
107
112
|
AND CAST(line_item_usage_start_date AS TIMESTAMP) < TIMESTAMP ${sqlString(`${range.End} 00:00:00`)}`;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
function inCondition(column, values = []) {
|
|
115
|
+
function inCondition(column, values = []) {
|
|
111
116
|
if (!values.length) return "";
|
|
112
117
|
return `AND CAST("${column}" AS VARCHAR) IN (${values.map(sqlString).join(", ")})`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function cur2TagExpression(tagKey) {
|
|
116
|
-
const key = String(tagKey || "
|
|
117
|
-
if (!key) throw new Error("Invalid CUR tag key");
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
)`;
|
|
120
135
|
|
|
121
136
|
function inExpression(expression, values = []) {
|
|
122
137
|
if (!values.length) return "";
|
|
123
138
|
return `AND CAST(${expression} AS VARCHAR) IN (${values.map(sqlString).join(", ")})`;
|
|
124
139
|
}
|
|
125
140
|
|
|
126
|
-
function filterWhere(filters = {}) {
|
|
127
|
-
const conditions = [
|
|
128
|
-
inCondition("product_product_name", filters.services),
|
|
129
|
-
|
|
130
|
-
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),
|
|
131
146
|
];
|
|
132
147
|
if (filters.tagKey && filters.tagValues?.length) {
|
|
133
148
|
conditions.push(inExpression(cur2TagExpression(filters.tagKey), filters.tagValues));
|
|
@@ -136,14 +151,15 @@ function filterWhere(filters = {}) {
|
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
function dimensionExpression(groupBy, tagKey) {
|
|
139
|
-
const dimensions = {
|
|
140
|
-
service: "product_product_name",
|
|
141
|
-
account: "line_item_usage_account_id",
|
|
142
|
-
region:
|
|
143
|
-
resource: "line_item_resource_id",
|
|
144
|
-
};
|
|
145
|
-
if (groupBy === "tag") return cur2TagExpression(tagKey);
|
|
146
|
-
|
|
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;
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
function summarize(periods, accountNames = new Map()) {
|
|
@@ -161,7 +177,7 @@ function summarize(periods, accountNames = new Map()) {
|
|
|
161
177
|
.sort((a, b) => b.amount - a.amount);
|
|
162
178
|
}
|
|
163
179
|
|
|
164
|
-
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 }) {
|
|
165
181
|
const column = dimensionExpression(groupBy, tagKey);
|
|
166
182
|
if (!column) throw new Error("Unsupported CUR report group");
|
|
167
183
|
const bucket = granularity === "DAILY" ? "day" : "month";
|
|
@@ -172,10 +188,11 @@ export async function getCurReport({ client, config, tenant, range, groupBy, gra
|
|
|
172
188
|
COALESCE(NULLIF(CAST(${column} AS VARCHAR), ''), 'Unallocated') AS item_key,
|
|
173
189
|
CAST(SUM(line_item_unblended_cost) AS DOUBLE) AS amount
|
|
174
190
|
FROM ${table(config)}
|
|
175
|
-
WHERE ${
|
|
176
|
-
AND ${rangeWhere(range)}
|
|
177
|
-
AND line_item_unblended_cost > 0
|
|
178
|
-
${
|
|
191
|
+
WHERE ${sourceWhere(config, tenant)}
|
|
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)}
|
|
179
196
|
GROUP BY 1, 2
|
|
180
197
|
ORDER BY 1, 3 DESC
|
|
181
198
|
${limit}
|
|
@@ -183,18 +200,19 @@ export async function getCurReport({ client, config, tenant, range, groupBy, gra
|
|
|
183
200
|
const rows = await execute(client, config, query);
|
|
184
201
|
const periods = rows.map((row) => ({ start: String(row.period_start).slice(0, 10), key: row.item_key || "Unallocated", amount: amount(row.amount) }));
|
|
185
202
|
let summary = summarize(periods, accountNames);
|
|
186
|
-
if (includeBreakdown
|
|
203
|
+
if (includeBreakdown) {
|
|
187
204
|
const relationQuery = `
|
|
188
205
|
SELECT
|
|
189
206
|
COALESCE(NULLIF(CAST(${column} AS VARCHAR), ''), 'Unallocated') AS item_key,
|
|
190
207
|
COALESCE(NULLIF(CAST(line_item_usage_account_id AS VARCHAR), ''), 'Unallocated') AS account_key,
|
|
191
|
-
|
|
208
|
+
${regionExpression} AS region_key,
|
|
192
209
|
CAST(SUM(line_item_unblended_cost) AS DOUBLE) AS amount
|
|
193
210
|
FROM ${table(config)}
|
|
194
|
-
WHERE ${
|
|
195
|
-
AND ${rangeWhere(range)}
|
|
196
|
-
AND line_item_unblended_cost > 0
|
|
197
|
-
${
|
|
211
|
+
WHERE ${sourceWhere(config, tenant)}
|
|
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)}
|
|
198
216
|
GROUP BY 1, 2, 3
|
|
199
217
|
`;
|
|
200
218
|
const relationRows = await execute(client, config, relationQuery);
|
|
@@ -208,7 +226,7 @@ export async function getCurReport({ client, config, tenant, range, groupBy, gra
|
|
|
208
226
|
values.set(relationKey, (values.get(relationKey) || 0) + amount(row.amount));
|
|
209
227
|
};
|
|
210
228
|
add(accountsByItem, row.account_key || "Unallocated");
|
|
211
|
-
add(regionsByItem, row.region_key || "
|
|
229
|
+
add(regionsByItem, row.region_key || "Region unavailable in CUR");
|
|
212
230
|
}
|
|
213
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);
|
|
214
232
|
summary = summary.map((item) => ({ ...item, accounts: relations(accountsByItem, item.key, "account"), regions: relations(regionsByItem, item.key, "region") }));
|
|
@@ -246,7 +264,7 @@ export async function getCurOverview({ client, config, tenant, range, trendRange
|
|
|
246
264
|
CAST(SUM(line_item_unblended_cost) AS DOUBLE) AS net_cost,
|
|
247
265
|
CAST(COUNT(DISTINCT CASE WHEN line_item_unblended_cost > 0 AND line_item_resource_id IS NOT NULL AND line_item_resource_id <> '' THEN line_item_resource_id END) AS BIGINT) AS active_resources
|
|
248
266
|
FROM ${table(config)}
|
|
249
|
-
WHERE ${
|
|
267
|
+
WHERE ${sourceWhere(config, tenant)} AND ${rangeWhere(range)}
|
|
250
268
|
`;
|
|
251
269
|
const [metricsRows, services, accounts, regions, trendReport] = await Promise.all([
|
|
252
270
|
execute(client, config, metricsQuery),
|
|
@@ -297,35 +315,44 @@ export async function getCurOverview({ client, config, tenant, range, trendRange
|
|
|
297
315
|
};
|
|
298
316
|
}
|
|
299
317
|
|
|
300
|
-
export async function getCurTags({ client, config, tenant }) {
|
|
318
|
+
export async function getCurTags({ client, config, tenant }) {
|
|
301
319
|
if (!tenant) throw new Error("A tenant is required to list CUR tags");
|
|
302
320
|
const query = `
|
|
303
321
|
SELECT DISTINCT tag_key
|
|
304
322
|
FROM ${table(config)}
|
|
305
323
|
CROSS JOIN UNNEST(map_keys(resource_tags)) AS tags(tag_key)
|
|
306
|
-
WHERE ${
|
|
324
|
+
WHERE ${sourceWhere(config, tenant)}
|
|
307
325
|
AND tag_key IS NOT NULL
|
|
308
326
|
AND TRIM(tag_key) <> ''
|
|
309
327
|
ORDER BY tag_key
|
|
310
328
|
`;
|
|
311
329
|
const rows = await execute(client, config, query);
|
|
312
|
-
return rows
|
|
313
|
-
|
|
330
|
+
return rows
|
|
331
|
+
.map((row) => String(row.tag_key || "").replace(/^(?:user:|resourceTags\/)/, ""))
|
|
332
|
+
.filter(Boolean);
|
|
333
|
+
}
|
|
314
334
|
|
|
315
335
|
export async function getCurFilterOptions({ client, config, tenant, range, tagKey, accountNames = new Map() }) {
|
|
316
336
|
const distinct = async (column) => execute(client, config, `
|
|
317
337
|
SELECT DISTINCT CAST("${column}" AS VARCHAR) AS value
|
|
318
338
|
FROM ${table(config)}
|
|
319
|
-
WHERE ${
|
|
339
|
+
WHERE ${sourceWhere(config, tenant)}
|
|
320
340
|
AND ${rangeWhere(range)}
|
|
321
341
|
AND line_item_unblended_cost > 0
|
|
322
342
|
AND "${column}" IS NOT NULL
|
|
323
343
|
AND TRIM(CAST("${column}" AS VARCHAR)) <> ''
|
|
324
344
|
ORDER BY 1
|
|
325
345
|
`);
|
|
326
|
-
const [services, regions, accounts, tags] = await Promise.all([
|
|
327
|
-
distinct("product_product_name"),
|
|
328
|
-
|
|
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
|
+
`),
|
|
329
356
|
distinct("line_item_usage_account_id"),
|
|
330
357
|
getCurTags({ client, config, tenant }),
|
|
331
358
|
]);
|
|
@@ -334,7 +361,7 @@ export async function getCurFilterOptions({ client, config, tenant, range, tagKe
|
|
|
334
361
|
tagValues = (await execute(client, config, `
|
|
335
362
|
SELECT DISTINCT CAST(${cur2TagExpression(tagKey)} AS VARCHAR) AS value
|
|
336
363
|
FROM ${table(config)}
|
|
337
|
-
WHERE ${
|
|
364
|
+
WHERE ${sourceWhere(config, tenant)}
|
|
338
365
|
AND ${rangeWhere(range)}
|
|
339
366
|
AND line_item_unblended_cost > 0
|
|
340
367
|
AND ${cur2TagExpression(tagKey)} IS NOT NULL
|
|
@@ -23,7 +23,11 @@ export class SaasAthenaContextService {
|
|
|
23
23
|
this.env.COST_CUR_TENANT_PARTITION
|
|
24
24
|
|| ingestion.tenantPartition,
|
|
25
25
|
).trim();
|
|
26
|
-
const resolvedConfig = getCurConfig(this.env, {
|
|
26
|
+
const resolvedConfig = getCurConfig(this.env, {
|
|
27
|
+
...catalogMeta,
|
|
28
|
+
curTenantPartition: tenantPartition,
|
|
29
|
+
curConnectionPartition: tenantMeta.connectionId,
|
|
30
|
+
});
|
|
27
31
|
const roleArn = String(this.env.COST_CUR_ROLE_ARN || "").trim();
|
|
28
32
|
const config = resolvedConfig;
|
|
29
33
|
const credentials = roleArn ? this.credentialProvider({
|
|
@@ -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;
|