@everystack/cli 0.1.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 +255 -0
- package/package.json +104 -0
- package/src/cli/aws.ts +121 -0
- package/src/cli/commands/analyze.ts +61 -0
- package/src/cli/commands/branches.ts +97 -0
- package/src/cli/commands/cache.ts +72 -0
- package/src/cli/commands/certs.ts +117 -0
- package/src/cli/commands/channels.ts +109 -0
- package/src/cli/commands/console.ts +68 -0
- package/src/cli/commands/db.ts +183 -0
- package/src/cli/commands/diag.ts +242 -0
- package/src/cli/commands/logs.ts +282 -0
- package/src/cli/commands/update.ts +432 -0
- package/src/cli/config.ts +98 -0
- package/src/cli/discover.ts +321 -0
- package/src/cli/hydration-analyzer.ts +224 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/output.ts +25 -0
- package/src/cli/ssr-analyzer.ts +445 -0
- package/src/cli/utils/export.ts +8 -0
- package/src/cli/utils/table.ts +39 -0
- package/src/cli/utils/upload.ts +52 -0
- package/src/cli/utils/walk.ts +59 -0
- package/src/client/app-state-provider.tsx +83 -0
- package/src/client/index.ts +2 -0
- package/src/client/updates-provider.tsx +69 -0
- package/src/handler/assets.ts +30 -0
- package/src/handler/branches.ts +70 -0
- package/src/handler/channels-crud.ts +174 -0
- package/src/handler/helpers.ts +239 -0
- package/src/handler/index.ts +78 -0
- package/src/handler/manifest.ts +276 -0
- package/src/handler/multipart.ts +74 -0
- package/src/handler/publish-web.ts +311 -0
- package/src/handler/publish.ts +346 -0
- package/src/handler/signing.ts +29 -0
- package/src/handler/types.ts +16 -0
- package/src/index.ts +4 -0
- package/src/schema.ts +245 -0
- package/src/storage/filesystem.ts +103 -0
- package/src/storage/index.ts +27 -0
- package/src/storage/s3.ts +125 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diag — diagnose deployed page freshness and hydration issues.
|
|
3
|
+
*
|
|
4
|
+
* Fetches the deployed page via HEAD, reads the latest channel manifest
|
|
5
|
+
* from S3, and compares. Reports CDN cache status, release version,
|
|
6
|
+
* and whether the served content matches the latest OTA update.
|
|
7
|
+
*
|
|
8
|
+
* With --hydration flag, fetches full HTML and analyzes for common
|
|
9
|
+
* SSR hydration issues: type mismatches, timezone problems, relative time.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* everystack diag [url] [--path /about] [--channel production] [--stage dev]
|
|
13
|
+
* everystack diag [url] --hydration # Analyze SSR hydration issues
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { resolveConfig } from '../config.js';
|
|
17
|
+
import { getFromS3 } from '../aws.js';
|
|
18
|
+
import { step, success, fail, info, warn } from '../output.js';
|
|
19
|
+
import { analyzeHtml, generateHydrationReport } from '../hydration-analyzer.js';
|
|
20
|
+
|
|
21
|
+
interface DiagResult {
|
|
22
|
+
url: string;
|
|
23
|
+
status: number;
|
|
24
|
+
statusText: string;
|
|
25
|
+
cacheStatus?: string;
|
|
26
|
+
cacheAge?: string;
|
|
27
|
+
cacheControl?: string;
|
|
28
|
+
bundleKey?: string;
|
|
29
|
+
updateId?: string;
|
|
30
|
+
runtimeVersion?: string;
|
|
31
|
+
releaseCreated?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Manifest {
|
|
35
|
+
updateId?: string;
|
|
36
|
+
runtimeVersion?: string;
|
|
37
|
+
createdAt?: string;
|
|
38
|
+
storagePrefix?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extractStoragePrefix(bundleKey: string): string | null {
|
|
42
|
+
// Bundle key format: releases/{branch}/{version}/{groupId}/{platform}/bundle.tar.br
|
|
43
|
+
// Storage prefix: releases/{branch}/{version}/{groupId}
|
|
44
|
+
const match = bundleKey.match(/^(.+)\/web\/bundle\.tar\.br$/);
|
|
45
|
+
return match?.[1] ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function diagCommand(
|
|
49
|
+
positionalUrl: string | undefined,
|
|
50
|
+
flags: Record<string, string>,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
const channel = flags.channel || 'production';
|
|
53
|
+
|
|
54
|
+
step('Resolving deployed config...');
|
|
55
|
+
let config;
|
|
56
|
+
try {
|
|
57
|
+
config = await resolveConfig(flags.stage);
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
fail(err.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build the URL: positional arg > baseUrl + --path
|
|
64
|
+
let url = positionalUrl;
|
|
65
|
+
if (!url) {
|
|
66
|
+
if (!config.baseUrl) {
|
|
67
|
+
fail('No URL provided and no baseUrl in config. Pass a URL or use --stage.');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
url = config.baseUrl;
|
|
71
|
+
}
|
|
72
|
+
// Strip trailing slash from base URL before appending path
|
|
73
|
+
url = url.replace(/\/+$/, '');
|
|
74
|
+
if (flags.path) {
|
|
75
|
+
const p = flags.path.startsWith('/') ? flags.path : '/' + flags.path;
|
|
76
|
+
url = url + p;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 1. Fetch the deployed page
|
|
80
|
+
const checkHydration = flags.hydration === 'true' || flags.hydration === '1';
|
|
81
|
+
const method = checkHydration ? 'GET' : 'HEAD';
|
|
82
|
+
|
|
83
|
+
step(`Fetching ${url}...`);
|
|
84
|
+
let diag: DiagResult;
|
|
85
|
+
let html: string | undefined;
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch(url, {
|
|
88
|
+
method,
|
|
89
|
+
redirect: 'follow',
|
|
90
|
+
});
|
|
91
|
+
diag = {
|
|
92
|
+
url,
|
|
93
|
+
status: response.status,
|
|
94
|
+
statusText: response.statusText,
|
|
95
|
+
cacheStatus: response.headers.get('x-cache') ?? undefined,
|
|
96
|
+
cacheAge: response.headers.get('age') ?? undefined,
|
|
97
|
+
cacheControl: response.headers.get('cache-control') ?? undefined,
|
|
98
|
+
bundleKey: response.headers.get('x-bundle-key') ?? undefined,
|
|
99
|
+
updateId: response.headers.get('x-update-id') ?? undefined,
|
|
100
|
+
runtimeVersion: response.headers.get('x-runtime-version') ?? undefined,
|
|
101
|
+
releaseCreated: response.headers.get('x-release-created') ?? undefined,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Fetch full HTML if hydration analysis requested
|
|
105
|
+
if (checkHydration) {
|
|
106
|
+
html = await response.text();
|
|
107
|
+
}
|
|
108
|
+
} catch (err: any) {
|
|
109
|
+
fail(`Failed to fetch ${url}: ${err.message}`);
|
|
110
|
+
info('Check the URL is correct and includes the protocol (https://).');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 2. Fetch the latest manifest from S3
|
|
115
|
+
step(`Reading latest ${channel} manifest from S3...`);
|
|
116
|
+
let manifest: Manifest | null = null;
|
|
117
|
+
try {
|
|
118
|
+
const manifestKey = `${channel}/web/manifest.json`;
|
|
119
|
+
const data = await getFromS3(config.region, config.updatesBucket, manifestKey);
|
|
120
|
+
if (data) {
|
|
121
|
+
manifest = JSON.parse(data.toString('utf8'));
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Non-fatal — report what we have
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. Compare and report
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(' Diagnostic Report');
|
|
130
|
+
console.log(' ============================================================');
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log(` URL: ${diag.url}`);
|
|
133
|
+
console.log(` Status: ${diag.status} ${diag.statusText}`);
|
|
134
|
+
console.log('');
|
|
135
|
+
|
|
136
|
+
// CDN info
|
|
137
|
+
if (diag.cacheStatus) {
|
|
138
|
+
console.log(` CDN Cache: ${diag.cacheStatus}`);
|
|
139
|
+
}
|
|
140
|
+
if (diag.cacheAge) {
|
|
141
|
+
console.log(` Cache Age: ${diag.cacheAge}s`);
|
|
142
|
+
}
|
|
143
|
+
if (diag.cacheControl) {
|
|
144
|
+
console.log(` Cache-Control: ${diag.cacheControl}`);
|
|
145
|
+
}
|
|
146
|
+
if (diag.cacheStatus || diag.cacheAge || diag.cacheControl) {
|
|
147
|
+
console.log('');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Served release info
|
|
151
|
+
if (diag.bundleKey) {
|
|
152
|
+
console.log(` Bundle Key: ${diag.bundleKey}`);
|
|
153
|
+
}
|
|
154
|
+
if (diag.updateId) {
|
|
155
|
+
console.log(` Update ID: ${diag.updateId}`);
|
|
156
|
+
}
|
|
157
|
+
if (diag.runtimeVersion) {
|
|
158
|
+
console.log(` Runtime Version: ${diag.runtimeVersion}`);
|
|
159
|
+
}
|
|
160
|
+
if (diag.releaseCreated) {
|
|
161
|
+
console.log(` Release Created: ${diag.releaseCreated}`);
|
|
162
|
+
}
|
|
163
|
+
if (!diag.bundleKey && !diag.updateId) {
|
|
164
|
+
warn(' No release headers found. Update @everystack/server to get version headers on SSR responses.');
|
|
165
|
+
}
|
|
166
|
+
console.log('');
|
|
167
|
+
|
|
168
|
+
// Latest manifest info
|
|
169
|
+
if (manifest) {
|
|
170
|
+
console.log(` Latest (${channel}):`);
|
|
171
|
+
if (manifest.updateId) {
|
|
172
|
+
console.log(` Update ID: ${manifest.updateId}`);
|
|
173
|
+
}
|
|
174
|
+
if (manifest.runtimeVersion) {
|
|
175
|
+
console.log(` Runtime Ver: ${manifest.runtimeVersion}`);
|
|
176
|
+
}
|
|
177
|
+
if (manifest.createdAt) {
|
|
178
|
+
console.log(` Created At: ${manifest.createdAt}`);
|
|
179
|
+
}
|
|
180
|
+
if (manifest.storagePrefix) {
|
|
181
|
+
console.log(` Storage Prefix: ${manifest.storagePrefix}`);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
warn(` No manifest found in S3 for channel "${channel}".`);
|
|
185
|
+
info(' Run `everystack update` to publish a release first.');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log(' ------------------------------------------------------------');
|
|
190
|
+
|
|
191
|
+
// 4. Version comparison
|
|
192
|
+
if (!manifest) {
|
|
193
|
+
warn(' Cannot compare: no manifest available.');
|
|
194
|
+
} else if (diag.updateId && manifest.updateId) {
|
|
195
|
+
// Primary comparison: updateId
|
|
196
|
+
if (diag.updateId === manifest.updateId) {
|
|
197
|
+
success(' Version match: serving the latest release');
|
|
198
|
+
} else {
|
|
199
|
+
fail(` Version mismatch: serving ${diag.updateId}, latest is ${manifest.updateId}`);
|
|
200
|
+
info(' Run `everystack cache:purge --origin web` to bust the CDN cache.');
|
|
201
|
+
}
|
|
202
|
+
} else if (diag.bundleKey && manifest.storagePrefix) {
|
|
203
|
+
// Fallback comparison: storagePrefix from bundle key
|
|
204
|
+
const servedPrefix = extractStoragePrefix(diag.bundleKey);
|
|
205
|
+
if (servedPrefix && servedPrefix === manifest.storagePrefix) {
|
|
206
|
+
success(' Version match: serving the latest release (matched via bundle key)');
|
|
207
|
+
} else if (servedPrefix) {
|
|
208
|
+
fail(` Version mismatch: serving ${servedPrefix}, latest is ${manifest.storagePrefix}`);
|
|
209
|
+
info(' Run `everystack cache:purge --origin web` to bust the CDN cache.');
|
|
210
|
+
} else {
|
|
211
|
+
warn(' Cannot determine served version from bundle key.');
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
warn(' Cannot compare: missing version headers. Update @everystack/server to emit release metadata.');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log('');
|
|
218
|
+
|
|
219
|
+
// 5. Hydration analysis (if requested)
|
|
220
|
+
if (html) {
|
|
221
|
+
console.log(' ------------------------------------------------------------');
|
|
222
|
+
console.log('');
|
|
223
|
+
step('Analyzing SSR hydration...');
|
|
224
|
+
|
|
225
|
+
const cacheAge = diag.cacheAge ? parseInt(diag.cacheAge, 10) : undefined;
|
|
226
|
+
const analysis = analyzeHtml(html, cacheAge);
|
|
227
|
+
const report = generateHydrationReport(analysis);
|
|
228
|
+
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(report);
|
|
231
|
+
console.log('');
|
|
232
|
+
|
|
233
|
+
if (analysis.issues.length > 0) {
|
|
234
|
+
info(' Use these findings to identify type mismatches, timezone issues, and');
|
|
235
|
+
info(' relative time rendering that may cause hydration errors in production.');
|
|
236
|
+
}
|
|
237
|
+
console.log('');
|
|
238
|
+
} else if (checkHydration) {
|
|
239
|
+
info(' Hydration analysis skipped: no HTML content received.');
|
|
240
|
+
console.log('');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logs commands — query application logs and tail CloudWatch logs.
|
|
3
|
+
*
|
|
4
|
+
* These commands help debug production issues by accessing:
|
|
5
|
+
* - Application logs stored in the database (logs:errors)
|
|
6
|
+
* - Lambda container logs from CloudWatch (logs:tail)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resolveConfig } from '../config.js';
|
|
10
|
+
import { invokeAction } from '../aws.js';
|
|
11
|
+
import { step, success, fail, info } from '../output.js';
|
|
12
|
+
|
|
13
|
+
export async function logsErrorsCommand(flags: Record<string, string>): Promise<void> {
|
|
14
|
+
step('Resolving deployed config...');
|
|
15
|
+
let config;
|
|
16
|
+
try {
|
|
17
|
+
config = await resolveConfig(flags.stage);
|
|
18
|
+
} catch (err: any) {
|
|
19
|
+
fail(err.message);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
info(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
|
|
24
|
+
|
|
25
|
+
const limit = flags.limit ? parseInt(flags.limit, 10) : 20;
|
|
26
|
+
const source = flags.source;
|
|
27
|
+
const level = flags.level || 'error'; // error or fatal
|
|
28
|
+
|
|
29
|
+
step(`Fetching last ${limit} ${level} logs...`);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const result: any = await invokeAction(
|
|
33
|
+
config.region,
|
|
34
|
+
config.apiFunctionName,
|
|
35
|
+
'logs:errors',
|
|
36
|
+
{ limit, source, level },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (result?.error) {
|
|
40
|
+
fail(`Failed to fetch errors: ${result.error}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const errors = result.errors || [];
|
|
45
|
+
|
|
46
|
+
if (errors.length === 0) {
|
|
47
|
+
success('No errors found!');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sourceLabel = result.source === 's3' ? ' (s3)' : '';
|
|
52
|
+
success(`Found ${errors.length} ${level} log(s)${sourceLabel}:\n`);
|
|
53
|
+
|
|
54
|
+
// Format and display errors
|
|
55
|
+
for (const error of errors) {
|
|
56
|
+
console.log(`[${error.timestamp}] ${error.level.toUpperCase()}`);
|
|
57
|
+
console.log(`Source: ${error.source || 'unknown'}`);
|
|
58
|
+
console.log(`Message: ${error.message}`);
|
|
59
|
+
|
|
60
|
+
if (error.traceId) {
|
|
61
|
+
console.log(`TraceID: ${error.traceId}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (error.userId) {
|
|
65
|
+
console.log(`UserID: ${error.userId}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (error.error) {
|
|
69
|
+
console.log(`Error: ${error.error.name}: ${error.error.message}`);
|
|
70
|
+
if (error.error.stack) {
|
|
71
|
+
console.log(`Stack:\n${error.error.stack.split('\n').slice(0, 5).join('\n')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (error.data) {
|
|
76
|
+
console.log(`Data: ${JSON.stringify(error.data, null, 2)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log('---\n');
|
|
80
|
+
}
|
|
81
|
+
} catch (err: any) {
|
|
82
|
+
fail(`Failed to fetch errors: ${err.message}`);
|
|
83
|
+
info('Ensure your IAM user/role has lambda:InvokeFunction permission.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function logsTailCommand(flags: Record<string, string>): Promise<void> {
|
|
89
|
+
step('Resolving deployed config...');
|
|
90
|
+
let config;
|
|
91
|
+
try {
|
|
92
|
+
config = await resolveConfig(flags.stage);
|
|
93
|
+
} catch (err: any) {
|
|
94
|
+
fail(err.message);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
info(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
|
|
99
|
+
|
|
100
|
+
const filterPattern = flags.filter;
|
|
101
|
+
const since = flags.since || '5m'; // 5m, 1h, 1d
|
|
102
|
+
|
|
103
|
+
step(`Tailing logs (filter: ${filterPattern || 'none'}, since: ${since})...`);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Import AWS SDK dynamically
|
|
107
|
+
const { CloudWatchLogsClient, FilterLogEventsCommand, DescribeLogStreamsCommand } =
|
|
108
|
+
await import('@aws-sdk/client-cloudwatch-logs');
|
|
109
|
+
|
|
110
|
+
const client = new CloudWatchLogsClient({ region: config.region });
|
|
111
|
+
|
|
112
|
+
// Determine log group name from function name
|
|
113
|
+
const logGroupName = `/aws/lambda/${config.apiFunctionName}`;
|
|
114
|
+
|
|
115
|
+
// Parse since duration to milliseconds
|
|
116
|
+
const sinceMs = parseDuration(since);
|
|
117
|
+
const startTime = Date.now() - sinceMs;
|
|
118
|
+
|
|
119
|
+
success(`Streaming logs from ${logGroupName}...\n`);
|
|
120
|
+
|
|
121
|
+
// Poll for new logs every 2 seconds
|
|
122
|
+
let nextToken: string | undefined;
|
|
123
|
+
let lastTimestamp = startTime;
|
|
124
|
+
|
|
125
|
+
const poll = async () => {
|
|
126
|
+
try {
|
|
127
|
+
const params: any = {
|
|
128
|
+
logGroupName,
|
|
129
|
+
startTime: lastTimestamp,
|
|
130
|
+
filterPattern,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (nextToken) {
|
|
134
|
+
params.nextToken = nextToken;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const response = await client.send(new FilterLogEventsCommand(params));
|
|
138
|
+
|
|
139
|
+
if (response.events && response.events.length > 0) {
|
|
140
|
+
for (const event of response.events) {
|
|
141
|
+
if (event.timestamp && event.timestamp > lastTimestamp) {
|
|
142
|
+
lastTimestamp = event.timestamp;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const timestamp = event.timestamp
|
|
146
|
+
? new Date(event.timestamp).toISOString()
|
|
147
|
+
: '';
|
|
148
|
+
const message = event.message || '';
|
|
149
|
+
|
|
150
|
+
console.log(`[${timestamp}] ${message.trimEnd()}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
nextToken = response.nextToken;
|
|
155
|
+
|
|
156
|
+
// Continue polling
|
|
157
|
+
setTimeout(poll, 2000);
|
|
158
|
+
} catch (err: any) {
|
|
159
|
+
if (err.name === 'ResourceNotFoundException') {
|
|
160
|
+
fail(`Log group not found: ${logGroupName}`);
|
|
161
|
+
info('The Lambda function may not have been invoked yet.');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await poll();
|
|
169
|
+
} catch (err: any) {
|
|
170
|
+
fail(`Failed to tail logs: ${err.message}`);
|
|
171
|
+
info('Ensure your IAM user/role has logs:FilterLogEvents permission.');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function logsQueryCommand(flags: Record<string, string>): Promise<void> {
|
|
177
|
+
step('Resolving deployed config...');
|
|
178
|
+
let config;
|
|
179
|
+
try {
|
|
180
|
+
config = await resolveConfig(flags.stage);
|
|
181
|
+
} catch (err: any) {
|
|
182
|
+
fail(err.message);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
info(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
|
|
187
|
+
|
|
188
|
+
// Build query parameters
|
|
189
|
+
const query: Record<string, any> = {
|
|
190
|
+
limit: flags.limit ? parseInt(flags.limit, 10) : 50,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (flags.level) query.level = flags.level;
|
|
194
|
+
if (flags.source) query.source = flags.source;
|
|
195
|
+
if (flags.traceId) query.traceId = flags.traceId;
|
|
196
|
+
if (flags.userId) query.userId = flags.userId;
|
|
197
|
+
if (flags.deviceId) query.deviceId = flags.deviceId;
|
|
198
|
+
if (flags.sessionId) query.sessionId = flags.sessionId;
|
|
199
|
+
if (flags.platform) query.platform = flags.platform;
|
|
200
|
+
if (flags.environment) query.environment = flags.environment;
|
|
201
|
+
if (flags.search) query.search = flags.search;
|
|
202
|
+
if (flags.since) query.since = flags.since;
|
|
203
|
+
|
|
204
|
+
step(`Querying logs with filters: ${JSON.stringify(query)}...`);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const result: any = await invokeAction(
|
|
208
|
+
config.region,
|
|
209
|
+
config.apiFunctionName,
|
|
210
|
+
'logs:query',
|
|
211
|
+
query,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (result?.error) {
|
|
215
|
+
fail(`Failed to query logs: ${result.error}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const logs = result.logs || [];
|
|
220
|
+
|
|
221
|
+
if (logs.length === 0) {
|
|
222
|
+
success('No logs found matching the criteria.');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const sourceLabel = result.source === 's3' ? ' (s3)' : '';
|
|
227
|
+
success(`Found ${logs.length} log(s)${sourceLabel}:\n`);
|
|
228
|
+
|
|
229
|
+
// Format and display logs
|
|
230
|
+
for (const log of logs) {
|
|
231
|
+
const timestamp = new Date(log.timestamp).toISOString();
|
|
232
|
+
const level = (log.level || 'info').toUpperCase().padEnd(5);
|
|
233
|
+
|
|
234
|
+
console.log(`[${timestamp}] ${level} ${log.source || 'unknown'}`);
|
|
235
|
+
console.log(` ${log.message || '(no message)'}`);
|
|
236
|
+
|
|
237
|
+
if (log.traceId) {
|
|
238
|
+
console.log(` TraceID: ${log.traceId}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (log.userId) {
|
|
242
|
+
console.log(` UserID: ${log.userId}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (log.deviceId) {
|
|
246
|
+
console.log(` DeviceID: ${log.deviceId}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (log.error) {
|
|
250
|
+
console.log(` Error: ${log.error.name}: ${log.error.message}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (log.data && Object.keys(log.data).length > 0) {
|
|
254
|
+
console.log(` Data: ${JSON.stringify(log.data, null, 2).split('\n').join('\n ')}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log('');
|
|
258
|
+
}
|
|
259
|
+
} catch (err: any) {
|
|
260
|
+
fail(`Failed to query logs: ${err.message}`);
|
|
261
|
+
info('Ensure your IAM user/role has lambda:InvokeFunction permission.');
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function parseDuration(duration: string): number {
|
|
267
|
+
const match = duration.match(/^(\d+)([smhd])$/);
|
|
268
|
+
if (!match) {
|
|
269
|
+
return 5 * 60 * 1000; // Default: 5 minutes
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const value = parseInt(match[1], 10);
|
|
273
|
+
const unit = match[2];
|
|
274
|
+
|
|
275
|
+
switch (unit) {
|
|
276
|
+
case 's': return value * 1000;
|
|
277
|
+
case 'm': return value * 60 * 1000;
|
|
278
|
+
case 'h': return value * 60 * 60 * 1000;
|
|
279
|
+
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
280
|
+
default: return 5 * 60 * 1000;
|
|
281
|
+
}
|
|
282
|
+
}
|