Skip to content

Commit 05556ce

Browse files
committed
Update function diagnostics surfaces
1 parent da8fd1b commit 05556ce

15 files changed

Lines changed: 318 additions & 46 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ The full MCP surface — every tool is a thin shim over an SDK call.
412412
|------|-------------|
413413
| `deploy_function` | Deploy a Node 22 serverless function. Cron-schedulable. |
414414
| `invoke_function` | Invoke a deployed function over the direct API-key-protected test path. |
415-
| `get_function_logs` | Recent logs (CloudWatch). |
415+
| `get_function_logs` | Recent logs (CloudWatch), filterable by `since` and routed `request_id`. |
416416
| `update_function` | Update schedule / timeout / memory without redeploying code. |
417417
| `list_functions` / `delete_function` | List / remove functions. |
418418
| `set_secret` / `list_secrets` / `delete_secret` | Manage `process.env` secrets injected into all functions. Values are write-only; list returns keys and timestamps only. |

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ npx run402-mcp
2424
| `deploy_site` | 部署静态 HTML/CSS/JS 站点 |
2525
| `deploy_function` | 部署 Node 22 Serverless 函数 |
2626
| `invoke_function` | 调用已部署的函数 |
27-
| `get_function_logs` | 获取函数日志 |
27+
| `get_function_logs` | 获取函数日志,可按 `since` 和 routed `request_id` 过滤 |
2828
| `set_secret` | 设置函数环境变量 |
2929

3030
## 定价

SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Important fields:
4444
- `safe_to_retry` — repeating the same request should not duplicate or corrupt a mutation
4545
- `mutation_state` — gateway-known mutation progress: `none`, `not_started`, `committed`, `rolled_back`, `partial`, or `unknown`
4646
- `trace_id` — include this when reporting a Run402 issue
47+
- `request_id` — routed/function failure handle; use `get_function_logs` with `request_id` for function diagnostics. This is distinct from gateway `trace_id`.
4748
- `details` — structured route-specific context
4849
- `next_actions` — advisory suggestions such as `authenticate`, `submit_payment`, `renew_tier`, `check_usage`, `retry`, `resume_deploy`, `edit_request`, or `edit_migration`; render or follow them only after validating the action is safe
4950

@@ -299,7 +300,7 @@ No `route_scopes` means no CI route-declaration authority. With route scopes, CI
299300

300301
- **`deploy_function`** — deploy a Node 22 serverless function. Cron-schedulable via `schedule`. Pass `deps` as npm specs (bare names → latest at deploy time, pinned `lodash@4.17.21` or ranges `date-fns@^3.0.0` honored verbatim, max 30 entries / 200 chars each, native binaries rejected). Response surfaces `runtime_version`, `deps_resolved`, `warnings`.
301302
- **`invoke_function`** — invoke for testing over the direct `/functions/v1/:name` API-key-protected path.
302-
- **`get_function_logs`** — recent logs (CloudWatch). Use `since` for incremental polling.
303+
- **`get_function_logs`** — recent logs (CloudWatch). Use `since` for incremental polling and `request_id` (`req_...`) to follow a routed browser failure from `X-Run402-Request-Id` / JSON `request_id`.
303304
- **`update_function`** — change schedule / timeout / memory without redeploying code.
304305
- **`list_functions`** / **`delete_function`** — list / remove.
305306
- **`set_secret`** / **`list_secrets`** / **`delete_secret`**`process.env` secrets injected into every function. Values are write-only; `list_secrets` returns keys and timestamps only. Deploy specs use `secrets.require[]` as a dependency gate, not as a value carrier or per-function allowlist.

