@decocms/start 4.5.0 → 5.0.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/src/sdk/otel.ts CHANGED
@@ -4,21 +4,30 @@
4
4
  * `instrumentWorker(handler, options)` wraps a Worker handler with:
5
5
  * - structured JSON logger (stdout → Cloudflare Workers Logs) — always
6
6
  * - Workers Analytics Engine metrics — when `env.DECO_METRICS` binding exists
7
- * - OTLP/HTTP metrics exporter (HyperDX) — when `OTEL_EXPORTER_OTLP_ENDPOINT`
8
- * is set (CF doesn't support OTLP metrics export yet, so this stays app-side)
9
- * - Per-request `ctx.waitUntil(forceFlush)` for any registered OTel batch
10
- * processors so log/metric batches don't die with the isolate
11
7
  * - Bridges framework-internal `withTracing()` calls onto the global
12
8
  * `@opentelemetry/api` tracer, stamping `deco.*` attributes on every span
13
9
  * so they survive Cloudflare's platform-managed trace export
14
10
  *
15
- * **Logs and traces export to HyperDX is now handled by Cloudflare** via the
16
- * `observability.{logs,traces}.destinations` block in `wrangler.jsonc`. CF
17
- * captures `console.*` output and `@opentelemetry/api` global tracer spans
18
- * out-of-band and ships them OTLP-encoded to whatever destination is
19
- * configured. This eliminates the in-Worker exporter SDK, the per-request
20
- * subrequest cost of pushing OTLP, and the entire class of bug PR #153 fixed
21
- * (batch processors that never flush before isolate recycling).
11
+ * **All export goes through Cloudflare.** Logs reach the dashboard via
12
+ * `console.*` capture; traces reach the dashboard via CF auto-instrumentation
13
+ * plus the global-tracer spans this module forwards. There is no in-Worker
14
+ * OTLP exporter and no third-party destination the CF dashboard is the
15
+ * destination.
16
+ *
17
+ * Required `wrangler.jsonc` block (run `scripts/migrate-to-cf-observability.ts`
18
+ * to inject this automatically):
19
+ * ```jsonc
20
+ * "observability": {
21
+ * "enabled": true,
22
+ * "logs": { "enabled": true, "invocation_logs": true,
23
+ * "head_sampling_rate": 1, "persist": true },
24
+ * "traces": { "enabled": true,
25
+ * "head_sampling_rate": 0.1, "persist": true }
26
+ * },
27
+ * "version_metadata": { "binding": "CF_VERSION_METADATA" },
28
+ * "analytics_engine_datasets": [{ "binding": "DECO_METRICS",
29
+ * "dataset": "deco_metrics_my_site" }]
30
+ * ```
22
31
  *
23
32
  * @example
24
33
  * ```ts
@@ -30,54 +39,18 @@
30
39
  * export default instrumentWorker(handler, { serviceName: "my-store" });
31
40
  * ```
32
41
  *
33
- * Companion `wrangler.jsonc` block (run `scripts/migrate-to-cf-observability.ts`
34
- * to inject this automatically):
35
- * ```jsonc
36
- * "observability": {
37
- * "logs": { "enabled": true, "destinations": ["hyperdx-logs"],
38
- * "head_sampling_rate": 1.0, "persist": false },
39
- * "traces": { "enabled": true, "destinations": ["hyperdx-traces"],
40
- * "head_sampling_rate": 0.1, "persist": false }
41
- * },
42
- * "version_metadata": { "binding": "CF_VERSION_METADATA" },
43
- * "analytics_engine_datasets": [{ "binding": "DECO_METRICS",
44
- * "dataset": "deco_metrics_my_site" }]
45
- * ```
46
- *
47
- * **Back-compat seam.** Sites that need to keep app-side OTLP log export
48
- * (custom destination not covered by CF, custom batching, etc.) can opt back
49
- * in with `enableAppSideOtlpLogs: true` and the existing `OTEL_EXPORTER_OTLP_*`
50
- * secrets. Slated for removal in 5.0.0.
42
+ * **Future ClickHouse path.** When a co-deployed OTel collector lands, an
43
+ * exporter that pushes spans + logs + metrics to that collector will live in
44
+ * `./otelAdapters/clickhouseCollector.ts` (today: documented stub that throws).
45
+ * The `withTracing` / `recordRequestMetric` / `logger` instrumentation surface
46
+ * does not change — only the transport layer wires up.
51
47
  */
52
48
 
53
49
  import { trace } from "@opentelemetry/api";
54
- import { type Resource, resourceFromAttributes } from "@opentelemetry/resources";
55
50
 
56
51
  import { configureMeter, configureTracer } from "../middleware/observability";
57
- import { createCompositeLogger, createCompositeMeter } from "./composite";
58
- import {
59
- configureLogger,
60
- defaultLoggerAdapter,
61
- type LogLevel,
62
- logger,
63
- setLoggerAttributeFloor,
64
- } from "./logger";
65
- import {
66
- createAnalyticsEngineMeterAdapter,
67
- createOtelLoggerAdapter,
68
- createOtelMeterAdapter,
69
- flushOtelProviders,
70
- setRuntimeEnv,
71
- } from "./otelAdapters";
72
- import { RequestContext } from "./requestContext";
73
-
74
- const VALID_LOG_LEVELS: readonly LogLevel[] = ["debug", "info", "warn", "error"];
75
-
76
- function parseLogLevel(value: unknown): LogLevel | undefined {
77
- if (typeof value !== "string") return undefined;
78
- const lc = value.toLowerCase();
79
- return VALID_LOG_LEVELS.find((l) => l === lc);
80
- }
52
+ import { configureLogger, defaultLoggerAdapter, setLoggerAttributeFloor } from "./logger";
53
+ import { createAnalyticsEngineMeterAdapter } from "./otelAdapters";
81
54
 
82
55
  // ---------------------------------------------------------------------------
83
56
  // Types
@@ -86,49 +59,17 @@ function parseLogLevel(value: unknown): LogLevel | undefined {
86
59
  export interface OtelOptions {
87
60
  /** Logical service name. Falls back to `env.DECO_SITE_NAME`, then "deco-site". */
88
61
  serviceName?: string;
89
- /** Override OTLP endpoint. Defaults to `env.OTEL_EXPORTER_OTLP_ENDPOINT`. */
90
- endpoint?: string;
91
- /** Override OTLP auth headers. Defaults to parsed `env.OTEL_EXPORTER_OTLP_HEADERS`. */
92
- headers?: Record<string, string>;
93
62
  /** Env var name holding the AE binding. Defaults to `"DECO_METRICS"`. */
94
63
  analyticsEngineBindingName?: string;
95
64
  /** Set to `false` to disable AE even when the binding is present. */
96
65
  analyticsEngineEnabled?: boolean;
97
- /** Push interval for OTLP metrics, in ms. Defaults to env.OTEL_EXPORT_INTERVAL or 60_000. */
98
- metricsExportIntervalMillis?: number;
99
- /**
100
- * Minimum severity to forward to the **app-side OTLP** logger (only
101
- * relevant when `enableAppSideOtlpLogs: true`). The default `console.*`
102
- * adapter is unaffected and continues to capture every level for
103
- * Cloudflare Workers Logs / CF-side OTLP export.
104
- *
105
- * Defaults to `"warn"`. Falls back to env `OTEL_LOG_MIN_SEVERITY` when
106
- * unset. Set to `"debug"` to forward everything.
107
- */
108
- otlpMinSeverity?: LogLevel;
109
- /**
110
- * Opt-in: also wire an in-Worker OTLP logger that pushes log records to
111
- * `OTEL_EXPORTER_OTLP_ENDPOINT`. Defaults to `false` — sites should
112
- * prefer the platform-managed CF-side path
113
- * (`observability.logs.destinations` in `wrangler.jsonc`), which is
114
- * cheaper, has no flush-bug class, and consumes zero subrequest budget.
115
- *
116
- * Use this only when CF's OTLP logs export doesn't meet a specific need
117
- * (e.g. shipping to a destination CF doesn't support, custom batching,
118
- * staging-only debugging). Requires `OTEL_EXPORTER_OTLP_ENDPOINT` and
119
- * `OTEL_EXPORTER_OTLP_HEADERS` to be set.
120
- *
121
- * Slated for removal in 5.0.0.
122
- */
123
- enableAppSideOtlpLogs?: boolean;
124
66
  /**
125
67
  * Version of `@decocms/start` to advertise as `deco.runtime.version`
126
- * on every span (CF doesn't preserve it as a resource attribute since
127
- * we no longer ship our own resource — we stamp it per-span instead).
128
- * Falls back to a build-time constant; override only for tests.
68
+ * on every span and every log line. Falls back to a build-time constant;
69
+ * override only for tests.
129
70
  */
130
71
  decoRuntimeVersion?: string;
131
- /** Optional `@decocms/apps` version, stamped as `deco.apps.version` on every span. */
72
+ /** Optional `@decocms/apps` version, stamped as `deco.apps.version`. */
132
73
  decoAppsVersion?: string;
133
74
  }
134
75
 
@@ -151,24 +92,12 @@ interface WorkerHandler {
151
92
 
152
93
  let booted = false;
153
94
 
154
- interface BootState {
155
- serviceName: string;
156
- otlpEndpoint: string | null;
157
- otlpHeaders: Record<string, string>;
158
- resource: Resource;
159
- }
160
-
161
- let bootState: BootState | null = null;
162
-
163
95
  /**
164
96
  * Per-span attribute floor — stamped on every span we create via
165
- * `configureTracer().startSpan(...)`. These match what the legacy resource
166
- * attributes used to carry (when `@microlabs/otel-cf-workers` shipped its
167
- * own OTel `Resource`); we now stamp them on each span so HyperDX panels
168
- * filtering on `deco.runtime.version`, `deco.apps.version`, or
169
- * `deployment.environment` keep working with CF-managed export, which only
170
- * preserves CF's own resource attribute set (`service.name`, `faas.name`,
171
- * `cloudflare.script_version.id`, etc.).
97
+ * `configureTracer().startSpan(...)`. CF's trace export emits its own
98
+ * resource attribute set (service.name=Worker name, faas.name,
99
+ * cloudflare.script_version.id, etc.) so framework-level dimensions like
100
+ * `deco.runtime.version` only survive when stamped per-span.
172
101
  *
173
102
  * Populated by `bootObservability` before any span is created. Stays an
174
103
  * empty object until then so early span creation is a no-op stamp.
@@ -182,19 +111,15 @@ let spanAttributeFloor: Record<string, string> = {};
182
111
  /**
183
112
  * Wraps a Cloudflare Worker handler with the @decocms/start observability
184
113
  * stack:
185
- * - structured JSON logger (always)
114
+ * - structured JSON logger to console.* (CF captures via observability.logs)
186
115
  * - AE meter (when `DECO_METRICS` binding present)
187
- * - optional app-side OTLP meter (when `OTEL_EXPORTER_OTLP_ENDPOINT` set)
188
- * - optional app-side OTLP logger (when `enableAppSideOtlpLogs: true`)
189
- * - per-request `ctx.waitUntil(forceFlush)` for any registered batch processors
190
116
  * - bridge from framework-internal `withTracing()` to `@opentelemetry/api`
191
- * global tracer, with `deco.*` attributes stamped on every span
117
+ * global tracer (CF observability.traces ingests via auto-instrumentation
118
+ * + the global-tracer hook)
192
119
  *
193
- * Logs and traces export to HyperDX (or any OTLP destination) is handled
194
- * by Cloudflare via `observability.{logs,traces}.destinations` in
195
- * `wrangler.jsonc`. This wrapper does NOT call `@microlabs/otel-cf-workers`
196
- * `instrument()` — CF's platform-managed export captures `console.*` output
197
- * and global-tracer spans out-of-band.
120
+ * No external destinations, no OTLP transport. Forwarding to a future
121
+ * OTel collector for ClickHouse will live behind a separate adapter
122
+ * (see `./otelAdapters/clickhouseCollector.ts`).
198
123
  */
199
124
  export function instrumentWorker(
200
125
  handler: WorkerHandler,
@@ -206,9 +131,9 @@ export function instrumentWorker(
206
131
  //
207
132
  // CF Workers Tracing (when `observability.traces.enabled = true` in
208
133
  // wrangler) installs its own TracerProvider into the @opentelemetry/api
209
- // global, so these spans flow through to whatever OTLP destination is
210
- // configured. Without CF tracing the global tracer is a no-op proxy and
211
- // the spans simply drop — same outcome as before, no error.
134
+ // global, so these spans flow through to the CF dashboard. Without CF
135
+ // tracing the global tracer is a no-op proxy and the spans simply drop
136
+ // — same outcome as before, no error.
212
137
  configureTracer({
213
138
  startSpan: (name, attrs) => {
214
139
  const merged = { ...spanAttributeFloor, ...(attrs ?? {}) };
@@ -228,34 +153,10 @@ export function instrumentWorker(
228
153
  const opts =
229
154
  typeof options === "function" ? options(env as Record<string, unknown>) : options;
230
155
  bootObservability(opts, env as Record<string, unknown>);
231
-
232
- // Stash env so request-scoped adapters (AE) can resolve their
233
- // bindings. Done inside RequestContext.run wrapping in workerEntry.ts
234
- // too, but we re-stash here in case `instrumentWorker` is wrapped
235
- // over a handler that doesn't go through `createDecoWorkerEntry`.
236
- const wrap = async () => {
237
- setRuntimeEnv(env);
238
- return handler.fetch(request, env, ctx);
239
- };
240
-
241
- try {
242
- if (RequestContext.current) {
243
- return await wrap();
244
- }
245
- return await RequestContext.run(request, wrap);
246
- } finally {
247
- // Drain OTLP meter (and OTLP logger, if `enableAppSideOtlpLogs`)
248
- // batches inside the post-response window `waitUntil` guarantees.
249
- // Without this hook, `PeriodicExportingMetricReader` (60s flush)
250
- // batches usually die with the isolate before the timer fires.
251
- // No-op when no batch processors are registered.
252
- try {
253
- ctx.waitUntil(flushOtelProviders());
254
- } catch {
255
- // `waitUntil` only throws if `ctx` isn't a real ExecutionContext
256
- // (e.g. test stubs). Telemetry flush failures are not request-fatal.
257
- }
258
- }
156
+ // RequestContext.run + setRuntimeEnv(env) is handled inside
157
+ // workerEntry.ts on the inner handler instrumentWorker does
158
+ // NOT re-wrap so we don't double-enter AsyncLocalStorage.
159
+ return handler.fetch(request, env, ctx);
259
160
  },
260
161
  };
261
162
  }
@@ -265,119 +166,47 @@ export function instrumentWorker(
265
166
  // ---------------------------------------------------------------------------
266
167
 
267
168
  function bootObservability(opts: OtelOptions, env: Record<string, unknown>): void {
268
- if (booted && bootState) return;
169
+ if (booted) return;
269
170
 
270
171
  const serviceName = opts.serviceName ?? (env.DECO_SITE_NAME as string | undefined) ?? "deco-site";
271
-
272
- const otlpEndpoint =
273
- opts.endpoint ?? (env.OTEL_EXPORTER_OTLP_ENDPOINT as string | undefined) ?? null;
274
- const otlpHeaders =
275
- opts.headers ?? parseHeaders(env.OTEL_EXPORTER_OTLP_HEADERS as string | undefined);
276
-
277
172
  const decoRuntimeVersion = opts.decoRuntimeVersion ?? DECO_RUNTIME_VERSION;
278
173
  const deploymentEnvironment = (env.DECO_ENV_NAME as string | undefined) ?? "production";
279
174
 
280
- const resource = buildResource({
281
- serviceName,
282
- serviceVersion: (env.CF_VERSION_METADATA as { id?: string } | undefined)?.id,
283
- serviceInstanceId: cryptoRandomId(),
284
- deploymentEnvironment,
285
- decoRuntimeVersion,
286
- decoAppsVersion: opts.decoAppsVersion,
287
- });
288
-
289
- // Stamp deco.* attributes on every span we create. CF-managed trace
290
- // export emits its own resource attribute set (service.name=Worker name,
291
- // faas.name, cloudflare.script_version.id, faas.version, etc.) so the
292
- // legacy resource attrs from `buildResource` don't survive on the
293
- // CF-side path. Stamping them per-span preserves the dimensions
294
- // existing HyperDX dashboards filter on.
295
- spanAttributeFloor = {
175
+ const floor: Record<string, string> = {
296
176
  "deco.runtime.version": decoRuntimeVersion,
297
177
  "deployment.environment": deploymentEnvironment,
298
- ...(opts.decoAppsVersion ? { "deco.apps.version": opts.decoAppsVersion } : {}),
299
178
  };
179
+ if (opts.decoAppsVersion) floor["deco.apps.version"] = opts.decoAppsVersion;
300
180
 
301
- // Same set, stamped on every log record. CF Workers Logs ships the JSON
302
- // body verbatim (resource attrs from `buildResource` are NOT applied to
303
- // logs in default mode), so HyperDX panels grouping by these dimensions
304
- // would otherwise return empty. Caller-supplied `attrs` still win on key
305
- // collision.
306
- setLoggerAttributeFloor({
307
- "deco.runtime.version": decoRuntimeVersion,
308
- "deployment.environment": deploymentEnvironment,
309
- ...(opts.decoAppsVersion ? { "deco.apps.version": opts.decoAppsVersion } : {}),
310
- });
181
+ // Stamp on every span we create. CF-managed trace export emits its own
182
+ // resource attribute set, so legacy resource attrs don't survive.
183
+ // Stamping per-span preserves the dimensions dashboards / saved searches
184
+ // filter on.
185
+ spanAttributeFloor = floor;
311
186
 
312
- // ---- Logger ----------------------------------------------------------
313
- // Default mode: console JSON only. Cloudflare Workers Logs captures the
314
- // output and ships it via `observability.logs.destinations` to whichever
315
- // OTLP destination is configured in `wrangler.jsonc`. This is the
316
- // recommended path: zero in-Worker exporter, no flush bug, no subrequest
317
- // cost per emit.
318
- //
319
- // Opt-in mode (`enableAppSideOtlpLogs: true`): also wire the OTLP logger
320
- // adapter for sites with destinations CF doesn't support. Requires
321
- // `OTEL_EXPORTER_OTLP_ENDPOINT` to be set.
322
- const otlpMinSeverity =
323
- opts.otlpMinSeverity ?? parseLogLevel(env.OTEL_LOG_MIN_SEVERITY) ?? "warn";
324
-
325
- const wantAppSideLogs = opts.enableAppSideOtlpLogs === true;
326
- const otelLogger =
327
- wantAppSideLogs && otlpEndpoint != null
328
- ? createOtelLoggerAdapter({
329
- endpoint: otlpEndpoint,
330
- headers: otlpHeaders,
331
- resource,
332
- name: serviceName,
333
- minSeverity: otlpMinSeverity,
334
- })
335
- : null;
187
+ // Stamp on every log record. CF Workers Logs ships the JSON body
188
+ // verbatim without this floor, panels grouping logs by these
189
+ // dimensions return empty. Caller-supplied `attrs` still win on
190
+ // key collision (see logger.ts).
191
+ setLoggerAttributeFloor(floor);
336
192
 
337
- configureLogger(createCompositeLogger([defaultLoggerAdapter, otelLogger]));
338
-
339
- // ---- Meter -----------------------------------------------------------
340
- // OTLP meter stays default-on when an endpoint is configured: CF doesn't
341
- // support OTLP metrics export yet, so this is the only path to
342
- // HyperDX-compatible metrics. Drop this branch when CF ships metrics.
343
- const otelMeter =
344
- otlpEndpoint != null
345
- ? createOtelMeterAdapter({
346
- endpoint: otlpEndpoint,
347
- headers: otlpHeaders,
348
- resource,
349
- exportIntervalMillis:
350
- opts.metricsExportIntervalMillis ?? numericEnv(env.OTEL_EXPORT_INTERVAL, 60_000),
351
- name: serviceName,
352
- })
353
- : null;
193
+ // Logger: structured JSON to console.*, captured by CF observability.logs.
194
+ configureLogger(defaultLoggerAdapter);
354
195
 
196
+ // Meter: AE only. OTLP metrics path was removed in 5.0.0; will return
197
+ // via the ClickHouse collector adapter when that lands.
355
198
  const aeBindingName = opts.analyticsEngineBindingName ?? "DECO_METRICS";
356
199
  const aeEnabled = opts.analyticsEngineEnabled !== false && Boolean(env[aeBindingName]);
357
- const aeMeter = aeEnabled
358
- ? createAnalyticsEngineMeterAdapter({ bindingName: aeBindingName })
359
- : null;
360
-
361
- configureMeter(createCompositeMeter([aeMeter, otelMeter]));
200
+ if (aeEnabled) {
201
+ configureMeter(createAnalyticsEngineMeterAdapter({ bindingName: aeBindingName }));
202
+ }
362
203
 
363
- bootState = {
364
- serviceName,
365
- otlpEndpoint,
366
- otlpHeaders,
367
- resource,
368
- };
369
204
  booted = true;
370
205
 
371
206
  // Single boot-time breadcrumb so operators can confirm the wiring at a
372
- // glance from CF Logs without enabling debug. Surfaces which export
373
- // mode is active (CF-native vs app-side) so misconfigured sites are
374
- // obvious from the first request.
375
- logger.info("observability booted", {
207
+ // glance from CF Logs without enabling debug.
208
+ defaultLoggerAdapter.log("info", "observability booted", {
376
209
  service: serviceName,
377
- mode: wantAppSideLogs ? "hybrid (app-side OTLP logs + CF traces)" : "cf-native",
378
- otlpMeter: Boolean(otlpEndpoint),
379
- otlpLogger: wantAppSideLogs && otlpEndpoint != null,
380
- otlpMinSeverity: wantAppSideLogs && otlpEndpoint != null ? otlpMinSeverity : null,
381
210
  analyticsEngine: aeEnabled,
382
211
  runtimeVersion: decoRuntimeVersion,
383
212
  deploymentEnvironment,
@@ -390,37 +219,10 @@ function bootObservability(opts: OtelOptions, env: Record<string, unknown>): voi
390
219
  */
391
220
  export function _resetBootStateForTests(): void {
392
221
  booted = false;
393
- bootState = null;
394
222
  spanAttributeFloor = {};
395
223
  setLoggerAttributeFloor({});
396
224
  }
397
225
 
398
- // ---------------------------------------------------------------------------
399
- // Resource attributes
400
- // ---------------------------------------------------------------------------
401
-
402
- interface ResourceInput {
403
- serviceName: string;
404
- serviceVersion?: string;
405
- serviceInstanceId: string;
406
- deploymentEnvironment: string;
407
- decoRuntimeVersion: string;
408
- decoAppsVersion?: string;
409
- }
410
-
411
- function buildResource(input: ResourceInput): Resource {
412
- const attrs: Record<string, string> = {
413
- "service.name": input.serviceName,
414
- "service.version": input.serviceVersion ?? "unknown",
415
- "service.instance.id": input.serviceInstanceId,
416
- "cloud.provider": "cloudflare",
417
- "deployment.environment": input.deploymentEnvironment,
418
- "deco.runtime.version": input.decoRuntimeVersion,
419
- };
420
- if (input.decoAppsVersion) attrs["deco.apps.version"] = input.decoAppsVersion;
421
- return resourceFromAttributes(attrs);
422
- }
423
-
424
226
  // ---------------------------------------------------------------------------
425
227
  // Helpers
426
228
  // ---------------------------------------------------------------------------
@@ -434,31 +236,4 @@ function buildResource(input: ResourceInput): Resource {
434
236
  * Drift is acceptable — this attribute is for operator triage, not for
435
237
  * billing / SLOs.
436
238
  */
437
- const DECO_RUNTIME_VERSION = "4.4.0";
438
-
439
- function parseHeaders(str?: string): Record<string, string> {
440
- if (!str) return {};
441
- return Object.fromEntries(
442
- str
443
- .split(",")
444
- .map((kv) => {
445
- const [k, ...v] = kv.split("=");
446
- return [k.trim(), v.join("=").trim()] as const;
447
- })
448
- .filter(([k]) => k.length > 0),
449
- );
450
- }
451
-
452
- function numericEnv(value: unknown, fallback: number): number {
453
- const n = Number(value);
454
- return Number.isFinite(n) && n > 0 ? n : fallback;
455
- }
456
-
457
- function cryptoRandomId(): string {
458
- // crypto.randomUUID is universally available in CF Workers + Node 19+.
459
- try {
460
- return crypto.randomUUID();
461
- } catch {
462
- return `inst-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
463
- }
464
- }
239
+ const DECO_RUNTIME_VERSION = "5.0.0";
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Placeholder for a future OTel-collector based exporter that ships
3
+ * logs / metrics / traces from this Worker to a co-deployed OTel Collector
4
+ * gateway, which in turn batches into ClickHouse.
5
+ *
6
+ * **NOT IMPLEMENTED in 5.0.0.** The framework today defaults to CF-native
7
+ * observability: logs and traces appear in the Cloudflare dashboard
8
+ * (`observability.{logs,traces}` in `wrangler.jsonc`), and metrics go to
9
+ * Workers Analytics Engine. When the platform-side ClickHouse + collector
10
+ * deployment lands, this file gets the real OTLP/HTTP exporter wiring back
11
+ * — mirror the implementation that lived in `src/sdk/otelAdapters.ts` and
12
+ * `src/sdk/sampler.ts` at the pre-5.0.0 git tag.
13
+ *
14
+ * The expected shape (`endpoint`, optional `headers`, optional `resource`,
15
+ * `exportIntervalMillis`) is recorded here so site-side bootstrapping code
16
+ * can compile against the future surface today and we don't churn the
17
+ * `OtelOptions` type when the implementation lands.
18
+ *
19
+ * Reference: https://clickhouse.com/docs/observability/integrating-opentelemetry
20
+ *
21
+ * Expected wiring once implemented:
22
+ * - Worker → OTLP/HTTP → OTel Collector (gateway, per region)
23
+ * - Collector pipelines:
24
+ * receivers[otlp] → processors[batch, memory_limiter] → exporters[clickhouse]
25
+ * - The Worker uses the collector URL as `endpoint`, NOT a direct
26
+ * ClickHouse URL. The collector owns the credentials, retry policy,
27
+ * and schema mapping.
28
+ */
29
+
30
+ export interface ClickhouseCollectorOptions {
31
+ /**
32
+ * OTel Collector OTLP/HTTP endpoint, e.g. `https://otel-collector.internal`.
33
+ * The Worker never talks to ClickHouse directly — it talks to the collector.
34
+ */
35
+ endpoint: string;
36
+ /** Optional bearer token / mTLS identity headers — collector-defined. */
37
+ headers?: Record<string, string>;
38
+ /**
39
+ * Resource attributes to stamp on every emitted record. Keep narrow
40
+ * (`service.name`, `deco.runtime.version`, `deployment.environment`) —
41
+ * higher-cardinality attributes belong on individual spans / log records.
42
+ */
43
+ resource?: Record<string, string>;
44
+ /** Periodic metric reader push interval in ms. Defaults to 60_000 once wired. */
45
+ exportIntervalMillis?: number;
46
+ }
47
+
48
+ /**
49
+ * Returns nothing today — throws to make accidental usage loud and obvious.
50
+ *
51
+ * The intended return shape is `{ logger: LoggerAdapter, meter: MeterAdapter,
52
+ * tracer: TracerAdapter, flush: () => Promise<void> }` so the wiring inside
53
+ * `instrumentWorker` can compose this with the AE meter (dual-emit) and the
54
+ * default console logger. None of that ships in 5.0.0.
55
+ */
56
+ export function createClickhouseCollectorAdapter(_options: ClickhouseCollectorOptions): never {
57
+ throw new Error(
58
+ "createClickhouseCollectorAdapter is not implemented in @decocms/start@5.x. " +
59
+ "ClickHouse + OTel-collector integration is on the roadmap. " +
60
+ "Until then, use Cloudflare-native observability " +
61
+ "(observability.{logs,traces} in wrangler.jsonc) plus the Workers " +
62
+ "Analytics Engine meter wired by instrumentWorker(). Track progress " +
63
+ "at https://github.com/decocms/deco-start/issues.",
64
+ );
65
+ }