@awsless/cli 0.1.10 → 0.1.12

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.
Files changed (2) hide show
  1. package/dist/bin.js +420 -85
  2. package/package.json +15 -15
package/dist/bin.js CHANGED
@@ -155069,8 +155069,19 @@ import { readFile as readFile12 } from "fs/promises";
155069
155069
  import { createServer as createServer2 } from "http";
155070
155070
 
155071
155071
  // src/dev/util.ts
155072
+ import { randomBytes } from "crypto";
155072
155073
  import { createServer } from "net";
155073
155074
  var LOCAL_ACCOUNT_ID = "000000000000";
155075
+ var TRACE_HEADER = "x-awsless-trace";
155076
+ var traceId = () => randomBytes(4).toString("hex");
155077
+ var formatTraceHeader = (trace) => `${trace.traceId}:${trace.spanId}`;
155078
+ var parseTraceHeader = (value) => {
155079
+ if (typeof value !== "string") {
155080
+ return;
155081
+ }
155082
+ const [traceId2, spanId] = value.split(":");
155083
+ return traceId2 && spanId ? { traceId: traceId2, spanId } : undefined;
155084
+ };
155074
155085
  var readBody = (req) => {
155075
155086
  return new Promise((resolve3, reject) => {
155076
155087
  const chunks = [];
@@ -156210,9 +156221,24 @@ var createLambdaFunction = (ctx, id, local) => {
156210
156221
  for (const [key, value] of Object.entries(props.environment ?? {})) {
156211
156222
  variables[key] = value;
156212
156223
  }
156213
- if (!sandboxed) {
156214
- ctx.onEnv(build2.addEnv);
156215
- ctx.onBind(build2.addEnv);
156224
+ ctx.onEnv(build2.addEnv);
156225
+ ctx.onBind(build2.addEnv);
156226
+ if (sandboxed) {
156227
+ const configs = typeof local.sandbox === "object" && local.sandbox.configs || [];
156228
+ variables.CONFIGS = configs.join(",");
156229
+ if (configs.length > 0) {
156230
+ addPermission({
156231
+ actions: [
156232
+ "ssm:GetParameter",
156233
+ "ssm:GetParameters",
156234
+ "ssm:GetParametersByPath",
156235
+ "ssm:GetParameterHistory"
156236
+ ],
156237
+ resources: configs.map((configName) => {
156238
+ return `arn:aws:ssm:${ctx.appConfig.region}:${ctx.accountId}:parameter${configParameterPrefix(ctx.app.name)}/${configName}`;
156239
+ })
156240
+ });
156241
+ }
156216
156242
  }
156217
156243
  if (typeof local.sandbox === "object") {
156218
156244
  const routes = [
@@ -156258,21 +156284,6 @@ var createLambdaFunction = (ctx, id, local) => {
156258
156284
  resources: [proxy.lambda.arn.pipe((arn) => `${arn}:*`)]
156259
156285
  });
156260
156286
  }
156261
- const configs = local.sandbox.configs ?? [];
156262
- if (configs.length > 0) {
156263
- variables.CONFIGS = configs.join(",");
156264
- addPermission({
156265
- actions: [
156266
- "ssm:GetParameter",
156267
- "ssm:GetParameters",
156268
- "ssm:GetParametersByPath",
156269
- "ssm:GetParameterHistory"
156270
- ],
156271
- resources: configs.map((configName) => {
156272
- return `arn:aws:ssm:${ctx.appConfig.region}:${ctx.accountId}:parameter${configParameterPrefix(ctx.app.name)}/${configName}`;
156273
- })
156274
- });
156275
- }
156276
156287
  }
156277
156288
  if (props.log.retention.value > 0n) {
156278
156289
  const logGroup = new aws2.cloudwatch.LogGroup(group4, "log", {
@@ -157363,7 +157374,7 @@ var createSnsServer = (props) => {
157363
157374
  let closeServer;
157364
157375
  let dispatch;
157365
157376
  let reportFailure;
157366
- const publish2 = (input) => {
157377
+ const publish2 = (input, trace) => {
157367
157378
  const messageId = randomUUID4();
157368
157379
  for (const capture of props?.captures ?? []) {
157369
157380
  if (capture(input)) {
@@ -157396,7 +157407,7 @@ var createSnsServer = (props) => {
157396
157407
  ]
157397
157408
  };
157398
157409
  setImmediate(() => {
157399
- dispatch?.(event).catch((error3) => {
157410
+ dispatch?.(event, trace).catch((error3) => {
157400
157411
  reportFailure?.({ kind: "async", event, error: error3 });
157401
157412
  });
157402
157413
  });
@@ -157427,7 +157438,7 @@ var createSnsServer = (props) => {
157427
157438
  res.end(`The local dev sns emulator only supports Publish, got: ${action}`);
157428
157439
  return;
157429
157440
  }
157430
- const messageId = publish2(input);
157441
+ const messageId = publish2(input, parseTraceHeader(req.headers[TRACE_HEADER]));
157431
157442
  if (isJson) {
157432
157443
  res.writeHead(200, { "content-type": "application/x-amz-json-1.0" });
157433
157444
  res.end(JSON.stringify({ MessageId: messageId }));
@@ -158265,7 +158276,7 @@ var startDevRouter = async (props) => {
158265
158276
  const sourceIp = server2.requestIP(request)?.address ?? "127.0.0.1";
158266
158277
  let result;
158267
158278
  try {
158268
- result = await props.dispatch(formatWebEvent(request, route, body, url, sourceIp));
158279
+ result = await props.dispatch(formatWebEvent(request, route, body, url, sourceIp), parseTraceHeader(request.headers.get(TRACE_HEADER)));
158269
158280
  } catch (error3) {
158270
158281
  props.onError?.(error3, route.routeKey);
158271
158282
  const detail = error3 instanceof Error ? error3.stack ?? error3.message : String(error3);
@@ -158427,7 +158438,7 @@ var createRestServer = (props) => {
158427
158438
  },
158428
158439
  body: body.length > 0 ? body.toString("base64") : undefined,
158429
158440
  isBase64Encoded: body.length > 0
158430
- });
158441
+ }, parseTraceHeader(req.headers[TRACE_HEADER]));
158431
158442
  writeWebResponse(res, result);
158432
158443
  };
158433
158444
  return {
@@ -159314,9 +159325,6 @@ ${(errors3.trim() || output.trim()).slice(-2000)}`);
159314
159325
  }
159315
159326
  if (props.ssr && ctx.dev && props.dev) {} else if (props.ssr && isStandaloneFunction(props.ssr) && !ctx.dev) {
159316
159327
  const fn = createLambdaFunction(ctx, `${id}-ssr`, props.ssr);
159317
- if (ctx.shared.has("router", "endpoint", props.router)) {
159318
- fn.setEnvironment(`ROUTER_${constantCase(props.router)}_ENDPOINT`, ctx.shared.entry("router", "endpoint", props.router));
159319
- }
159320
159328
  const deployment = new FunctionDeployment(group4, "ssr-deployment", {
159321
159329
  functionName: fn.lambda.functionName,
159322
159330
  id: ctx.deploymentId ?? "local-0",
@@ -159787,12 +159795,12 @@ var tableOnDev = async (ctx) => {
159787
159795
  if (props.stream) {
159788
159796
  const routeKey = formatRouteKey(stackName, "table", id);
159789
159797
  const eventSourceARN = `arn:aws:dynamodb:${ctx.appConfig.region}:000000000000:table/${name}/stream/local`;
159790
- const unsubscribe = server.onStreamRecord(name, (record) => {
159798
+ const unsubscribe = server.onStreamRecord(name, (record, context) => {
159791
159799
  if (isWiping()) {
159792
159800
  return;
159793
159801
  }
159794
159802
  const event = { Records: [{ ...record, eventSourceARN }] };
159795
- dispatch(event).catch((error3) => {
159803
+ dispatch(event, parseTraceHeader(context)).catch((error3) => {
159796
159804
  reportFailure({
159797
159805
  kind: "stream",
159798
159806
  routeKey,
@@ -160011,8 +160019,9 @@ var createSchedulerServer = () => {
160011
160019
  const name = decodeURIComponent(match3[1]);
160012
160020
  const input = JSON.parse(body.toString() || "{}");
160013
160021
  const payload = input.Target?.Input ? JSON.parse(input.Target.Input) : {};
160022
+ const trace = parseTraceHeader(req.headers[TRACE_HEADER]);
160014
160023
  setImmediate(() => {
160015
- dispatch?.(payload).catch((error3) => {
160024
+ dispatch?.(payload, trace).catch((error3) => {
160016
160025
  const routeKey = payload?.["$awsless-route"];
160017
160026
  reportFailure?.({
160018
160027
  kind: "async",
@@ -164730,6 +164739,7 @@ var startTest = async (props) => {
164730
164739
  for (const error3 of module.errors()) {
164731
164740
  const stack = error3.stacks?.[0];
164732
164741
  const { code, syscall, address, port } = error3;
164742
+ const { name: _n, message: _m2, stack: _s3, stacks: _ss, diff: _d2, cause, ...rest } = error3;
164733
164743
  errors3.push({
164734
164744
  type: error3.name,
164735
164745
  message: error3.message,
@@ -164738,7 +164748,9 @@ var startTest = async (props) => {
164738
164748
  code,
164739
164749
  syscall,
164740
164750
  address,
164741
- port
164751
+ port,
164752
+ cause,
164753
+ props: Object.keys(rest).length > 0 ? rest : undefined
164742
164754
  });
164743
164755
  }
164744
164756
  }
@@ -164836,7 +164848,20 @@ var logTestErrors = (event) => {
164836
164848
  };
164837
164849
  var formatModuleError = (error3) => {
164838
164850
  const syscall = [error3.syscall, error3.address, error3.port && `port ${error3.port}`].filter(Boolean).join(" ");
164839
- return [error3.message, syscall && `(${syscall})`, `
164851
+ const extra = [];
164852
+ if (error3.props) {
164853
+ try {
164854
+ extra.push(`props: ${JSON.stringify(error3.props)}`);
164855
+ } catch (_4) {}
164856
+ }
164857
+ if (error3.cause) {
164858
+ try {
164859
+ extra.push(`cause: ${JSON.stringify(error3.cause, Object.getOwnPropertyNames(error3.cause))}`);
164860
+ } catch (_4) {
164861
+ extra.push(`cause: ${String(error3.cause)}`);
164862
+ }
164863
+ }
164864
+ return [error3.message, syscall && `(${syscall})`, ...extra, `
164840
164865
  `, error3.stack ?? "(no stack captured)"].filter(Boolean).join(" ");
164841
164866
  };
164842
164867
  var runTest = async (stack, dir, filters, workspace, opts) => {
@@ -166568,7 +166593,7 @@ var createS3Server = (props) => {
166568
166593
  let closeServer;
166569
166594
  let dispatch;
166570
166595
  let reportFailure;
166571
- const notify = (eventName, bucket, key, size2, eTag) => {
166596
+ const notify = (eventName, bucket, key, size2, eTag, trace) => {
166572
166597
  for (const rule of props.rules) {
166573
166598
  const matches = rule.events.some((event2) => {
166574
166599
  const type2 = event2.replace(/^s3:/, "");
@@ -166603,7 +166628,7 @@ var createS3Server = (props) => {
166603
166628
  }
166604
166629
  ]
166605
166630
  };
166606
- dispatch?.(event).catch((error3) => {
166631
+ dispatch?.(event, trace).catch((error3) => {
166607
166632
  reportFailure?.({ kind: "async", routeKey: rule.id, event, error: error3 });
166608
166633
  });
166609
166634
  }
@@ -166670,7 +166695,7 @@ var createS3Server = (props) => {
166670
166695
  const eTag = createHash22("md5").update(body).digest("hex");
166671
166696
  res.writeHead(200, { etag: `"${eTag}"` });
166672
166697
  res.end();
166673
- notify("ObjectCreated:Put", bucket, key, body.length, eTag);
166698
+ notify("ObjectCreated:Put", bucket, key, body.length, eTag, parseTraceHeader(req.headers[TRACE_HEADER]));
166674
166699
  return;
166675
166700
  }
166676
166701
  if (req.method === "GET" || req.method === "HEAD") {
@@ -166693,7 +166718,7 @@ var createS3Server = (props) => {
166693
166718
  await rm10(file2, { force: true });
166694
166719
  res.writeHead(204);
166695
166720
  res.end();
166696
- notify("ObjectRemoved:Delete", bucket, key, 0, "");
166721
+ notify("ObjectRemoved:Delete", bucket, key, 0, "", parseTraceHeader(req.headers[TRACE_HEADER]));
166697
166722
  return;
166698
166723
  }
166699
166724
  res.writeHead(501, { "content-type": "application/xml" });
@@ -166783,7 +166808,7 @@ var createSqsServer = (props) => {
166783
166808
  }
166784
166809
  return stores.get(queue);
166785
166810
  };
166786
- const deliver = (queue, input, messageId) => {
166811
+ const deliver = (queue, input, messageId, trace) => {
166787
166812
  const now = Date.now();
166788
166813
  if (props.queues.get(queue) === undefined) {
166789
166814
  storeOf(queue).push({
@@ -166817,7 +166842,7 @@ var createSqsServer = (props) => {
166817
166842
  ]
166818
166843
  };
166819
166844
  const send = () => {
166820
- dispatch?.(event).catch((error3) => {
166845
+ dispatch?.(event, trace).catch((error3) => {
166821
166846
  let body = input.MessageBody;
166822
166847
  try {
166823
166848
  body = JSON.parse(input.MessageBody ?? "");
@@ -166863,16 +166888,16 @@ var createSqsServer = (props) => {
166863
166888
  QueueUrl: queueUrl(input.QueueName)
166864
166889
  };
166865
166890
  },
166866
- SendMessage(input) {
166891
+ SendMessage(input, _signal, trace) {
166867
166892
  const queue = queueFromUrl(input.QueueUrl);
166868
166893
  if (!queue) {
166869
166894
  throw new Error(`Unknown local queue: ${input.QueueUrl}`);
166870
166895
  }
166871
166896
  const messageId = randomUUID8();
166872
- deliver(queue, input, messageId);
166897
+ deliver(queue, input, messageId, trace);
166873
166898
  return sendResult(input, messageId);
166874
166899
  },
166875
- SendMessageBatch(input) {
166900
+ SendMessageBatch(input, _signal, trace) {
166876
166901
  const queue = queueFromUrl(input.QueueUrl);
166877
166902
  if (!queue) {
166878
166903
  throw new Error(`Unknown local queue: ${input.QueueUrl}`);
@@ -166880,7 +166905,7 @@ var createSqsServer = (props) => {
166880
166905
  return {
166881
166906
  Successful: (input.Entries ?? []).map((entry) => {
166882
166907
  const messageId = randomUUID8();
166883
- deliver(queue, entry, messageId);
166908
+ deliver(queue, entry, messageId, trace);
166884
166909
  return {
166885
166910
  Id: entry.Id,
166886
166911
  ...sendResult(entry, messageId)
@@ -167012,7 +167037,7 @@ var createSqsServer = (props) => {
167012
167037
  if (!action) {
167013
167038
  throw new Error(`The local dev sqs emulator does not support: ${target2}`);
167014
167039
  }
167015
- const result = await action(JSON.parse(body.toString() || "{}"), abort.signal);
167040
+ const result = await action(JSON.parse(body.toString() || "{}"), abort.signal, parseTraceHeader(req.headers[TRACE_HEADER]));
167016
167041
  if (res.writableEnded || res.destroyed) {
167017
167042
  return;
167018
167043
  }
@@ -167310,12 +167335,90 @@ class WorkerError extends Error {
167310
167335
  }
167311
167336
  }
167312
167337
  }
167313
- var WORKER_ENTRY = `import { createServer } from 'node:http'
167338
+ var WORKER_ENTRY = `import { AsyncLocalStorage } from 'node:async_hooks'
167339
+ import http from 'node:http'
167340
+ import https from 'node:https'
167341
+ import { createServer } from 'node:http'
167342
+ import { syncBuiltinESMExports } from 'node:module'
167314
167343
  import { format } from 'node:util'
167315
167344
 
167316
167345
  let handler
167317
167346
  let getCurrentRoute = () => undefined
167318
167347
 
167348
+ // The trace of the running invocation, set per dispatch by the dev
167349
+ // server. The async context keeps it accurate under concurrent
167350
+ // requests, exactly like the route context.
167351
+ const traceContext = new AsyncLocalStorage()
167352
+
167353
+ const isLoopback = host => host === '127.0.0.1' || host === 'localhost' || host === '::1'
167354
+
167355
+ // Outgoing requests to the local emulators carry the active trace as a
167356
+ // header, so a queue send or task invoke made by a handler links the
167357
+ // dispatch it causes back to this invocation. Only loopback targets:
167358
+ // a request to a real external api must never see the header.
167359
+ const injectTrace = module => {
167360
+ const original = module.request
167361
+
167362
+ module.request = function (...args) {
167363
+ const trace = traceContext.getStore()
167364
+
167365
+ if (trace) {
167366
+ // request(url[, options][, callback]) or request(options[, callback])
167367
+ const index = typeof args[0] === 'string' || args[0] instanceof URL ? 1 : 0
167368
+ let url
167369
+
167370
+ try {
167371
+ url = index === 1 ? new URL(String(args[0])) : undefined
167372
+ } catch (_) {}
167373
+
167374
+ let options = args[index]
167375
+
167376
+ if (typeof options === 'function' || options === undefined) {
167377
+ args.splice(index, 0, {})
167378
+ options = args[index]
167379
+ }
167380
+
167381
+ const host = String(url?.hostname ?? options.hostname ?? options.host ?? '').split(':')[0]
167382
+
167383
+ if (isLoopback(host)) {
167384
+ args[index] = { ...options, headers: { ...options.headers, 'x-awsless-trace': trace } }
167385
+ }
167386
+ }
167387
+
167388
+ return original.apply(this, args)
167389
+ }
167390
+ }
167391
+
167392
+ injectTrace(http)
167393
+ injectTrace(https)
167394
+
167395
+ // The aws sdk grabs request via ESM named imports (like
167396
+ // "const { request } = await import('node:http')"), and ESM named
167397
+ // exports of builtins are snapshots - the sync pushes the patched
167398
+ // functions into those bindings too.
167399
+ syncBuiltinESMExports()
167400
+
167401
+ const originalFetch = globalThis.fetch
167402
+
167403
+ globalThis.fetch = (input, init) => {
167404
+ const trace = traceContext.getStore()
167405
+
167406
+ if (trace) {
167407
+ try {
167408
+ const url = new URL(input instanceof Request ? input.url : String(input))
167409
+
167410
+ if (isLoopback(url.hostname)) {
167411
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined))
167412
+
167413
+ headers.set('x-awsless-trace', trace)
167414
+ init = { ...init, headers }
167415
+ }
167416
+ } catch (_) {}
167417
+ }
167418
+
167419
+ return originalFetch(input, init)
167420
+ }
167421
+
167319
167422
  // Every console call writes ONE framed record: an invisible \\x1f
167320
167423
  // marker with the active bundle route & the json-encoded text, so
167321
167424
  // multi-line output stays a single log entry downstream. The route
@@ -167361,12 +167464,12 @@ const server = createServer((req, res) => {
167361
167464
  let response
167362
167465
 
167363
167466
  try {
167364
- const { event, context } = JSON.parse(Buffer.concat(chunks).toString())
167467
+ const { event, context, trace } = JSON.parse(Buffer.concat(chunks).toString())
167365
167468
 
167366
167469
  // Fabricate the parts of the lambda context that only exist
167367
167470
  // at runtime, like the remaining time counter.
167368
167471
  const started = Date.now()
167369
- const result = await handler(event, {
167472
+ const run = () => handler(event, {
167370
167473
  functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
167371
167474
  functionVersion: process.env.AWS_LAMBDA_FUNCTION_VERSION,
167372
167475
  memoryLimitInMB: '512',
@@ -167375,6 +167478,8 @@ const server = createServer((req, res) => {
167375
167478
  ...context,
167376
167479
  })
167377
167480
 
167481
+ const result = await (trace ? traceContext.run(trace, run) : run())
167482
+
167378
167483
  response = { result: typeof result === 'undefined' ? null : result }
167379
167484
  } catch (error) {
167380
167485
  response = {
@@ -167506,7 +167611,7 @@ var createBundleWorker = (props) => {
167506
167611
  await doStop();
167507
167612
  await doStart();
167508
167613
  }),
167509
- async dispatch(event) {
167614
+ async dispatch(event, trace) {
167510
167615
  if (workers.length === 0) {
167511
167616
  throw new Error("The bundle worker is not running.");
167512
167617
  }
@@ -167515,7 +167620,7 @@ var createBundleWorker = (props) => {
167515
167620
  try {
167516
167621
  const res = await fetch(`http://127.0.0.1:${worker.port}`, {
167517
167622
  method: "POST",
167518
- body: JSON.stringify({ event, context })
167623
+ body: JSON.stringify({ event, context, trace })
167519
167624
  });
167520
167625
  const body = await res.json();
167521
167626
  if (body.error) {
@@ -167583,8 +167688,16 @@ var dashboardHtml = `<!doctype html>
167583
167688
  aside .event .body {
167584
167689
  color: var(--muted);
167585
167690
  margin-top: 4px;
167586
- word-break: break-word;
167691
+ white-space: nowrap;
167692
+ overflow: hidden;
167693
+ text-overflow: ellipsis;
167694
+ }
167695
+ /* Hovering an event unfolds its payload in place, fully wrapped -
167696
+ the same behavior as the activity feed. */
167697
+ aside .event:hover .body {
167587
167698
  white-space: pre-wrap;
167699
+ overflow: visible;
167700
+ word-break: break-word;
167588
167701
  }
167589
167702
  aside .empty { padding: 8px; }
167590
167703
  .logs {
@@ -167764,20 +167877,63 @@ var dashboardHtml = `<!doctype html>
167764
167877
  gap: 0 20px;
167765
167878
  align-items: start;
167766
167879
  }
167767
- /* The activity feed reads like the log cards. */
167768
- .activity-card {
167769
- background: var(--panel);
167880
+ .logs .route.link { cursor: pointer; }
167881
+ .logs .route.link:hover { text-decoration: underline; }
167882
+ .logs .took { color: var(--muted); margin-left: auto; flex-shrink: 0; }
167883
+ .logs .payload {
167884
+ color: var(--muted);
167885
+ white-space: nowrap;
167886
+ overflow: hidden;
167887
+ text-overflow: ellipsis;
167888
+ }
167889
+ /* Hovering an entry unfolds its payload in place, fully wrapped. */
167890
+ .logs .entry:hover .payload {
167891
+ white-space: pre-wrap;
167892
+ overflow: visible;
167893
+ word-break: break-word;
167894
+ }
167895
+ /* The trace chip appears once a dispatch has trace siblings & opens
167896
+ the trace tree of the whole request chain. */
167897
+ .logs .trace-link {
167898
+ color: var(--accent);
167899
+ cursor: pointer;
167900
+ flex-shrink: 0;
167901
+ }
167902
+ .logs .trace-link:hover { text-decoration: underline; }
167903
+ .overlay {
167904
+ position: fixed;
167905
+ inset: 0;
167906
+ background: rgb(0 0 0 / 55%);
167907
+ display: flex;
167908
+ align-items: center;
167909
+ justify-content: center;
167910
+ z-index: 50;
167911
+ }
167912
+ .overlay .modal {
167913
+ background: var(--bg);
167770
167914
  border: 1px solid var(--border);
167771
- border-radius: 6px;
167772
- padding: 8px;
167773
- margin-top: 8px;
167774
- max-height: 420px;
167915
+ border-radius: 8px;
167916
+ width: min(760px, 92vw);
167917
+ max-height: 82vh;
167775
167918
  overflow-y: auto;
167776
- font-size: 12px;
167919
+ padding: 16px 20px;
167777
167920
  }
167778
- .activity-card .rows { grid-template-columns: fit-content(110px) 1fr auto; }
167779
- .activity-card .row { padding: 3px 4px; gap: 8px; }
167780
- .activity-card .empty { padding: 4px; }
167921
+ .overlay .modal-head { display: flex; align-items: baseline; gap: 10px; }
167922
+ .overlay .modal-head h3 { margin: 0; }
167923
+ .overlay .modal-head .detail { color: var(--muted); font-size: 12px; }
167924
+ .overlay .modal-head button {
167925
+ margin-left: auto;
167926
+ background: none;
167927
+ border: none;
167928
+ color: var(--muted);
167929
+ font-size: 16px;
167930
+ cursor: pointer;
167931
+ }
167932
+ .overlay .modal-head button:hover { color: var(--text); }
167933
+ /* The tree rows reuse the log entry format, the guide marks the
167934
+ parent-child steps of the chain. */
167935
+ .trace-tree { max-height: none; }
167936
+ .trace-tree .entry .guide { color: var(--muted); flex-shrink: 0; }
167781
167937
  .row.problem .id { color: var(--bad); }
167782
167938
  .empty.good { color: var(--good); }
167783
167939
  .row .stack {
@@ -168289,6 +168445,50 @@ const rpcPanel = (main, r) => {
168289
168445
  // A terminal-style view streaming a resource's live event channel,
168290
168446
  // like the dev server output of a site. The bus replays the recent
168291
168447
  // lines, so the boot output shows even when the panel opens later.
168448
+ // Resolve a feed label back to its resource: route keys match
168449
+ // directly, while queue/stream/topic labels carry the physical name
168450
+ // their resource lists as its detail.
168451
+ const findRouteResource = route => {
168452
+ const direct = state.resources.find(r => r.routeKey === route)
168453
+
168454
+ if (direct) {
168455
+ return direct
168456
+ }
168457
+
168458
+ const [kind, name] = route.split(' ')
168459
+
168460
+ if (!name) {
168461
+ return undefined
168462
+ }
168463
+
168464
+ if (kind === 'queue') {
168465
+ return state.resources.find(r => r.kind === 'queue' && r.detail === name)
168466
+ }
168467
+
168468
+ if (kind === 'stream') {
168469
+ return state.resources.find(r => r.kind === 'table' && r.detail === name)
168470
+ }
168471
+
168472
+ if (kind === 'topic') {
168473
+ return state.resources.find(r => r.kind === 'topic' && r.detail === name)
168474
+ }
168475
+
168476
+ return undefined
168477
+ }
168478
+
168479
+ // A route tag that links through to its resource, shared by the log
168480
+ // feeds & the activity feed.
168481
+ const routeTag = route => {
168482
+ const resource = findRouteResource(route)
168483
+ const tag = $('span', { className: 'route' + (resource ? ' link' : ''), title: route }, route)
168484
+
168485
+ if (resource) {
168486
+ tag.onclick = () => selectResource(resource)
168487
+ }
168488
+
168489
+ return tag
168490
+ }
168491
+
168292
168492
  const attachLogFeed = (main, channel, route, title = 'Logs') => {
168293
168493
  if (title) {
168294
168494
  main.append($('h3', {}, title))
@@ -168320,7 +168520,7 @@ const attachLogFeed = (main, channel, route, title = 'Logs') => {
168320
168520
  feed.append($('div', { className: 'entry' + error }, [
168321
168521
  $('div', { className: 'meta' }, [
168322
168522
  $('span', { className: 'time' }, new Date(data.date).toLocaleTimeString()),
168323
- showRoute ? $('span', { className: 'route' }, data.route) : '',
168523
+ showRoute ? routeTag(data.route) : '',
168324
168524
  ]),
168325
168525
  $('div', { className: 'text' }, data.line),
168326
168526
  ]))
@@ -169326,35 +169526,139 @@ const renderHome = main => {
169326
169526
 
169327
169527
  activityCol.append($('h3', {}, 'Activity'))
169328
169528
 
169329
- const card = $('div', { className: 'activity-card' })
169330
- const activity = $('div', { className: 'rows' })
169331
- const idle = $('p', { className: 'empty' }, 'Nothing ran yet.')
169529
+ const activityFeed = $('div', { className: 'logs' }, $('p', { className: 'empty' }, 'Nothing ran yet.'))
169530
+ activityCol.append(activityFeed)
169332
169531
 
169333
- card.append(idle, activity)
169334
- activityCol.append(card)
169532
+ // The recent dispatches kept as data, not just DOM - the trace tree
169533
+ // renders straight from this buffer. It outlives the visible feed,
169534
+ // so a chain's early spans stay renderable after the feed pruned them.
169535
+ const activityData = []
169536
+ const traceCounts = new Map()
169335
169537
 
169336
- const activityRows = []
169538
+ const closeTrace = () => {
169539
+ document.querySelector('.overlay')?.remove()
169540
+ }
169541
+
169542
+ // The trace tree: the whole request chain of one trace as nested
169543
+ // spans - what ran, what caused it, how long each step took & where
169544
+ // it failed. Children hang under their caller in dispatch order.
169545
+ const openTrace = traceId => {
169546
+ closeTrace()
169547
+
169548
+ const entries = activityData.filter(entry => entry.trace === traceId)
169549
+
169550
+ if (entries.length === 0) {
169551
+ return
169552
+ }
169553
+
169554
+ const started = Math.min(...entries.map(entry => entry.date))
169555
+ const ended = Math.max(...entries.map(entry => entry.date + entry.ms))
169556
+ const failed = entries.some(entry => !entry.ok)
169557
+
169558
+ const tree = $('div', { className: 'logs trace-tree' })
169559
+ const spans = new Set(entries.map(entry => entry.span))
169560
+
169561
+ // A span whose parent already fell out of the buffer renders as
169562
+ // its own root instead of vanishing.
169563
+ const roots = entries.filter(entry => !entry.parent || !spans.has(entry.parent))
169564
+
169565
+ const renderSpan = (entry, depth) => {
169566
+ const guide = $('span', { className: 'guide' }, '\\u2514')
169567
+ guide.style.paddingLeft = ((depth - 1) * 18) + 'px'
169568
+
169569
+ tree.append($('div', { className: 'entry' + (entry.ok ? '' : ' error') }, [
169570
+ $('div', { className: 'meta' }, [
169571
+ depth > 0 ? guide : '',
169572
+ $('span', { className: 'time' }, new Date(entry.date).toLocaleTimeString()),
169573
+ routeTag(entry.route),
169574
+ $('span', { className: 'took' }, (entry.ok ? '' : '\\u2717 ') + entry.ms + 'ms'),
169575
+ ]),
169576
+ entry.ok ? '' : $('div', { className: 'text', title: entry.error ?? '' }, entry.error ?? 'failed'),
169577
+ entry.payload ? $('div', { className: 'payload' }, entry.payload) : '',
169578
+ ]))
169579
+
169580
+ for (const child of entries.filter(other => other.parent === entry.span)) {
169581
+ renderSpan(child, depth + 1)
169582
+ }
169583
+ }
169584
+
169585
+ for (const root of roots) {
169586
+ renderSpan(root, 0)
169587
+ }
169588
+
169589
+ const close = $('button', { title: 'Close' }, '\\u2715')
169590
+ const overlay = $('div', { className: 'overlay' }, $('div', { className: 'modal' }, [
169591
+ $('div', { className: 'modal-head' }, [
169592
+ $('h3', {}, 'Trace ' + traceId),
169593
+ $('span', { className: 'detail' },
169594
+ entries.length + ' spans \\u00b7 ' + (ended - started) + 'ms' + (failed ? ' \\u00b7 failed' : '')),
169595
+ close,
169596
+ ]),
169597
+ tree,
169598
+ ]))
169599
+
169600
+ close.onclick = closeTrace
169601
+ overlay.onclick = event => {
169602
+ if (event.target === overlay) closeTrace()
169603
+ }
169604
+
169605
+ document.body.append(overlay)
169606
+ }
169607
+
169608
+ const traceChip = traceId => {
169609
+ const chip = $('span', { className: 'trace-link', title: 'View the whole request chain' }, 'trace')
169610
+ chip.onclick = () => openTrace(traceId)
169611
+ return chip
169612
+ }
169613
+
169614
+ // A lone dispatch shows no chip - once a second span of its trace
169615
+ // arrives, every entry of the chain gets one, retroactively.
169616
+ const markTraced = traceId => {
169617
+ for (const entry of activityFeed.querySelectorAll('[data-trace="' + traceId + '"]')) {
169618
+ if (!entry.querySelector('.trace-link')) {
169619
+ entry.querySelector('.meta')?.append(traceChip(traceId))
169620
+ }
169621
+ }
169622
+ }
169337
169623
 
169338
169624
  const activityRow = data => {
169339
- idle.hidden = true
169625
+ activityFeed.querySelector('.empty')?.remove()
169626
+
169627
+ activityData.push(data)
169340
169628
 
169341
- const resource = state.resources.find(r => r.routeKey === data.route)
169342
- const row = $('button', { className: 'row' + (data.ok ? '' : ' problem') }, [
169343
- $('span', { className: 'stack' }, new Date(data.date).toLocaleTimeString()),
169344
- $('span', { className: 'id', title: data.route }, (data.ok ? '' : '\\u2717 ') + data.route),
169345
- $('span', { className: 'info', title: data.ok ? '' : (data.error ?? '') }, data.ok ? data.ms + 'ms' : (data.error ?? 'failed').slice(0, 80)),
169629
+ while (activityData.length > 200) {
169630
+ activityData.shift()
169631
+ }
169632
+
169633
+ // The same entry format as the log feeds: meta header with the
169634
+ // linked route tag & the duration, the payload truncated on its
169635
+ // own line with the full value on hover.
169636
+ const row = $('div', { className: 'entry' + (data.ok ? '' : ' error') }, [
169637
+ $('div', { className: 'meta' }, [
169638
+ $('span', { className: 'time' }, new Date(data.date).toLocaleTimeString()),
169639
+ routeTag(data.route),
169640
+ $('span', { className: 'took' }, (data.ok ? '' : '\\u2717 ') + data.ms + 'ms'),
169641
+ ]),
169642
+ data.ok ? '' : $('div', { className: 'text', title: data.error ?? '' }, data.error ?? 'failed'),
169643
+ data.payload ? $('div', { className: 'payload' }, data.payload) : '',
169346
169644
  ])
169347
169645
 
169348
- if (resource) {
169349
- row.onclick = () => selectResource(resource)
169646
+ if (data.trace) {
169647
+ row.setAttribute('data-trace', data.trace)
169648
+ traceCounts.set(data.trace, (traceCounts.get(data.trace) ?? 0) + 1)
169350
169649
  }
169351
169650
 
169352
- activity.prepend(row)
169353
- activityRows.push(row)
169651
+ activityFeed.append(row)
169652
+
169653
+ if (data.trace && traceCounts.get(data.trace) > 1) {
169654
+ markTraced(data.trace)
169655
+ }
169354
169656
 
169355
- while (activityRows.length > 50) {
169356
- activityRows.shift().remove()
169657
+ while (activityFeed.children.length > 50) {
169658
+ activityFeed.firstChild.remove()
169357
169659
  }
169660
+
169661
+ activityFeed.scrollTop = activityFeed.scrollHeight
169358
169662
  }
169359
169663
 
169360
169664
  const activitySource = new EventSource('/api/events?channel=activity')
@@ -169372,6 +169676,7 @@ const renderHome = main => {
169372
169676
 
169373
169677
  cleanupPanel = () => {
169374
169678
  clearInterval(uptimeTimer)
169679
+ closeTrace()
169375
169680
  healthSource.close()
169376
169681
  problemSource.close()
169377
169682
  activitySource.close()
@@ -169404,6 +169709,7 @@ try {
169404
169709
  id: route.pattern,
169405
169710
  stack: route.routeKey ? route.routeKey.split(':')[0] : undefined,
169406
169711
  router: route.routerId,
169712
+ routeKey: route.routeKey,
169407
169713
  detail: route.routeKey ?? ('proxy \u2192 ' + route.proxy),
169408
169714
  url: 'http://localhost:' + state.routerPorts[route.routerId] + route.pattern.split('{')[0].replace(/\\*$/, ''),
169409
169715
  })),
@@ -169802,7 +170108,8 @@ var createFailureReporter = (props) => {
169802
170108
  return;
169803
170109
  }
169804
170110
  const event = JSON.parse(stringify5(format4(report)));
169805
- props.dispatch(formatRoutePayload2(CONSUMER_ROUTE, event)).catch((error3) => {
170111
+ const parent = report.error?.trace;
170112
+ props.dispatch(formatRoutePayload2(CONSUMER_ROUTE, event), parent).catch((error3) => {
169806
170113
  props.log(`The on-failure consumer itself failed: ${error3 instanceof Error ? error3.message : String(error3)}`);
169807
170114
  });
169808
170115
  };
@@ -169875,10 +170182,11 @@ var createLambdaServer = (props) => {
169875
170182
  }
169876
170183
  const type2 = String(req.headers["x-amz-invocation-type"] ?? "RequestResponse");
169877
170184
  const event = JSON.parse(body.toString() || "{}");
170185
+ const trace = parseTraceHeader(req.headers[TRACE_HEADER]);
169878
170186
  if (type2 === "Event") {
169879
170187
  res.writeHead(202, { "content-type": "application/json" });
169880
170188
  res.end();
169881
- dispatch?.(event).catch((error3) => {
170189
+ dispatch?.(event, trace).catch((error3) => {
169882
170190
  const routeKey = event?.["$awsless-route"];
169883
170191
  reportFailure?.({
169884
170192
  kind: "async",
@@ -169890,7 +170198,7 @@ var createLambdaServer = (props) => {
169890
170198
  return;
169891
170199
  }
169892
170200
  try {
169893
- const result = await dispatch?.(event);
170201
+ const result = await dispatch?.(event, trace);
169894
170202
  res.writeHead(200, { "content-type": "application/json" });
169895
170203
  res.end(typeof result === "undefined" || result === null ? "" : JSON.stringify(result));
169896
170204
  } catch (error3) {
@@ -170162,21 +170470,48 @@ var startDev = async (props) => {
170162
170470
  }
170163
170471
  return "invoke";
170164
170472
  };
170165
- const dispatch = async (event) => {
170473
+ const describePayload = (event) => {
170474
+ let payload = "";
170475
+ try {
170476
+ const envelope = event;
170477
+ const inner = typeof envelope?.["$awsless-route"] === "string" ? envelope.event : event;
170478
+ payload = JSON.stringify(inner) ?? "";
170479
+ } catch (_4) {}
170480
+ return payload.length > 1000 ? payload.slice(0, 1000) + "\u2026" : payload;
170481
+ };
170482
+ const dispatch = async (event, parent) => {
170166
170483
  await ensureFresh();
170167
170484
  const started = Date.now();
170168
170485
  const route = describeDispatch(event);
170486
+ const payload = describePayload(event);
170487
+ const trace = { traceId: parent?.traceId ?? traceId(), spanId: traceId() };
170169
170488
  try {
170170
- const result = await worker.dispatch(event);
170171
- dev.events.emit("activity", { date: started, route, ms: Date.now() - started, ok: true });
170489
+ const result = await worker.dispatch(event, formatTraceHeader(trace));
170490
+ dev.events.emit("activity", {
170491
+ date: started,
170492
+ route,
170493
+ payload,
170494
+ ms: Date.now() - started,
170495
+ ok: true,
170496
+ trace: trace.traceId,
170497
+ span: trace.spanId,
170498
+ parent: parent?.spanId
170499
+ });
170172
170500
  return result;
170173
170501
  } catch (error3) {
170502
+ if (error3 instanceof Error) {
170503
+ error3.trace = trace;
170504
+ }
170174
170505
  dev.events.emit("activity", {
170175
170506
  date: started,
170176
170507
  route,
170508
+ payload,
170177
170509
  ms: Date.now() - started,
170178
170510
  ok: false,
170179
- error: error3 instanceof Error ? error3.message : String(error3)
170511
+ error: error3 instanceof Error ? error3.message : String(error3),
170512
+ trace: trace.traceId,
170513
+ span: trace.spanId,
170514
+ parent: parent?.spanId
170180
170515
  });
170181
170516
  throw error3;
170182
170517
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awsless/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -38,17 +38,17 @@
38
38
  "@opensearch-project/opensearch": "^2.13.0",
39
39
  "rolldown": "1.2.1",
40
40
  "vitest": "4.1.10",
41
- "@awsless/dynamodb-server": "^0.1.8",
42
- "@awsless/ts-file-cache": "^0.0.16"
41
+ "@awsless/ts-file-cache": "^0.0.16",
42
+ "@awsless/dynamodb-server": "^0.1.10"
43
43
  },
44
44
  "peerDependencies": {
45
+ "@awsless/big-float": "^0.1.7",
45
46
  "@awsless/duration": "^0.0.4",
47
+ "@awsless/dynamodb": "^0.3.25",
46
48
  "@awsless/json": "^0.0.11",
47
49
  "@awsless/lambda": "^0.0.48",
48
- "@awsless/big-float": "^0.1.7",
49
- "@awsless/dynamodb": "^0.3.23",
50
- "@awsless/validate": "^0.2.0",
51
50
  "@awsless/s3": "^0.0.22",
51
+ "@awsless/validate": "^0.2.0",
52
52
  "awsless": "^0.1.4",
53
53
  "@awsless/weak-cache": "^0.0.1"
54
54
  },
@@ -109,24 +109,24 @@
109
109
  "zod": "^3.24.2",
110
110
  "zod-to-json-schema": "^3.24.3",
111
111
  "@awsless/big-float": "^0.1.7",
112
- "@awsless/clui": "^0.0.9",
113
112
  "@awsless/cloudwatch": "^0.0.1",
114
- "@awsless/dynamodb": "^0.3.23",
115
113
  "@awsless/duration": "^0.0.4",
116
114
  "@awsless/json": "^0.0.11",
117
- "@awsless/open-search": "^0.0.28",
115
+ "@awsless/dynamodb": "^0.3.25",
118
116
  "@awsless/iot": "^0.0.5",
119
- "@awsless/redis": "^0.1.13",
120
- "@awsless/s3": "^0.0.22",
117
+ "@awsless/clui": "^0.0.9",
118
+ "@awsless/open-search": "^0.0.28",
119
+ "@awsless/scheduler": "^0.0.5",
121
120
  "@awsless/size": "^0.0.2",
122
- "@awsless/lambda": "^0.0.48",
123
121
  "@awsless/sns": "^0.0.11",
124
- "@awsless/scheduler": "^0.0.5",
122
+ "@awsless/validate": "^0.2.0",
123
+ "@awsless/redis": "^0.1.13",
125
124
  "@awsless/sqs": "^0.0.24",
125
+ "@awsless/lambda": "^0.0.48",
126
126
  "@awsless/ssm": "^0.0.8",
127
- "@awsless/validate": "^0.2.0",
128
127
  "@awsless/weak-cache": "^0.0.1",
129
- "awsless": "^0.1.4"
128
+ "awsless": "^0.1.4",
129
+ "@awsless/s3": "^0.0.22"
130
130
  },
131
131
  "scripts": {
132
132
  "test": "bun cli/build-handlers.ts && pnpm vitest",