@lunora/container 1.0.0-alpha.10 → 1.0.0-alpha.11

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/dist/index.d.mts CHANGED
@@ -209,12 +209,15 @@ interface ContainerBindingSpec {
209
209
  }
210
210
  /**
211
211
  * Build the `ctx.containers` record from the Worker `env`. Called by the
212
- * generated ShardDO with the specs codegen derived from
213
- * `lunora/containers.ts`. A missing binding doesn't throw here — only when the
214
- * handle is actually used — so one unprovisioned container never breaks
215
- * unrelated functions.
212
+ * generated ShardDO with the specs codegen derived from `lunora/containers.ts`.
213
+ * A missing binding doesn't throw here — only when the handle is actually used —
214
+ * so one unprovisioned container never breaks unrelated functions.
215
+ *
216
+ * `traceparent` (the inbound RPC's W3C trace context, forwarded by the runtime
217
+ * and read off the request by the DO) is stamped onto every outbound container
218
+ * `fetch`, so the container's own spans stitch under the Worker's trace.
216
219
  */
217
- declare const createContainerContext: (env: Record<string, unknown>, specs: ReadonlyArray<ContainerBindingSpec>, jurisdiction?: DurableObjectJurisdiction) => Record<string, ContainerAccessor>;
220
+ declare const createContainerContext: (env: Record<string, unknown>, specs: ReadonlyArray<ContainerBindingSpec>, jurisdiction?: DurableObjectJurisdiction, traceparent?: string) => Record<string, ContainerAccessor>;
218
221
  /** A test handler: receives the request plus the targeted instance name. */