cli-argv.test.mjs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,97 @@ describe("--flag=value", () => {
170170
assert.ok(call, `expected logs request, got ${JSON.stringify(calls)}`);
171171
assert.match(call.url, /tail=10/);
172172
});
173+
174+
it("functions logs accepts equals-form request-id filters", async () => {
175+
const { run } = await import("./cli/lib/functions.mjs");
176+
captureStart();
177+
await run("logs", ["prj_test123", "hello", "--request-id=req_abc123"]);
178+
captureStop();
179+
180+
const call = calls.find((c) => /\/logs\?/.test(c.path));
181+
assert.ok(call, `expected logs request, got ${JSON.stringify(calls)}`);
182+
assert.equal(new URL(call.url).searchParams.get("request_id"), "req_abc123");
183+
});
184+
});
185+
186+
describe("function log filter validation", () => {
187+
it("functions logs rejects invalid --since before network", async () => {
188+
const { run } = await import("./cli/lib/functions.mjs");
189+
const err = await expectExit1(() =>
190+
run("logs", ["prj_test123", "hello", "--since", "not-a-date"]));
191+
192+
assert.equal(err.code, "BAD_USAGE");
193+
assert.equal(err.details.flag, "--since");
194+
assert.equal(calls.length, 0, "bad --since must not hit the network");
195+
});
196+
197+
it("functions logs rejects invalid --request-id before network", async () => {
198+
const { run } = await import("./cli/lib/functions.mjs");
199+
const err = await expectExit1(() =>
200+
run("logs", ["prj_test123", "hello", "--request-id", "trace_abc"]));
201+
202+
assert.equal(err.code, "BAD_USAGE");
203+
assert.equal(err.details.flag, "--request-id");
204+
assert.equal(calls.length, 0, "bad --request-id must not hit the network");
205+
});
206+
207+
it("functions logs follow dedupes same-millisecond entries by event identity", async () => {
208+
const { run } = await import("./cli/lib/functions.mjs");
209+
const prevFetch = globalThis.fetch;
210+
const prevSetTimeout = globalThis.setTimeout;
211+
const timestamp = "2026-05-01T00:00:00.000Z";
212+
let logFetches = 0;
213+
let sleeps = 0;
214+
globalThis.fetch = (input, init) => {
215+
const info = requestInfo(input, init);
216+
calls.push(info);
217+
const pathNoQuery = info.path.split("?")[0];
218+
if (/\/functions\/hello\/logs$/.test(pathNoQuery) && info.method === "GET") {
219+
logFetches += 1;
220+
const logs = logFetches === 1
221+
? [
222+
{ timestamp, message: "first", event_id: "evt-1", log_stream_name: "stream-a" },
223+
{ timestamp, message: "second", event_id: "evt-2", log_stream_name: "stream-a" },
224+
]
225+
: [
226+
{ timestamp, message: "first", event_id: "evt-1", log_stream_name: "stream-a" },
227+
{ timestamp, message: "second", event_id: "evt-2", log_stream_name: "stream-a" },
228+
{ timestamp, message: "third", event_id: "evt-3", log_stream_name: "stream-a" },
229+
];
230+
return Promise.resolve(json({ logs }));
231+
}
232+
return mockFetch(input, init);
233+
};
234+
globalThis.setTimeout = (fn, _ms, ...args) => {
235+
sleeps += 1;
236+
queueMicrotask(() => {
237+
if (sleeps >= 2) process.emit("SIGINT");
238+
fn(...args);
239+
});
240+
return 0;
241+
};
242+
243+
try {
244+
captureStart();
245+
await run("logs", ["prj_test123", "hello", "--follow"]);
246+
captureStop();
247+
} finally {
248+
captureStop();
249+
globalThis.fetch = prevFetch;
250+
globalThis.setTimeout = prevSetTimeout;
251+
}
252+
253+
const output = stdout.join("\n");
254+
assert.equal((output.match(/first/g) || []).length, 1);
255+
assert.equal((output.match(/second/g) || []).length, 1);
256+
assert.equal((output.match(/third/g) || []).length, 1);
257+
const logCalls = calls.filter((c) => /\/logs\?/.test(c.path));
258+
assert.ok(logCalls.length >= 2, `expected at least two log polls, got ${JSON.stringify(logCalls)}`);
259+
assert.equal(
260+
new URL(logCalls[1].url).searchParams.get("since"),
261+
String(new Date(timestamp).getTime()),
262+
);
263+
});
173264
});
174265

