@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 +8 -5
- package/dist/index.d.ts +8 -5
- package/dist/index.mjs +1 -1
- package/dist/otel.d.mts +23 -0
- package/dist/otel.d.ts +23 -0
- package/dist/otel.mjs +20 -3
- package/dist/packem_shared/{createContainerContext-CIVzsY5m.mjs → createContainerContext-Cg53QGdf.mjs} +19 -16
- package/package.json +1 -1
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
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
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
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
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-
|
|
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.<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.<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