219
222
  type ContainerTestHandler = (request: Request, instance: {
220
223
  name: string;
package/dist/index.d.ts CHANGED
@@ -209,12 +209,15 @@ interface ContainerBindingSpec {
209
209
  }
210
210
  /**
211
211
  * Build the `ctx.containers` record from the Worker `env`. Called by the
212
- * generated ShardDO with the specs codegen derived from
213
- * `lunora/containers.ts`. A missing binding doesn't throw here — only when the
214
- * handle is actually used — so one unprovisioned container never breaks
215
- * unrelated functions.
212
+ * generated ShardDO with the specs codegen derived from `lunora/containers.ts`.
213
+ * A missing binding doesn't throw here — only when the handle is actually used —
214
+ * so one unprovisioned container never breaks unrelated functions.
215
+ *
216
+ * `traceparent` (the inbound RPC's W3C trace context, forwarded by the runtime
217
+ * and read off the request by the DO) is stamped onto every outbound container
218
+ * `fetch`, so the container's own spans stitch under the Worker's trace.
216
219
  */
217
- declare const createContainerContext: (env: Record<string, unknown>, specs: ReadonlyArray<ContainerBindingSpec>, jurisdiction?: DurableObjectJurisdiction) => Record<string, ContainerAccessor>;
220
+ declare const createContainerContext: (env: Record<string, unknown>, specs: ReadonlyArray<ContainerBindingSpec>, jurisdiction?: DurableObjectJurisdiction, traceparent?: string) => Record<string, ContainerAccessor>;
218
221
  /** A test handler: receives the request plus the targeted instance name. */
219
222
  type ContainerTestHandler = (request: Request, instance: {
220
223
  name: string;
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export { createContainerContext, createContainerTestContext } from './packem_shared/createContainerContext-CIVzsY5m.mjs';
1
+ export { createContainerContext, createContainerTestContext } from './packem_shared/createContainerContext-Cg53QGdf.mjs';
2
2
  export { containerBindingName, containerBuildTag, containerClassName, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVars } from './packem_shared/containerBindingName-BiTrAF1J.mjs';
package/dist/otel.d.mts CHANGED
@@ -62,6 +62,29 @@ interface ContainerTelemetryOptions {
62
62
  timeoutMs?: number;
63
63
  /** Bearer token sent as an `Authorization: Bearer` header; defaults to the `LUNORA_OTLP_TOKEN` env var. */
64
64
  token?: string;
65
+ /**
66
+ * W3C `traceparent` of the Worker RPC that invoked this container; defaults to
67
+ * the `LUNORA_TRACEPARENT` env var. When present (and well-formed) every span
68
+ * inherits its trace id and hangs off its span id, so container spans stitch
69
+ * under the Worker's trace instead of forming a fresh, disconnected trace.
70
+ *
71
+ * `@lunora/container` stamps this trace context as the **`traceparent` request
72
+ * header** on every proxied fetch (`ctx.containers.&lt;name>.…`), so a container
73
+ * that serves many requests should read it per request and create a telemetry
74
+ * instance scoped to that request — the trace context differs each call, so a
75
+ * single process-lifetime instance can't carry it:
76
+ *
77
+ * ```ts
78
+ * // inside the container's request handler
79
+ * const telemetry = createContainerTelemetry({ traceparent: request.headers.get("traceparent") ?? undefined });
80
+ * await telemetry.trace("transcode", () => transcode(job));
81
+ * await telemetry.flush();
82
+ * ```
83
+ *
84
+ * The `LUNORA_TRACEPARENT` env fallback fits a one-shot container that
85
+ * processes a single job per start (the value is fixed for the process).
86
+ */
87
+ traceparent?: string;
65
88
  }
66
89
  /** The exporter handle {@link createContainerTelemetry} returns. */
67
90
  interface ContainerTelemetry {
package/dist/otel.d.ts CHANGED
@@ -62,6 +62,29 @@ interface ContainerTelemetryOptions {
62
62
  timeoutMs?: number;
63
63
  /** Bearer token sent as an `Authorization: Bearer` header; defaults to the `LUNORA_OTLP_TOKEN` env var. */
64
64
  token?: string;
65
+ /**
66
+ * W3C `traceparent` of the Worker RPC that invoked this container; defaults to
67
+ * the `LUNORA_TRACEPARENT` env var. When present (and well-formed) every span
68
+ * inherits its trace id and hangs off its span id, so container spans stitch
69
+ * under the Worker's trace instead of forming a fresh, disconnected trace.
70
+ *
71
+ * `@lunora/container` stamps this trace context as the **`traceparent` request
72
+ * header** on every proxied fetch (`ctx.containers.&lt;name>.…`), so a container
73
+ * that serves many requests should read it per request and create a telemetry
74
+ * instance scoped to that request — the trace context differs each call, so a
75
+ * single process-lifetime instance can't carry it:
76
+ *
77
+ * ```ts
78
+ * // inside the container's request handler
79
+ * const telemetry = createContainerTelemetry({ traceparent: request.headers.get("traceparent") ?? undefined });
80
+ * await telemetry.trace("transcode", () => transcode(job));
81
+ * await telemetry.flush();
82
+ * ```
83
+ *
84
+ * The `LUNORA_TRACEPARENT` env fallback fits a one-shot container that
85
+ * processes a single job per start (the value is fixed for the process).
86
+ */
87
+ traceparent?: string;
65
88
  }
66
89
  /** The exporter handle {@link createContainerTelemetry} returns. */
67
90
  interface ContainerTelemetry {
package/dist/otel.mjs CHANGED
@@ -20,6 +20,19 @@ const otlpRandomHex = (bytes) => {
20
20
  }
21
21
  return hex;
22
22
  };
23
+ const HEX_ONLY = /^[0-9a-f]+$/;
24
+ const parseTraceparent = (header) => {
25
+ if (header === null || header === void 0) {
26
+ return void 0;
27
+ }
28
+ const parts = header.trim().toLowerCase().split("-");
29
+ const [version, traceId, parentSpanId, flags] = parts;
30
+ if (parts.length < 4 || version === void 0 || version.length !== 2 || !HEX_ONLY.test(version) || version === "ff" || // Version 00 forbids trailing fields; only a future version may carry them.
31
+ version === "00" && parts.length !== 4 || traceId === void 0 || parentSpanId === void 0 || flags === void 0 || flags.length !== 2 || !HEX_ONLY.test(flags) || traceId.length !== 32 || parentSpanId.length !== 16 || !HEX_ONLY.test(traceId) || !HEX_ONLY.test(parentSpanId) || traceId === "00000000000000000000000000000000" || parentSpanId === "0000000000000000") {
32
+ return void 0;
33
+ }
34
+ return { parentSpanId, traceId };
35
+ };
23
36
  const encodeAttribute = (key, value) => {
24
37
  if (typeof value === "boolean") {
25
38
  return { key, value: { boolValue: value } };
@@ -96,7 +109,7 @@ const resolveFetch = (injected) => {
96
109
  }
97
110
  return void 0;
98
111
  };
99
- const traceBody = (span, serviceName) => {
112
+ const traceBody = (span, serviceName, parent) => {
100
113
  const attributes = encodeAttributes(span.attributes);
101
114
  if (span.error?.type !== void 0) {
102
115
  attributes.push(encodeAttribute("error.type", span.error.type));
@@ -107,11 +120,14 @@ const traceBody = (span, serviceName) => {
107
120
  // SPAN_KIND_INTERNAL — the container's own work, not a server/client edge.
108
121
  kind: 1,
109
122
  name: span.name,
123
+ // Always the span's own (child) id; with a parent, hang it off the parent
124
+ // span and inherit the parent's trace id so the spans stitch into one trace.
125
+ ...parent === void 0 ? {} : { parentSpanId: parent.parentSpanId },
110
126
  spanId: otlpRandomHex(8),
111
127
  startTimeUnixNano: otlpUnixNano(span.startMs),
112
128
  // STATUS_CODE_OK (1) / STATUS_CODE_ERROR (2).
113
129
  status: span.error === void 0 ? { code: 1 } : { code: 2, message: span.error.message },
114
- traceId: otlpRandomHex(16)
130
+ traceId: parent?.traceId ?? otlpRandomHex(16)
115
131
  };
116
132
  return wrapResourceSpans(otlpSpan, "@lunora/container", serviceName);
117
133
  };
@@ -131,6 +147,7 @@ const createContainerTelemetry = (options = {}) => {
131
147
  const enabled = endpoint !== void 0 && endpoint.length > 0;
132
148
  const token = options.token ?? readEnv("LUNORA_OTLP_TOKEN");
133
149
  const serviceName = options.serviceName ?? readEnv("LUNORA_SERVICE_NAME") ?? "lunora-container";
150
+ const parent = parseTraceparent(options.traceparent ?? readEnv("LUNORA_TRACEPARENT"));
134
151
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
135
152
  const fetchImpl = resolveFetch(options.fetch);
136
153
  const headers = mergeHeaders({ "content-type": "application/json" }, options.headers, token);
@@ -169,7 +186,7 @@ const createContainerTelemetry = (options = {}) => {
169
186
  if (!enabled) {
170
187
  return;
171
188
  }
172
- send(tracesUrl, traceBody(span, serviceName));
189
+ send(tracesUrl, traceBody(span, serviceName, parent));
173
190
  };
174
191
  const emitLog = (log) => {
175
192
  if (!enabled) {
@@ -7,11 +7,14 @@ const DEFAULT_MAX_BACKOFF_MS = 3e4;
7
7
  const DEFAULT_COLD_START_ATTEMPTS = 3;
8
8
  const DEFAULT_COLD_START_BACKOFF_MS = 500;
9
9
  const TARGET_PORT_HEADER = "cf-container-target-port";
10
- const toRequest = (input, init, port) => {
10
+ const toRequest = (input, init, port, traceparent) => {
11
11
  const request = typeof input === "string" && input.startsWith("/") ? new Request(`http://container${input}`, init) : new Request(input, init);
12
12
  if (port !== void 0) {
13
13
  request.headers.set(TARGET_PORT_HEADER, String(port));
14
14
  }
15
+ if (traceparent !== void 0) {
16
+ request.headers.set("traceparent", traceparent);
17
+ }
15
18
  return request;
16
19
  };
17
20
  const sleep = async (ms) => {
@@ -62,7 +65,7 @@ const isColdStartTransient = async (response) => {
62
65
  return false;
63
66
  }
64
67
  };
65
- const coldStartRetryingHandle = (send, options = {}, port) => {
68
+ const coldStartRetryingHandle = (send, options = {}, port, traceparent) => {
66
69
  const attempts = Math.max(1, options.attempts ?? DEFAULT_COLD_START_ATTEMPTS);
67
70
  const baseBackoff = options.backoffMs ?? DEFAULT_COLD_START_BACKOFF_MS;
68
71
  const maxBackoff = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
@@ -76,7 +79,7 @@ const coldStartRetryingHandle = (send, options = {}, port) => {
76
79
  await sleep(Math.min(baseBackoff * 2 ** (attempt - 1), maxBackoff));
77
80
  }
78
81
  try {
79
- const response = await send(toRequest(input, init, port));
82
+ const response = await send(toRequest(input, init, port, traceparent));
80
83
  if (isLastAttempt || !await isColdStartTransient(response)) {
81
84
  return response;
82
85
  }
@@ -89,10 +92,10 @@ const coldStartRetryingHandle = (send, options = {}, port) => {
89
92
  }
90
93
  throw lastError instanceof Error ? lastError : new Error("ctx.containers: cold-start retry exhausted");
91
94
  },
92
- port: (targetPort) => coldStartRetryingHandle(send, options, targetPort)
95
+ port: (targetPort) => coldStartRetryingHandle(send, options, targetPort, traceparent)
93
96
  };
94
97
  };
95
- const handleFor = (namespace, instanceName, options) => coldStartRetryingHandle(async (request) => namespace.get(namespace.idFromName(instanceName)).fetch(request), options);
98
+ const handleFor = (namespace, instanceName, options, traceparent) => coldStartRetryingHandle(async (request) => namespace.get(namespace.idFromName(instanceName)).fetch(request), options, void 0, traceparent);
96
99
  const lifecycleCall = async (stub, method, binding, argument) => {
97
100
  const rpc = stub[method];
98
101
  if (typeof rpc !== "function") {
@@ -110,10 +113,10 @@ const egressControlsFor = (stub, binding) => {
110
113
  setDenied: async (hosts) => lifecycleCall(stub(), "setDeniedHosts", binding, [...hosts])
111
114
  };
112
115
  };
113
- const instanceHandleFor = (namespace, spec, instanceName, options) => {
116
+ const instanceHandleFor = (namespace, spec, instanceName, options, traceparent) => {
114
117
  const stub = () => namespace.get(namespace.idFromName(instanceName));
115
118
  return {
116
- ...coldStartRetryingHandle(async (request) => stub().fetch(request), options),
119
+ ...coldStartRetryingHandle(async (request) => stub().fetch(request), options, void 0, traceparent),
117
120
  destroy: async () => lifecycleCall(stub(), "destroy", spec.binding),
118
121
  egress: egressControlsFor(stub, spec.binding),
119
122
  getState: async () => lifecycleCall(stub(), "getState", spec.binding),
@@ -127,7 +130,7 @@ const randomPoolName = (size) => (
127
130
  `pool-${String(Math.floor(Math.random() * size))}`
128
131
  );
129
132
  const retryOnServerError = (response) => response.status >= 500;
130
- const poolHandleFor = (namespace, spec, options = {}, port) => {
133
+ const poolHandleFor = (namespace, spec, options = {}, port, traceparent) => {
131
134
  const size = options.size ?? spec.maxInstances ?? DEFAULT_POOL_SIZE;
132
135
  const attempts = Math.max(1, options.attempts ?? 3);
133
136
  const baseBackoff = options.backoffMs ?? 100;
@@ -141,7 +144,7 @@ const poolHandleFor = (namespace, spec, options = {}, port) => {
141
144
  if (attempt > 0) {
142
145
  await sleep(Math.min(baseBackoff * 2 ** (attempt - 1), maxBackoff));
143
146
  }
144
- const request = toRequest(input, init, port);
147
+ const request = toRequest(input, init, port, traceparent);
145
148
  try {
146
149
  const response = await namespace.get(namespace.idFromName(randomPoolName(size))).fetch(request);
147
150
  if (attempt === totalAttempts - 1 || !shouldRetry(response)) {
@@ -153,14 +156,14 @@ const poolHandleFor = (namespace, spec, options = {}, port) => {
153
156
  }
154
157
  throw lastError instanceof Error ? lastError : new Error(`ctx.containers.${spec.exportName}.pool(): all ${String(totalAttempts)} attempts failed`);
155
158
  },
156
- port: (targetPort) => poolHandleFor(namespace, spec, options, targetPort)
159
+ port: (targetPort) => poolHandleFor(namespace, spec, options, targetPort, traceparent)
157
160
  };
158
161
  };
159
- const accessorFor = (namespace, spec) => {
162
+ const accessorFor = (namespace, spec, traceparent) => {
160
163
  return {
161
- any: (count, options) => handleFor(namespace, randomPoolName(count ?? spec.maxInstances ?? DEFAULT_POOL_SIZE), options),
162
- get: (name, options) => instanceHandleFor(namespace, spec, name, options),
163
- pool: (options) => poolHandleFor(namespace, spec, options)
164
+ any: (count, options) => handleFor(namespace, randomPoolName(count ?? spec.maxInstances ?? DEFAULT_POOL_SIZE), options, traceparent),
165
+ get: (name, options) => instanceHandleFor(namespace, spec, name, options, traceparent),
166
+ pool: (options) => poolHandleFor(namespace, spec, options, void 0, traceparent)
164
167
  };
165
168
  };
166
169
  const missingBindingAccessor = (spec) => {
@@ -172,11 +175,11 @@ const missingBindingAccessor = (spec) => {
172
175
  };
173
176
  return { any: fail, get: fail, pool: fail };
174
177
  };
175
- const createContainerContext = (env, specs, jurisdiction) => {
178
+ const createContainerContext = (env, specs, jurisdiction, traceparent) => {
176
179
  const containers = {};
177
180
  for (const spec of specs) {
178
181
  const binding = env[spec.binding];
179
- containers[spec.exportName] = binding && typeof binding.idFromName === "function" && typeof binding.get === "function" ? accessorFor(applyJurisdiction(binding, jurisdiction), spec) : missingBindingAccessor(spec);
182
+ containers[spec.exportName] = binding && typeof binding.idFromName === "function" && typeof binding.get === "function" ? accessorFor(applyJurisdiction(binding, jurisdiction), spec, traceparent) : missingBindingAccessor(spec);
180
183
  }
181
184
  return containers;
182
185
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/container",
3
- "version": "1.0.0-alpha.10",
3
+ "version": "1.0.0-alpha.11",
4
4
  "description": "Cloudflare Containers for Lunora: defineContainer, generated Container DO classes, and the ctx.containers action surface",
5
5
  "keywords": [
6
6
  "cloudflare",