@decocms/start 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION_TOOLING_PLAN.md +9 -0
- package/docs/observability.md +20 -10
- package/package.json +1 -1
- package/scripts/generate-invoke.test.ts +83 -56
- package/scripts/generate-invoke.ts +26 -10
- package/src/middleware/observability.test.ts +237 -0
- package/src/middleware/observability.ts +165 -8
- package/src/sdk/cachedLoader.ts +10 -7
- package/src/sdk/logger.test.ts +99 -0
- package/src/sdk/logger.ts +18 -7
- package/src/sdk/observability.ts +18 -0
- package/src/sdk/otel.ts +228 -38
- package/src/sdk/otelHttpTracer.test.ts +422 -0
- package/src/sdk/otelHttpTracer.ts +489 -0
- package/src/sdk/requestContext.ts +46 -0
- package/src/sdk/workerEntry.ts +138 -17
package/src/sdk/otel.ts
CHANGED
|
@@ -61,10 +61,16 @@
|
|
|
61
61
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
62
62
|
import { createCompositeLogger, createCompositeMeter } from "./composite";
|
|
63
63
|
import { configureLogger, defaultLoggerAdapter, setLoggerAttributeFloor } from "./logger";
|
|
64
|
-
import { configureMeter, configureTracer } from "./observability";
|
|
64
|
+
import { configureMeter, configureTracer, getActiveSpan } from "./observability";
|
|
65
65
|
import { createAnalyticsEngineMeterAdapter } from "./otelAdapters";
|
|
66
66
|
import { createOtlpHttpErrorLogAdapter, type OtlpHttpErrorLog } from "./otelHttpErrorLog";
|
|
67
67
|
import { createOtlpHttpMeterAdapter, type OtlpHttpMeter } from "./otelHttpMeter";
|
|
68
|
+
import {
|
|
69
|
+
createOtlpHttpTracerAdapter,
|
|
70
|
+
type OtlpHttpTracer,
|
|
71
|
+
type TraceContext,
|
|
72
|
+
} from "./otelHttpTracer";
|
|
73
|
+
import { RequestContext } from "./requestContext";
|
|
68
74
|
|
|
69
75
|
// ---------------------------------------------------------------------------
|
|
70
76
|
// Types
|
|
@@ -98,6 +104,36 @@ export interface OtelOptions {
|
|
|
98
104
|
otlpErrorLogsEndpointEnvVar?: string;
|
|
99
105
|
/** Set to `false` to disable the OTLP/HTTP error-log exporter explicitly. */
|
|
100
106
|
otlpErrorLogsEnabled?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Env var name holding the OTLP/HTTP traces endpoint used by the
|
|
109
|
+
* direct-POST span exporter. Defaults to `"DECO_OTEL_TRACES_ENDPOINT"`.
|
|
110
|
+
* When set (and `otlpTracesEnabled !== false`), framework `deco.*`
|
|
111
|
+
* spans created via `withTracing` are captured, sampled, and POSTed
|
|
112
|
+
* directly to this endpoint. Flushed alongside metrics via
|
|
113
|
+
* `ctx.waitUntil`.
|
|
114
|
+
*
|
|
115
|
+
* Without this endpoint configured, `withTracing` falls back to the
|
|
116
|
+
* `@opentelemetry/api` global tracer (the legacy CF auto-instrumentation
|
|
117
|
+
* path). The framework registers BOTH adapters when a traces endpoint
|
|
118
|
+
* is set so CF auto-spans stay intact AND framework spans get
|
|
119
|
+
* direct-POSTed to ClickHouse. See Phase 3 in
|
|
120
|
+
* `MIGRATION_TOOLING_PLAN.md`.
|
|
121
|
+
*/
|
|
122
|
+
otlpTracesEndpointEnvVar?: string;
|
|
123
|
+
/** Set to `false` to disable the OTLP/HTTP traces exporter explicitly. */
|
|
124
|
+
otlpTracesEnabled?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Head sampling rate for framework spans direct-POSTed via the OTLP
|
|
127
|
+
* traces endpoint. Default `0.01` matches the CF Destinations
|
|
128
|
+
* `traces.head_sampling_rate` recommendation. Decisions are consistent
|
|
129
|
+
* per trace (hash of `trace_id`), so child spans (`deco.cache.lookup`,
|
|
130
|
+
* `deco.cms.resolvePage`, ...) are kept iff their root
|
|
131
|
+
* `deco.http.request` span is kept. Set to `1` to capture every trace
|
|
132
|
+
* (preview / debug only — production cost grows linearly).
|
|
133
|
+
*/
|
|
134
|
+
otlpTracesSamplingRate?: number;
|
|
135
|
+
/** Test seam — replace the global `fetch` used by the traces exporter. */
|
|
136
|
+
otlpTracesFetchImpl?: typeof fetch;
|
|
101
137
|
/**
|
|
102
138
|
* Version of `@decocms/start` to advertise as `deco.runtime.version`
|
|
103
139
|
* on every span and every log line. Falls back to a build-time constant;
|
|
@@ -150,6 +186,37 @@ let otlpMeter: OtlpHttpMeter | null = null;
|
|
|
150
186
|
*/
|
|
151
187
|
let otlpErrorLog: OtlpHttpErrorLog | null = null;
|
|
152
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Module-level handle to the OTLP/HTTP trace exporter — installed by
|
|
191
|
+
* `bootObservability` when `DECO_OTEL_TRACES_ENDPOINT` is set on `env`.
|
|
192
|
+
* Flushed alongside the metrics + error-log exporters via
|
|
193
|
+
* `ctx.waitUntil(...)` at the end of every request.
|
|
194
|
+
*/
|
|
195
|
+
let otlpTracer: OtlpHttpTracer | null = null;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Per-request inbound W3C trace context, parsed from the `traceparent`
|
|
199
|
+
* header at request entry. Read by the OTLP trace exporter when it
|
|
200
|
+
* creates a root span so we honor remote parents and the `sampled`
|
|
201
|
+
* flag. Stored on a request-scoped slot (via `RequestContext.bag`) so
|
|
202
|
+
* concurrent requests in the same isolate don't trample each other.
|
|
203
|
+
*/
|
|
204
|
+
const TRACE_CTX_BAG_KEY = "deco.observability.traceContext.v1";
|
|
205
|
+
|
|
206
|
+
function getRequestTraceContext(): TraceContext | null {
|
|
207
|
+
return RequestContext.getBag<TraceContext>(TRACE_CTX_BAG_KEY) ?? null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Public entry point used by `workerEntry.ts` to stash the parsed
|
|
212
|
+
* traceparent for the OTLP tracer to consume. Exported (not just
|
|
213
|
+
* module-local) because the parser lives in `otelHttpTracer.ts` and
|
|
214
|
+
* the call site is `workerEntry.ts`.
|
|
215
|
+
*/
|
|
216
|
+
export function _setRequestTraceContext(ctx: TraceContext | null): void {
|
|
217
|
+
if (ctx) RequestContext.setBag(TRACE_CTX_BAG_KEY, ctx);
|
|
218
|
+
}
|
|
219
|
+
|
|
153
220
|
/**
|
|
154
221
|
* Per-span attribute floor — stamped on every span we create via
|
|
155
222
|
* `configureTracer().startSpan(...)`. CF's trace export emits its own
|
|
@@ -183,38 +250,13 @@ export function instrumentWorker(
|
|
|
183
250
|
handler: WorkerHandler,
|
|
184
251
|
options: OtelOptions | ((env: Record<string, unknown>) => OtelOptions) = {},
|
|
185
252
|
): WorkerHandler {
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
// tracing the global tracer is a no-op proxy and the spans simply drop
|
|
194
|
-
// — same outcome as before, no error.
|
|
195
|
-
configureTracer({
|
|
196
|
-
startSpan: (name, attrs) => {
|
|
197
|
-
const merged = { ...spanAttributeFloor, ...(attrs ?? {}) };
|
|
198
|
-
const span = trace.getTracer("@decocms/start").startSpan(name, { attributes: merged });
|
|
199
|
-
return {
|
|
200
|
-
end: () => span.end(),
|
|
201
|
-
setError: (error) => {
|
|
202
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
203
|
-
if (error instanceof Error) span.recordException(error);
|
|
204
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
205
|
-
},
|
|
206
|
-
setAttribute: (k, v) => span.setAttribute(k, v),
|
|
207
|
-
spanContext: () => {
|
|
208
|
-
const ctx = span.spanContext();
|
|
209
|
-
return {
|
|
210
|
-
traceId: ctx.traceId,
|
|
211
|
-
spanId: ctx.spanId,
|
|
212
|
-
traceFlags: ctx.traceFlags,
|
|
213
|
-
};
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
},
|
|
217
|
-
});
|
|
253
|
+
// Default tracer bridge — delegates to `@opentelemetry/api` global. When
|
|
254
|
+
// `bootObservability` discovers `DECO_OTEL_TRACES_ENDPOINT`, it composes
|
|
255
|
+
// this bridge with the direct-POST OTLP tracer so framework spans flow to
|
|
256
|
+
// BOTH the CF dashboard AND ClickHouse (the bridge stays a no-op when CF
|
|
257
|
+
// tracing isn't configured, which is the common case today). See
|
|
258
|
+
// `configureTracerStack` below.
|
|
259
|
+
configureTracer(buildOtelApiTracer());
|
|
218
260
|
|
|
219
261
|
return {
|
|
220
262
|
async fetch(request, env, ctx) {
|
|
@@ -227,11 +269,11 @@ export function instrumentWorker(
|
|
|
227
269
|
try {
|
|
228
270
|
return await handler.fetch(request, env, ctx);
|
|
229
271
|
} finally {
|
|
230
|
-
// Drain the OTLP metrics + error-log buffers via
|
|
231
|
-
// so
|
|
232
|
-
//
|
|
233
|
-
// the network only fires when the cooldown elapses or
|
|
234
|
-
// buffer fills.
|
|
272
|
+
// Drain the OTLP metrics + error-log + traces buffers via
|
|
273
|
+
// ctx.waitUntil so no POST blocks the response. Each exporter
|
|
274
|
+
// throttles itself per isolate — calling on every request is
|
|
275
|
+
// cheap; the network only fires when the cooldown elapses or
|
|
276
|
+
// the buffer fills.
|
|
235
277
|
if (otlpMeter) {
|
|
236
278
|
try {
|
|
237
279
|
ctx.waitUntil(otlpMeter.flush());
|
|
@@ -246,11 +288,122 @@ export function instrumentWorker(
|
|
|
246
288
|
/* swallow */
|
|
247
289
|
}
|
|
248
290
|
}
|
|
291
|
+
if (otlpTracer) {
|
|
292
|
+
try {
|
|
293
|
+
ctx.waitUntil(otlpTracer.flush());
|
|
294
|
+
} catch {
|
|
295
|
+
/* swallow */
|
|
296
|
+
}
|
|
297
|
+
}
|
|
249
298
|
}
|
|
250
299
|
},
|
|
251
300
|
};
|
|
252
301
|
}
|
|
253
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Build the legacy `@opentelemetry/api` global-tracer bridge. Stays a
|
|
305
|
+
* no-op when no global TracerProvider is registered — same outcome as
|
|
306
|
+
* the historical configuration.
|
|
307
|
+
*/
|
|
308
|
+
function buildOtelApiTracer(): import("../middleware/observability").TracerAdapter {
|
|
309
|
+
return {
|
|
310
|
+
startSpan: (name, attrs) => {
|
|
311
|
+
const merged = { ...spanAttributeFloor, ...(attrs ?? {}) };
|
|
312
|
+
const span = trace.getTracer("@decocms/start").startSpan(name, { attributes: merged });
|
|
313
|
+
return {
|
|
314
|
+
end: () => span.end(),
|
|
315
|
+
setError: (error) => {
|
|
316
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
317
|
+
if (error instanceof Error) span.recordException(error);
|
|
318
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
319
|
+
},
|
|
320
|
+
setAttribute: (k, v) => span.setAttribute(k, v),
|
|
321
|
+
spanContext: () => {
|
|
322
|
+
const ctx = span.spanContext();
|
|
323
|
+
return {
|
|
324
|
+
traceId: ctx.traceId,
|
|
325
|
+
spanId: ctx.spanId,
|
|
326
|
+
traceFlags: ctx.traceFlags,
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Wire the framework tracer. When the OTLP traces endpoint is configured,
|
|
336
|
+
* compose the direct-POST tracer ALONGSIDE the `@opentelemetry/api` bridge
|
|
337
|
+
* via a fanout adapter so:
|
|
338
|
+
* 1. `withTracing` calls feed BOTH adapters.
|
|
339
|
+
* 2. A child span's `spanContext()` reports the direct-POST span's IDs
|
|
340
|
+
* (the bridge is best-effort — if CF tracing isn't installed those
|
|
341
|
+
* IDs are zeros anyway).
|
|
342
|
+
*
|
|
343
|
+
* When no traces endpoint is configured, fall back to the bridge alone —
|
|
344
|
+
* preserves the legacy behavior for sites that haven't bumped wrangler.
|
|
345
|
+
*/
|
|
346
|
+
function configureTracerStack(otlpAdapter: OtlpHttpTracer | null): void {
|
|
347
|
+
const bridge = buildOtelApiTracer();
|
|
348
|
+
if (!otlpAdapter) {
|
|
349
|
+
configureTracer(bridge);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Compose. The OTLP adapter is the "primary" (its IDs win for
|
|
353
|
+
// `spanContext()` because callers downstream use them for trace
|
|
354
|
+
// propagation). The bridge is best-effort — fed the same name/attrs
|
|
355
|
+
// so CF Workers Observability still sees the spans if that channel
|
|
356
|
+
// is enabled in wrangler.jsonc.
|
|
357
|
+
configureTracer({
|
|
358
|
+
startSpan(name, attrs) {
|
|
359
|
+
const merged = { ...spanAttributeFloor, ...(attrs ?? {}) };
|
|
360
|
+
const primary = otlpAdapter.startSpan(name, merged);
|
|
361
|
+
const secondary = bridge.startSpan(name, merged);
|
|
362
|
+
return {
|
|
363
|
+
end(): void {
|
|
364
|
+
try {
|
|
365
|
+
primary.end();
|
|
366
|
+
} finally {
|
|
367
|
+
try {
|
|
368
|
+
secondary.end();
|
|
369
|
+
} catch {
|
|
370
|
+
/* swallow */
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
setError(error: unknown): void {
|
|
375
|
+
try {
|
|
376
|
+
primary.setError?.(error);
|
|
377
|
+
} finally {
|
|
378
|
+
try {
|
|
379
|
+
secondary.setError?.(error);
|
|
380
|
+
} catch {
|
|
381
|
+
/* swallow */
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
setAttribute(key: string, value: string | number | boolean): void {
|
|
386
|
+
primary.setAttribute?.(key, value);
|
|
387
|
+
try {
|
|
388
|
+
secondary.setAttribute?.(key, value);
|
|
389
|
+
} catch {
|
|
390
|
+
/* swallow */
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
spanContext() {
|
|
394
|
+
// The OTLP adapter owns the canonical IDs — those are the IDs
|
|
395
|
+
// we propagate downstream via `traceparent` headers.
|
|
396
|
+
return primary.spanContext?.() ?? secondary.spanContext?.() ?? {
|
|
397
|
+
traceId: "",
|
|
398
|
+
spanId: "",
|
|
399
|
+
traceFlags: 0,
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
254
407
|
// ---------------------------------------------------------------------------
|
|
255
408
|
// Boot — wires the loggers/meters once (per worker isolate)
|
|
256
409
|
// ---------------------------------------------------------------------------
|
|
@@ -391,6 +544,41 @@ function bootObservability(opts: OtelOptions, env: Record<string, unknown>): voi
|
|
|
391
544
|
// composite becomes a 0-element no-op via createCompositeMeter's filter.
|
|
392
545
|
configureMeter(composedMeter);
|
|
393
546
|
|
|
547
|
+
// Traces — direct-POST exporter for framework `deco.*` spans. Without
|
|
548
|
+
// this, `withTracing` delegates to the no-op `@opentelemetry/api`
|
|
549
|
+
// global tracer and every framework span silently disappears (the
|
|
550
|
+
// Phase 3 gap documented in `MIGRATION_TOOLING_PLAN.md`). Same
|
|
551
|
+
// transport pattern as metrics/error-logs — buffered, sampled by
|
|
552
|
+
// trace-id hash, flushed via `ctx.waitUntil`.
|
|
553
|
+
const otlpTracesEnvVar = opts.otlpTracesEndpointEnvVar ?? "DECO_OTEL_TRACES_ENDPOINT";
|
|
554
|
+
const otlpTracesEndpoint = (env[otlpTracesEnvVar] as string | undefined) ?? "";
|
|
555
|
+
const otlpTracesEnabled =
|
|
556
|
+
opts.otlpTracesEnabled !== false && otlpTracesEndpoint.length > 0;
|
|
557
|
+
if (otlpTracesEnabled) {
|
|
558
|
+
otlpTracer = createOtlpHttpTracerAdapter({
|
|
559
|
+
endpoint: otlpTracesEndpoint,
|
|
560
|
+
resourceAttributes: floor,
|
|
561
|
+
scopeVersion: decoRuntimeVersion,
|
|
562
|
+
headSamplingRate: opts.otlpTracesSamplingRate ?? 0.01,
|
|
563
|
+
fetchImpl: opts.otlpTracesFetchImpl,
|
|
564
|
+
getActiveSpanForParent: () => getActiveSpan(),
|
|
565
|
+
getRequestTraceContext,
|
|
566
|
+
onError: (kind, err) => {
|
|
567
|
+
defaultLoggerAdapter.log("warn", "otlp traces exporter", {
|
|
568
|
+
kind,
|
|
569
|
+
error: err instanceof Error ? err.message : String(err),
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
} else {
|
|
574
|
+
otlpTracer = null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Wire the tracer stack — composes the OTLP direct-POST adapter (when
|
|
578
|
+
// configured) with the @opentelemetry/api bridge. See
|
|
579
|
+
// `configureTracerStack` for the fanout semantics.
|
|
580
|
+
configureTracerStack(otlpTracer);
|
|
581
|
+
|
|
394
582
|
booted = true;
|
|
395
583
|
|
|
396
584
|
// Single boot-time breadcrumb so operators can confirm the wiring at a
|
|
@@ -400,6 +588,7 @@ function bootObservability(opts: OtelOptions, env: Record<string, unknown>): voi
|
|
|
400
588
|
analyticsEngine: aeEnabled,
|
|
401
589
|
otlpMetrics: otlpEnabled,
|
|
402
590
|
otlpErrorLogs: otlpErrorLogsEnabled,
|
|
591
|
+
otlpTraces: otlpTracesEnabled,
|
|
403
592
|
runtimeVersion: decoRuntimeVersion,
|
|
404
593
|
deploymentEnvironment,
|
|
405
594
|
...(serviceVersion ? { serviceVersion } : {}),
|
|
@@ -415,6 +604,7 @@ export function _resetBootStateForTests(): void {
|
|
|
415
604
|
spanAttributeFloor = {};
|
|
416
605
|
otlpMeter = null;
|
|
417
606
|
otlpErrorLog = null;
|
|
607
|
+
otlpTracer = null;
|
|
418
608
|
setLoggerAttributeFloor({});
|
|
419
609
|
}
|
|
420
610
|
|