relight-cli 0.2.0 → 0.4.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/package.json +1 -1
- package/src/cli.js +94 -49
- package/src/commands/apps.js +13 -10
- package/src/commands/auth.js +3 -63
- package/src/commands/config.js +16 -16
- package/src/commands/cost.js +6 -6
- package/src/commands/db.js +434 -190
- package/src/commands/deploy.js +38 -72
- package/src/commands/doctor.js +30 -6
- package/src/commands/domains.js +41 -24
- package/src/commands/logs.js +4 -4
- package/src/commands/open.js +4 -4
- package/src/commands/ps.js +20 -15
- package/src/commands/scale.js +4 -4
- package/src/commands/service.js +227 -0
- package/src/lib/clouds/gcp.js +97 -1
- package/src/lib/clouds/neon.js +147 -0
- package/src/lib/config.js +169 -11
- package/src/lib/link.js +13 -2
- package/src/lib/providers/aws/db.js +217 -226
- package/src/lib/providers/aws/dns.js +1 -1
- package/src/lib/providers/cf/db.js +33 -131
- package/src/lib/providers/cf/dns.js +3 -3
- package/src/lib/providers/gcp/app.js +82 -7
- package/src/lib/providers/gcp/db.js +169 -254
- package/src/lib/providers/gcp/dns.js +9 -7
- package/src/lib/providers/neon/db.js +306 -0
- package/src/lib/providers/resolve.js +33 -3
|
@@ -7,110 +7,51 @@ import {
|
|
|
7
7
|
importD1,
|
|
8
8
|
getWorkersSubdomain,
|
|
9
9
|
} from "../../clouds/cf.js";
|
|
10
|
-
import { getAppConfig, pushAppConfig } from "./app.js";
|
|
11
10
|
import { randomBytes } from "crypto";
|
|
12
11
|
|
|
13
|
-
export async function createDatabase(cfg,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!appConfig) {
|
|
17
|
-
throw new Error(`App ${appName} not found.`);
|
|
18
|
-
}
|
|
19
|
-
if (appConfig.dbId) {
|
|
20
|
-
throw new Error(`App ${appName} already has a database: ${appConfig.dbName}`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
var dbName = `relight-${appName}`;
|
|
25
|
-
var result = await createD1Database(cfg.accountId, cfg.apiToken, dbName, {
|
|
12
|
+
export async function createDatabase(cfg, name, opts = {}) {
|
|
13
|
+
var d1Name = `relight-${name}`;
|
|
14
|
+
var result = await createD1Database(cfg.accountId, cfg.apiToken, d1Name, {
|
|
26
15
|
locationHint: opts.location,
|
|
27
16
|
jurisdiction: opts.jurisdiction,
|
|
28
17
|
});
|
|
29
18
|
|
|
30
19
|
var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
|
|
31
20
|
var connectionUrl = subdomain
|
|
32
|
-
? `https://relight-${
|
|
21
|
+
? `https://relight-${name}.${subdomain}.workers.dev`
|
|
33
22
|
: null;
|
|
34
23
|
|
|
35
24
|
var dbToken = randomBytes(32).toString("hex");
|
|
36
25
|
|
|
37
|
-
if (!opts.skipAppConfig) {
|
|
38
|
-
appConfig.dbId = result.uuid;
|
|
39
|
-
appConfig.dbName = dbName;
|
|
40
|
-
|
|
41
|
-
if (!appConfig.envKeys) appConfig.envKeys = [];
|
|
42
|
-
if (!appConfig.secretKeys) appConfig.secretKeys = [];
|
|
43
|
-
if (!appConfig.env) appConfig.env = {};
|
|
44
|
-
|
|
45
|
-
if (connectionUrl) {
|
|
46
|
-
appConfig.env["DB_URL"] = connectionUrl;
|
|
47
|
-
if (!appConfig.envKeys.includes("DB_URL")) appConfig.envKeys.push("DB_URL");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
appConfig.env["DB_TOKEN"] = "[hidden]";
|
|
51
|
-
appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
|
|
52
|
-
appConfig.secretKeys.push("DB_TOKEN");
|
|
53
|
-
appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
|
|
54
|
-
|
|
55
|
-
var newSecrets = { DB_TOKEN: dbToken };
|
|
56
|
-
|
|
57
|
-
await pushAppConfig(cfg, appName, appConfig, { newSecrets });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
26
|
return {
|
|
61
27
|
dbId: result.uuid,
|
|
62
|
-
dbName,
|
|
28
|
+
dbName: d1Name,
|
|
63
29
|
dbToken,
|
|
64
30
|
connectionUrl,
|
|
65
31
|
};
|
|
66
32
|
}
|
|
67
33
|
|
|
68
|
-
export async function destroyDatabase(cfg,
|
|
69
|
-
var dbId = opts.dbId;
|
|
70
|
-
if (!dbId) {
|
|
71
|
-
var appConfig = await getAppConfig(cfg, appName);
|
|
72
|
-
if (!appConfig || !appConfig.dbId) {
|
|
73
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
74
|
-
}
|
|
75
|
-
dbId = appConfig.dbId;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
await deleteD1Database(cfg.accountId, cfg.apiToken, dbId);
|
|
79
|
-
|
|
34
|
+
export async function destroyDatabase(cfg, name, opts = {}) {
|
|
80
35
|
if (!opts.dbId) {
|
|
81
|
-
|
|
82
|
-
delete appConfig.dbName;
|
|
83
|
-
|
|
84
|
-
if (appConfig.env) {
|
|
85
|
-
delete appConfig.env["DB_URL"];
|
|
86
|
-
delete appConfig.env["DB_TOKEN"];
|
|
87
|
-
}
|
|
88
|
-
if (appConfig.envKeys) appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_URL");
|
|
89
|
-
if (appConfig.secretKeys) appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
|
|
90
|
-
|
|
91
|
-
await pushAppConfig(cfg, appName, appConfig);
|
|
36
|
+
throw new Error("dbId is required to destroy a CF database.");
|
|
92
37
|
}
|
|
38
|
+
await deleteD1Database(cfg.accountId, cfg.apiToken, opts.dbId);
|
|
93
39
|
}
|
|
94
40
|
|
|
95
|
-
export async function getDatabaseInfo(cfg,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
var appConfig = await getAppConfig(cfg, appName);
|
|
99
|
-
if (!appConfig || !appConfig.dbId) {
|
|
100
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
101
|
-
}
|
|
102
|
-
dbId = appConfig.dbId;
|
|
41
|
+
export async function getDatabaseInfo(cfg, name, opts = {}) {
|
|
42
|
+
if (!opts.dbId) {
|
|
43
|
+
throw new Error("dbId is required to get CF database info.");
|
|
103
44
|
}
|
|
104
45
|
|
|
105
|
-
var dbDetails = await getD1Database(cfg.accountId, cfg.apiToken, dbId);
|
|
46
|
+
var dbDetails = await getD1Database(cfg.accountId, cfg.apiToken, opts.dbId);
|
|
106
47
|
var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
|
|
107
48
|
var connectionUrl = subdomain
|
|
108
|
-
? `https://relight-${
|
|
49
|
+
? `https://relight-${name}.${subdomain}.workers.dev`
|
|
109
50
|
: null;
|
|
110
51
|
|
|
111
52
|
return {
|
|
112
|
-
dbId,
|
|
113
|
-
dbName: dbDetails.name || `relight-${
|
|
53
|
+
dbId: opts.dbId,
|
|
54
|
+
dbName: dbDetails.name || `relight-${name}`,
|
|
114
55
|
connectionUrl,
|
|
115
56
|
size: dbDetails.file_size,
|
|
116
57
|
numTables: dbDetails.num_tables,
|
|
@@ -118,27 +59,18 @@ export async function getDatabaseInfo(cfg, appName, opts = {}) {
|
|
|
118
59
|
};
|
|
119
60
|
}
|
|
120
61
|
|
|
121
|
-
export async function queryDatabase(cfg,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
var appConfig = await getAppConfig(cfg, appName);
|
|
125
|
-
if (!appConfig || !appConfig.dbId) {
|
|
126
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
127
|
-
}
|
|
128
|
-
dbId = appConfig.dbId;
|
|
62
|
+
export async function queryDatabase(cfg, name, sql, params, opts = {}) {
|
|
63
|
+
if (!opts.dbId) {
|
|
64
|
+
throw new Error("dbId is required to query a CF database.");
|
|
129
65
|
}
|
|
130
|
-
return queryD1(cfg.accountId, cfg.apiToken, dbId, sql, params);
|
|
66
|
+
return queryD1(cfg.accountId, cfg.apiToken, opts.dbId, sql, params);
|
|
131
67
|
}
|
|
132
68
|
|
|
133
|
-
export async function importDatabase(cfg,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
var appConfig = await getAppConfig(cfg, appName);
|
|
137
|
-
if (!appConfig || !appConfig.dbId) {
|
|
138
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
139
|
-
}
|
|
140
|
-
dbId = appConfig.dbId;
|
|
69
|
+
export async function importDatabase(cfg, name, sqlContent, opts = {}) {
|
|
70
|
+
if (!opts.dbId) {
|
|
71
|
+
throw new Error("dbId is required to import into a CF database.");
|
|
141
72
|
}
|
|
73
|
+
var dbId = opts.dbId;
|
|
142
74
|
|
|
143
75
|
// Step 1: Init import
|
|
144
76
|
var initRes = await importD1(cfg.accountId, cfg.apiToken, dbId, {
|
|
@@ -183,15 +115,11 @@ export async function importDatabase(cfg, appName, sqlContent, opts = {}) {
|
|
|
183
115
|
}
|
|
184
116
|
}
|
|
185
117
|
|
|
186
|
-
export async function exportDatabase(cfg,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
var appConfig = await getAppConfig(cfg, appName);
|
|
190
|
-
if (!appConfig || !appConfig.dbId) {
|
|
191
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
192
|
-
}
|
|
193
|
-
dbId = appConfig.dbId;
|
|
118
|
+
export async function exportDatabase(cfg, name, opts = {}) {
|
|
119
|
+
if (!opts.dbId) {
|
|
120
|
+
throw new Error("dbId is required to export a CF database.");
|
|
194
121
|
}
|
|
122
|
+
var dbId = opts.dbId;
|
|
195
123
|
|
|
196
124
|
var exportRes = await exportD1(cfg.accountId, cfg.apiToken, dbId, {
|
|
197
125
|
output_format: "polling",
|
|
@@ -221,48 +149,22 @@ export async function exportDatabase(cfg, appName, opts = {}) {
|
|
|
221
149
|
return dumpRes.text();
|
|
222
150
|
}
|
|
223
151
|
|
|
224
|
-
export async function rotateToken(cfg,
|
|
152
|
+
export async function rotateToken(cfg, name, opts = {}) {
|
|
225
153
|
var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
|
|
226
154
|
var connectionUrl = subdomain
|
|
227
|
-
? `https://relight-${
|
|
155
|
+
? `https://relight-${name}.${subdomain}.workers.dev`
|
|
228
156
|
: null;
|
|
229
157
|
|
|
230
158
|
var dbToken = randomBytes(32).toString("hex");
|
|
231
159
|
|
|
232
|
-
if (!opts.skipAppConfig) {
|
|
233
|
-
var appConfig = await getAppConfig(cfg, appName);
|
|
234
|
-
if (!appConfig || !appConfig.dbId) {
|
|
235
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (!appConfig.envKeys) appConfig.envKeys = [];
|
|
239
|
-
if (!appConfig.secretKeys) appConfig.secretKeys = [];
|
|
240
|
-
if (!appConfig.env) appConfig.env = {};
|
|
241
|
-
|
|
242
|
-
appConfig.env["DB_TOKEN"] = "[hidden]";
|
|
243
|
-
if (!appConfig.secretKeys.includes("DB_TOKEN")) appConfig.secretKeys.push("DB_TOKEN");
|
|
244
|
-
appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
|
|
245
|
-
|
|
246
|
-
if (connectionUrl) {
|
|
247
|
-
appConfig.env["DB_URL"] = connectionUrl;
|
|
248
|
-
if (!appConfig.envKeys.includes("DB_URL")) appConfig.envKeys.push("DB_URL");
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
await pushAppConfig(cfg, appName, appConfig, { newSecrets: { DB_TOKEN: dbToken } });
|
|
252
|
-
}
|
|
253
|
-
|
|
254
160
|
return { dbToken, connectionUrl };
|
|
255
161
|
}
|
|
256
162
|
|
|
257
|
-
export async function resetDatabase(cfg,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
var appConfig = await getAppConfig(cfg, appName);
|
|
261
|
-
if (!appConfig || !appConfig.dbId) {
|
|
262
|
-
throw new Error(`App ${appName} does not have a database.`);
|
|
263
|
-
}
|
|
264
|
-
dbId = appConfig.dbId;
|
|
163
|
+
export async function resetDatabase(cfg, name, opts = {}) {
|
|
164
|
+
if (!opts.dbId) {
|
|
165
|
+
throw new Error("dbId is required to reset a CF database.");
|
|
265
166
|
}
|
|
167
|
+
var dbId = opts.dbId;
|
|
266
168
|
|
|
267
169
|
var results = await queryD1(
|
|
268
170
|
cfg.accountId, cfg.apiToken, dbId,
|
|
@@ -107,7 +107,7 @@ export async function removeDomain(cfg, appName, domain) {
|
|
|
107
107
|
|
|
108
108
|
// --- Pure DNS record operations (for cross-cloud use) ---
|
|
109
109
|
|
|
110
|
-
export async function addDnsRecord(cfg, domain, target, zone) {
|
|
110
|
+
export async function addDnsRecord(cfg, domain, target, zone, opts = {}) {
|
|
111
111
|
// Check for existing records
|
|
112
112
|
var existing = await listDnsRecords(cfg.accountId, cfg.apiToken, zone.id, { name: domain });
|
|
113
113
|
if (existing.length > 0) {
|
|
@@ -117,12 +117,12 @@ export async function addDnsRecord(cfg, domain, target, zone) {
|
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
var proxied = opts.proxied !== undefined ? opts.proxied : true;
|
|
121
121
|
await createDnsRecord(cfg.accountId, cfg.apiToken, zone.id, {
|
|
122
122
|
type: "CNAME",
|
|
123
123
|
name: domain,
|
|
124
124
|
content: target,
|
|
125
|
-
proxied
|
|
125
|
+
proxied,
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -9,6 +9,13 @@ import {
|
|
|
9
9
|
listLogEntries,
|
|
10
10
|
queryTimeSeries,
|
|
11
11
|
deleteSqlInstance,
|
|
12
|
+
ensureFirebaseProject,
|
|
13
|
+
createHostingSite,
|
|
14
|
+
getHostingSite,
|
|
15
|
+
deleteHostingSite,
|
|
16
|
+
deployHostingProxy,
|
|
17
|
+
addHostingCustomDomain,
|
|
18
|
+
deleteHostingCustomDomain,
|
|
12
19
|
} from "../../clouds/gcp.js";
|
|
13
20
|
|
|
14
21
|
// --- Helpers ---
|
|
@@ -199,8 +206,14 @@ export async function getAppInfo(cfg, appName) {
|
|
|
199
206
|
var svc = await findService(token, cfg.project, appName);
|
|
200
207
|
if (!svc) return null;
|
|
201
208
|
|
|
209
|
+
var region = parseRegionFromName(svc.name);
|
|
210
|
+
var svcId = svc.name.split("/").pop();
|
|
202
211
|
var appConfig = await getAppConfig(cfg, appName);
|
|
203
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
appConfig,
|
|
214
|
+
url: svc.uri || null,
|
|
215
|
+
consoleUrl: `https://console.cloud.google.com/run/detail/${region}/${svcId}?project=${cfg.project}`,
|
|
216
|
+
};
|
|
204
217
|
}
|
|
205
218
|
|
|
206
219
|
// --- Destroy ---
|
|
@@ -216,6 +229,11 @@ export async function destroyApp(cfg, appName) {
|
|
|
216
229
|
} catch {}
|
|
217
230
|
}
|
|
218
231
|
|
|
232
|
+
// Delete Firebase Hosting site if exists
|
|
233
|
+
try {
|
|
234
|
+
await deleteHostingSite(token, cfg.project, hostingSiteId(appName));
|
|
235
|
+
} catch {}
|
|
236
|
+
|
|
219
237
|
// Delete Cloud Run service
|
|
220
238
|
var svc = await findService(token, cfg.project, appName);
|
|
221
239
|
if (!svc) throw new Error(`Service relight-${appName} not found.`);
|
|
@@ -275,6 +293,48 @@ export async function getAppUrl(cfg, appName) {
|
|
|
275
293
|
return svc?.uri || null;
|
|
276
294
|
}
|
|
277
295
|
|
|
296
|
+
// --- Custom domain via Firebase Hosting ---
|
|
297
|
+
|
|
298
|
+
function hostingSiteId(appName) {
|
|
299
|
+
return `relight-${appName}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function ensureHostingSite(token, project, appName) {
|
|
303
|
+
var siteId = hostingSiteId(appName);
|
|
304
|
+
try {
|
|
305
|
+
await getHostingSite(token, project, siteId);
|
|
306
|
+
} catch {
|
|
307
|
+
await ensureFirebaseProject(token, project);
|
|
308
|
+
await createHostingSite(token, project, siteId);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Deploy proxy config pointing to Cloud Run
|
|
312
|
+
var svc = await findService(token, project, appName);
|
|
313
|
+
if (!svc) throw new Error(`Service relight-${appName} not found.`);
|
|
314
|
+
var region = parseRegionFromName(svc.name);
|
|
315
|
+
var svcId = svc.name.split("/").pop();
|
|
316
|
+
await deployHostingProxy(token, siteId, svcId, region);
|
|
317
|
+
|
|
318
|
+
return siteId;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function mapCustomDomain(cfg, appName, domain) {
|
|
322
|
+
var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
|
|
323
|
+
var siteId = await ensureHostingSite(token, cfg.project, appName);
|
|
324
|
+
await addHostingCustomDomain(token, cfg.project, siteId, domain);
|
|
325
|
+
return { dnsTarget: `${siteId}.web.app`, proxied: false };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function unmapCustomDomain(cfg, appName, domain) {
|
|
329
|
+
var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
|
|
330
|
+
var siteId = hostingSiteId(appName);
|
|
331
|
+
try {
|
|
332
|
+
await deleteHostingCustomDomain(token, cfg.project, siteId, domain);
|
|
333
|
+
} catch (e) {
|
|
334
|
+
if (!e.message.includes("404")) throw e;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
278
338
|
// --- Log streaming ---
|
|
279
339
|
|
|
280
340
|
export async function streamLogs(cfg, appName) {
|
|
@@ -328,6 +388,10 @@ export async function getCosts(cfg, appNames, dateRange) {
|
|
|
328
388
|
var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
|
|
329
389
|
var { sinceISO, untilISO } = dateRange;
|
|
330
390
|
|
|
391
|
+
// MQL datetime format: YYYY/MM/DD-HH:MM:SS (not ISO 8601)
|
|
392
|
+
var sinceMql = isoToMql(sinceISO);
|
|
393
|
+
var untilMql = isoToMql(untilISO);
|
|
394
|
+
|
|
331
395
|
// Discover apps
|
|
332
396
|
var apps;
|
|
333
397
|
if (appNames) {
|
|
@@ -353,7 +417,7 @@ export async function getCosts(cfg, appNames, dateRange) {
|
|
|
353
417
|
query: `fetch cloud_run_revision
|
|
354
418
|
| metric 'run.googleapis.com/request_count'
|
|
355
419
|
| filter resource.service_name == '${app.serviceName}'
|
|
356
|
-
| within d'${
|
|
420
|
+
| within d'${sinceMql}', d'${untilMql}'
|
|
357
421
|
| group_by [], sum(val())`,
|
|
358
422
|
});
|
|
359
423
|
var reqData = reqRes.timeSeriesData || [];
|
|
@@ -362,7 +426,9 @@ export async function getCosts(cfg, appNames, dateRange) {
|
|
|
362
426
|
usage.requests += Number(pt.values?.[0]?.int64Value || 0);
|
|
363
427
|
}
|
|
364
428
|
}
|
|
365
|
-
} catch {
|
|
429
|
+
} catch (e) {
|
|
430
|
+
process.stderr.write(` Warning: failed to fetch request metrics: ${e.message}\n`);
|
|
431
|
+
}
|
|
366
432
|
|
|
367
433
|
try {
|
|
368
434
|
// CPU allocation
|
|
@@ -370,7 +436,7 @@ export async function getCosts(cfg, appNames, dateRange) {
|
|
|
370
436
|
query: `fetch cloud_run_revision
|
|
371
437
|
| metric 'run.googleapis.com/container/cpu/allocation_time'
|
|
372
438
|
| filter resource.service_name == '${app.serviceName}'
|
|
373
|
-
| within d'${
|
|
439
|
+
| within d'${sinceMql}', d'${untilMql}'
|
|
374
440
|
| group_by [], sum(val())`,
|
|
375
441
|
});
|
|
376
442
|
var cpuData = cpuRes.timeSeriesData || [];
|
|
@@ -379,7 +445,9 @@ export async function getCosts(cfg, appNames, dateRange) {
|
|
|
379
445
|
usage.cpuSeconds += Number(pt.values?.[0]?.doubleValue || 0);
|
|
380
446
|
}
|
|
381
447
|
}
|
|
382
|
-
} catch {
|
|
448
|
+
} catch (e) {
|
|
449
|
+
process.stderr.write(` Warning: failed to fetch CPU metrics: ${e.message}\n`);
|
|
450
|
+
}
|
|
383
451
|
|
|
384
452
|
try {
|
|
385
453
|
// Memory allocation
|
|
@@ -387,7 +455,7 @@ export async function getCosts(cfg, appNames, dateRange) {
|
|
|
387
455
|
query: `fetch cloud_run_revision
|
|
388
456
|
| metric 'run.googleapis.com/container/memory/allocation_time'
|
|
389
457
|
| filter resource.service_name == '${app.serviceName}'
|
|
390
|
-
| within d'${
|
|
458
|
+
| within d'${sinceMql}', d'${untilMql}'
|
|
391
459
|
| group_by [], sum(val())`,
|
|
392
460
|
});
|
|
393
461
|
var memData = memRes.timeSeriesData || [];
|
|
@@ -397,7 +465,9 @@ export async function getCosts(cfg, appNames, dateRange) {
|
|
|
397
465
|
usage.memGibSeconds += Number(pt.values?.[0]?.doubleValue || 0);
|
|
398
466
|
}
|
|
399
467
|
}
|
|
400
|
-
} catch {
|
|
468
|
+
} catch (e) {
|
|
469
|
+
process.stderr.write(` Warning: failed to fetch memory metrics: ${e.message}\n`);
|
|
470
|
+
}
|
|
401
471
|
|
|
402
472
|
results.push({ name: app.name, usage });
|
|
403
473
|
}
|
|
@@ -405,6 +475,11 @@ export async function getCosts(cfg, appNames, dateRange) {
|
|
|
405
475
|
return results;
|
|
406
476
|
}
|
|
407
477
|
|
|
478
|
+
function isoToMql(iso) {
|
|
479
|
+
// Convert 2026-03-01T00:00:00Z -> 2026/03/01-00:00:00
|
|
480
|
+
return iso.replace(/Z$/, "").replace(/^(\d{4})-(\d{2})-(\d{2})T/, "$1/$2/$3-");
|
|
481
|
+
}
|
|
482
|
+
|
|
408
483
|
// --- Regions ---
|
|
409
484
|
|
|
410
485
|
export function getRegions() {
|