175266
describe("numeric flag validation", () => {

cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ run402 functions deploy <id> my-fn --file fn.ts \
114114
--timeout 30 --memory 256 \
115115
--schedule "*/15 * * * *" \
116116
--deps "stripe,zod@^3"
117-
run402 functions logs <id> my-fn --tail 100 --follow
117+
run402 functions logs <id> my-fn --tail 100 --request-id req_abc123 --follow
118118
run402 functions invoke <id> my-fn --body '{"hello":"world"}'
119119
```
120120

cli/lib/functions.mjs

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { getSdk } from "./sdk.mjs";
44
import { reportSdkError, fail } from "./sdk-errors.mjs";
55
import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag, validateRegularFile } from "./argparse.mjs";
66

7+
const FUNCTION_LOG_REQUEST_ID_RE = /^req_[A-Za-z0-9_-]{4,128}$/;
8+
79
const HELP = `run402 functions — Manage serverless functions
810
911
Usage:
@@ -14,7 +16,7 @@ Subcommands:
1416
Deploy a function to a project
1517
invoke <id> <name> [--method <M>] [--body <json>]
1618
Invoke a deployed function
17-
logs <id> <name> [--tail <n>] [--since <ts>] [--follow]
19+
logs <id> <name> [--tail <n>] [--since <ts>] [--request-id <req_...>] [--follow]
1820
Get function logs
1921
update <id> <name> [--schedule <cron>] [--schedule-remove] [--timeout <s>] [--memory <mb>]
2022
Update function schedule or config without re-deploying
@@ -28,6 +30,7 @@ Examples:
2830
run402 functions invoke prj_abc123 stripe-webhook --body '{"event":"test"}'
2931
run402 functions logs prj_abc123 stripe-webhook --tail 100
3032
run402 functions logs prj_abc123 stripe-webhook --since 2026-03-29T14:00:00Z
33+
run402 functions logs prj_abc123 stripe-webhook --request-id req_abc123
3134
run402 functions logs prj_abc123 stripe-webhook --follow
3235
run402 functions update prj_abc123 send-reminders --schedule '0 */4 * * *'
3336
run402 functions update prj_abc123 send-reminders --schedule-remove
@@ -112,11 +115,13 @@ Arguments:
112115
Options:
113116
--tail <n> Number of most-recent entries (default 50)
114117
--since <ts> ISO timestamp or epoch ms; only entries after this
118+
--request-id <id> Only entries correlated to this req_... request id
115119
--follow Poll every 3s and stream new entries (Ctrl-C to stop)
116120
117121
Examples:
118122
run402 functions logs prj_abc123 stripe-webhook --tail 100
119123
run402 functions logs prj_abc123 stripe-webhook --since 2026-03-29T14:00:00Z
124+
run402 functions logs prj_abc123 stripe-webhook --request-id req_abc123
120125
run402 functions logs prj_abc123 stripe-webhook --follow
121126
`,
122127
update: `run402 functions update — Update function config without re-deploying
@@ -227,14 +232,16 @@ async function invoke(projectId, name, args) {
227232
}
228233

229234
async function logs(projectId, name, args) {
230-
assertRequiredProjectAndName(projectId, name, "run402 functions logs <project_id> <name> [--tail <n>]");
231-
assertKnownFlags(args, ["--tail", "--since", "--follow", "--help", "-h"], ["--tail", "--since"]);
235+
assertRequiredProjectAndName(projectId, name, "run402 functions logs <project_id> <name> [--tail <n>] [--request-id <req_...>]");
236+
assertKnownFlags(args, ["--tail", "--since", "--request-id", "--follow", "--help", "-h"], ["--tail", "--since", "--request-id"]);
232237
let tail = 50;
233238
let since = undefined;
239+
let requestId = undefined;
234240
let follow = false;
235241
for (let i = 0; i < args.length; i++) {
236242
if (args[i] === "--tail") tail = parseIntegerFlag("--tail", args[++i], { min: 1 });
237243
if (args[i] === "--since" && args[i + 1]) since = args[++i];
244+
if (args[i] === "--request-id" && args[i + 1]) requestId = args[++i];
238245
if (args[i] === "--follow") follow = true;
239246
}
240247

@@ -254,12 +261,20 @@ async function logs(projectId, name, args) {
254261
}
255262
sinceIso = new Date(ms).toISOString();
256263
}
264+
if (requestId !== undefined && !FUNCTION_LOG_REQUEST_ID_RE.test(requestId)) {
265+
fail({
266+
code: "BAD_USAGE",
267+
message: `Invalid --request-id value: ${requestId}`,
268+
details: { flag: "--request-id", value: requestId, expected: "req_<4-128 url-safe chars>" },
269+
});
270+
}
257271

258272
const fetchLogs = async () => {
259273
try {
260274
const data = await getSdk().functions.logs(projectId, name, {
261275
tail,
262276
since: sinceIso,
277+
requestId,
263278
});
264279
return data.logs || [];
265280
} catch (err) {
@@ -278,27 +293,60 @@ async function logs(projectId, name, args) {
278293
let running = true;
279294
process.on("SIGINT", () => { running = false; });
280295

281-
const initial = await fetchLogs();
282-
for (const entry of initial) {
283-
console.log(`[${entry.timestamp}] ${entry.message}`);
284-
}
285-
if (initial.length > 0) {
286-
sinceIso = new Date(new Date(initial[initial.length - 1].timestamp).getTime() + 1).toISOString();
287-
}
296+
let highWaterMs = sinceIso === undefined ? Number.NEGATIVE_INFINITY : new Date(sinceIso).getTime();
297+
let seenAtHighWater = new Set();
288298

289-
while (running) {
290-
await new Promise(r => setTimeout(r, 3000));
291-
if (!running) break;
292-
const entries = await fetchLogs();
299+
const printFreshEntries = (entries) => {
300+
let nextHighWaterMs = highWaterMs;
301+
const fresh = [];
293302
for (const entry of entries) {
303+
const entryMs = logTimestampMs(entry);
304+
const identity = logEntryIdentity(entry);
305+
if (entryMs < highWaterMs) continue;
306+
if (entryMs === highWaterMs && seenAtHighWater.has(identity)) continue;
307+
fresh.push({ entry, entryMs, identity });
308+
if (entryMs > nextHighWaterMs) nextHighWaterMs = entryMs;
309+
}
310+
311+
for (const { entry } of fresh) {
294312
console.log(`[${entry.timestamp}] ${entry.message}`);
295313
}
296-
if (entries.length > 0) {
297-
sinceIso = new Date(new Date(entries[entries.length - 1].timestamp).getTime() + 1).toISOString();
314+
if (fresh.length === 0 || !Number.isFinite(nextHighWaterMs)) return;
315+
316+
const nextSeenAtHighWater = new Set();
317+
for (const entry of entries) {
318+
if (logTimestampMs(entry) === nextHighWaterMs) {
319+
nextSeenAtHighWater.add(logEntryIdentity(entry));
320+
}
298321
}
322+
for (const { entry, entryMs, identity } of fresh) {
323+
if (entryMs === nextHighWaterMs) {
324+
nextSeenAtHighWater.add(identity);
325+
}
326+
}
327+
highWaterMs = nextHighWaterMs;
328+
seenAtHighWater = nextSeenAtHighWater;
329+
sinceIso = new Date(highWaterMs).toISOString();
330+
};
331+
332+
printFreshEntries(await fetchLogs());
333+
334+
while (running) {
335+
await new Promise(r => setTimeout(r, 3000));
336+
if (!running) break;
337+
printFreshEntries(await fetchLogs());
299338
}
300339
}
301340

341+
function logTimestampMs(entry) {
342+
const ms = new Date(entry.timestamp).getTime();
343+
return Number.isNaN(ms) ? 0 : ms;
344+
}
345+
346+
function logEntryIdentity(entry) {
347+
return entry.event_id || `${entry.log_stream_name || ""}:${entry.timestamp || ""}:${entry.message || ""}`;
348+
}
349+
302350
async function update(projectId, name, args) {
303351
assertRequiredProjectAndName(projectId, name, "run402 functions update <project_id> <name> [options]");
304352
assertKnownFlags(args, ["--schedule", "--schedule-remove", "--timeout", "--memory", "--help", "-h"], ["--schedule", "--timeout", "--memory"]);

cli/llms-cli.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Canonical fields:
4747
- `safe_to_retry`: repeating the same request should not duplicate or corrupt a mutation
4848
- `mutation_state`: one of `none`, `not_started`, `committed`, `rolled_back`, `partial`, `unknown`
4949
- `trace_id`: include this when reporting the issue
50+
- `request_id`: routed/function failure handle; use `run402 functions logs <id> <name> --request-id <req_...>` for function diagnostics. This is distinct from gateway `trace_id`.
5051
- `details`: structured route-specific context
5152
- `next_actions`: advisory suggestions such as `authenticate`, `submit_payment`, `renew_tier`, `check_usage`, `retry`, `resume_deploy`, `edit_request`, `edit_migration`, or `deploy_site_first`; do not execute route-like suggestions without validating method/path/auth/safety
5253

@@ -671,10 +672,12 @@ const res = await fetch('https://api.run402.com/functions/v1/my-function', {
671672

672673
- `run402 functions deploy <id> <name> --file <file> [--deps "<spec,...>"] [--timeout <s>] [--memory <mb>] [--schedule "<cron>"]`
673674
- `run402 functions invoke <id> <name> [--body '<json>'] [--method <GET|POST|...>]`
674-
- `run402 functions logs <id> <name> [--tail <n>] [--since <iso-timestamp>] [--follow]`
675+
- `run402 functions logs <id> <name> [--tail <n>] [--since <iso-timestamp>] [--request-id <req_...>] [--follow]`
675676
- `run402 functions update <id> <name> [--schedule "<cron>"] [--schedule-remove] [--timeout <s>] [--memory <mb>]`
676677
- `run402 functions <list|delete> <id> [<name>]`
677678

679+
For routed browser 500s, copy `X-Run402-Request-Id` or the JSON `request_id` from the response and run `run402 functions logs <project> <function> --request-id req_...`. `--since` is validated locally and should be supplied for incidents older than the default recent lookup window.
680+
678681
#### --deps semantics, runtime_version, deps_resolved
679682

680683
`--deps` is a comma-separated list of npm specs that the gateway installs and bundles into the function zip alongside the user code:

llms-mcp.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Important fields:
4848
- `safe_to_retry` — repeating the same request will not duplicate or corrupt a mutation
4949
- `mutation_state` — `none` / `not_started` / `committed` / `rolled_back` / `partial` / `unknown`
5050
- `trace_id` — include this when reporting an issue
51+
- `request_id` — routed/function failure handle. Use `get_function_logs` with `request_id` for function diagnostics; it is distinct from gateway `trace_id`.
5152
- `details` — structured route-specific context
5253
- `next_actions` — `authenticate`, `submit_payment`, `renew_tier`, `check_usage`, `retry`, `resume_deploy`, `edit_request`, `edit_migration`
5354

@@ -262,10 +263,12 @@ No `route_scopes` means no CI route-declaration authority. Route scopes are exac
262263

263264
- **`deploy_function`** — deploy a Node 22 serverless function. Params: `project_id`, `name`, `code`, `config?` (`{ timeout?, memory? }`), `deps?` (npm specs: bare names → latest; pinned `lodash@4.17.21`; ranges `date-fns@^3.0.0`; max 30 entries / 200 chars; native binaries rejected; **don't list `@run402/functions`**), `schedule?` (5-field cron). Response surfaces `runtime_version`, `deps_resolved`, `warnings`.
264265
- **`invoke_function`** — invoke for testing over the direct `/functions/v1/:name` API-key-protected path. Params: `project_id`, `name`, `method?`, `body?`, `headers?`.
265-
- **`get_function_logs`** — recent logs (CloudWatch). Params: `project_id`, `name`, `tail?` (default 50, max 200), `since?` (ISO 8601, for incremental polling).
266+
- **`get_function_logs`** — recent logs (CloudWatch). Params: `project_id`, `name`, `tail?` (default 50, max 1000), `since?` (ISO 8601, locally validated), `request_id?` (`req_...`, for routed/function correlation). Returned lines include optional metadata such as `request_id`, `event_id`, log stream, and ingestion time.
266267
- **`update_function`** — change schedule / timeout / memory without redeploying code. Params: `project_id`, `name`, `schedule?` (`null` to remove), `timeout?`, `memory?`.
267268
- **`list_functions`** / **`delete_function`** — list / remove.
268269

270+
For routed browser 500s, copy `X-Run402-Request-Id` or the JSON `request_id` from the response and call `get_function_logs` with that `request_id`. If the incident is older than the default recent lookup window, also pass `since`.
271+
269272
Scheduled function tier limits: prototype 1 / 15 min, hobby 3 / 5 min, team 10 / 1 min. Deploying a scheduled function beyond the limit returns 403.
270273

271274
### Secrets

openclaw/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Fields to use:
7676
- `safe_to_retry`: repeating the same request should not duplicate or corrupt a mutation
7777
- `mutation_state`: one of `none`, `not_started`, `committed`, `rolled_back`, `partial`, `unknown`
7878
- `trace_id`: include when reporting an issue
79+
- `request_id`: routed/function failure handle; use `run402 functions logs <id> <name> --request-id <req_...>` for diagnostics. Distinct from gateway `trace_id`.
7980
- `details`: structured route-specific context
8081
- `next_actions`: advisory actions such as `authenticate`, `submit_payment`, `renew_tier`, `check_usage`, `retry`, `resume_deploy`, `edit_request`, `edit_migration`; never treat them as blindly executable
8182

@@ -373,7 +374,7 @@ run402 functions deploy <id> my-fn --file fn.ts \
373374
--deps "stripe,zod@^3,date-fns@3.6.0"
374375

375376
run402 functions invoke <id> my-fn --body '{"hello":"world"}'
376-
run402 functions logs <id> my-fn --tail 100 --follow
377+
run402 functions logs <id> my-fn --tail 100 --request-id req_abc123 --follow
377378
run402 functions update <id> my-fn --schedule "0 */6 * * *"
378379
run402 functions list <id>
379380
run402 functions delete <id> my-fn

0 commit comments

Comments
 (0)