@decocms/start 4.2.1 → 4.4.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/bun.lock +36 -110
- package/package.json +4 -3
- package/scripts/migrate/phase-report.ts +8 -0
- package/scripts/migrate/templates/server-entry.ts +39 -3
- package/scripts/migrate-to-cf-observability.test.ts +169 -0
- package/scripts/migrate-to-cf-observability.ts +611 -0
- package/src/sdk/logger.test.ts +79 -0
- package/src/sdk/logger.ts +40 -2
- package/src/sdk/observability.ts +2 -0
- package/src/sdk/otel.test.ts +128 -15
- package/src/sdk/otel.ts +210 -91
- package/src/sdk/otelAdapters.test.ts +138 -2
- package/src/sdk/otelAdapters.ts +83 -1
- package/src/sdk/sampler.ts +17 -5
- package/src/sdk/workerEntry.ts +7 -4
package/src/sdk/otelAdapters.ts
CHANGED
|
@@ -39,6 +39,56 @@ import { MetricNames } from "../middleware/observability";
|
|
|
39
39
|
import type { LoggerAdapter, LogLevel } from "./logger";
|
|
40
40
|
import { RequestContext } from "./requestContext";
|
|
41
41
|
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Flush registry — per-request `ctx.waitUntil` hook target
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Cloudflare Workers isolates are recycled aggressively (often before the
|
|
48
|
+
* next 5s `BatchLogRecordProcessor` tick or 60s `PeriodicExportingMetricReader`
|
|
49
|
+
* tick fires). Without a per-request flush, in-memory log/metric batches die
|
|
50
|
+
* with the isolate and never reach OTLP — observable as "spans show up in
|
|
51
|
+
* HyperDX, logs and metrics don't".
|
|
52
|
+
*
|
|
53
|
+
* Each OTLP adapter registers a `forceFlush` handler here at construction
|
|
54
|
+
* time. `instrumentWorker` calls `flushOtelProviders()` from
|
|
55
|
+
* `ctx.waitUntil(...)` after every response so the batch is drained inside
|
|
56
|
+
* the post-response window the platform guarantees.
|
|
57
|
+
*
|
|
58
|
+
* The registry is module-scoped (one entry per provider, not per request)
|
|
59
|
+
* and survives worker reloads — the boot guard in `bootObservability`
|
|
60
|
+
* prevents duplicate registration in steady state. Tests must call
|
|
61
|
+
* `_resetFlushHandlersForTests()` between fixtures to avoid leakage.
|
|
62
|
+
*/
|
|
63
|
+
const flushHandlers: Array<() => Promise<unknown>> = [];
|
|
64
|
+
|
|
65
|
+
/** Register a `forceFlush()`-style handler. Idempotency is the caller's problem. */
|
|
66
|
+
export function registerOtelFlushHandler(fn: () => Promise<unknown>): void {
|
|
67
|
+
flushHandlers.push(fn);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Drain every registered OTLP provider in parallel. Resolves once they
|
|
72
|
+
* all settle — never rejects, because flush failures are telemetry
|
|
73
|
+
* incidents, not request incidents.
|
|
74
|
+
*
|
|
75
|
+
* Safe to call when no adapters are registered (returns immediately).
|
|
76
|
+
*/
|
|
77
|
+
export async function flushOtelProviders(): Promise<void> {
|
|
78
|
+
if (flushHandlers.length === 0) return;
|
|
79
|
+
await Promise.allSettled(flushHandlers.map((fn) => fn()));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Test-only: clear the flush registry between fixtures. Do not call from app code. */
|
|
83
|
+
export function _resetFlushHandlersForTests(): void {
|
|
84
|
+
flushHandlers.length = 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Test-only: introspect the registry size. Do not call from app code. */
|
|
88
|
+
export function _getFlushHandlerCountForTests(): number {
|
|
89
|
+
return flushHandlers.length;
|
|
90
|
+
}
|
|
91
|
+
|
|
42
92
|
// ---------------------------------------------------------------------------
|
|
43
93
|
// Env / binding access
|
|
44
94
|
// ---------------------------------------------------------------------------
|
|
@@ -79,13 +129,37 @@ export interface OtelLoggerAdapterOptions {
|
|
|
79
129
|
resource?: Resource;
|
|
80
130
|
/** OTel logger name. Defaults to "@decocms/start". */
|
|
81
131
|
name?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Minimum severity to forward to OTLP. Calls below this floor are dropped
|
|
134
|
+
* by this adapter (and therefore don't ship to HyperDX or any other OTLP
|
|
135
|
+
* sink), but the framework's default console adapter still sees them, so
|
|
136
|
+
* they remain visible in Cloudflare Workers Logs.
|
|
137
|
+
*
|
|
138
|
+
* Defaults to `"warn"`. The reasoning: in Cloudflare Workers every OTLP
|
|
139
|
+
* emit eventually becomes an outbound subrequest (after batching). Routine
|
|
140
|
+
* `info` chatter compounded over many isolates can exhaust either the
|
|
141
|
+
* subrequest budget (if a hot loop logs) or the HyperDX log-ingest quota.
|
|
142
|
+
* Warn+ keeps the trace ↔ log correlation that matters during incidents
|
|
143
|
+
* without ingesting noise that's already captured by Cloudflare Logs.
|
|
144
|
+
*
|
|
145
|
+
* Override per-site via `OtelOptions.otlpMinSeverity` or the
|
|
146
|
+
* `OTEL_LOG_MIN_SEVERITY` env var. Set to `"debug"` to forward everything.
|
|
147
|
+
*/
|
|
148
|
+
minSeverity?: LogLevel;
|
|
82
149
|
}
|
|
83
150
|
|
|
84
151
|
/**
|
|
85
|
-
* Streams `logger.*` calls to an OTLP/HTTP logs
|
|
152
|
+
* Streams `logger.*` calls (at or above `minSeverity`) to an OTLP/HTTP logs
|
|
153
|
+
* endpoint (e.g. HyperDX).
|
|
86
154
|
*
|
|
87
155
|
* Returns `null` when no endpoint is configured — `instrumentWorker()`
|
|
88
156
|
* uses that signal to skip registering this adapter.
|
|
157
|
+
*
|
|
158
|
+
* Side effect: registers the underlying `LoggerProvider`'s `forceFlush()`
|
|
159
|
+
* with `registerOtelFlushHandler` so per-request hooks in `instrumentWorker`
|
|
160
|
+
* can drain the batch before the Workers isolate is recycled. Without that
|
|
161
|
+
* registration, `BatchLogRecordProcessor`'s 5s timer rarely fires before
|
|
162
|
+
* GC and log records are silently dropped.
|
|
89
163
|
*/
|
|
90
164
|
export function createOtelLoggerAdapter(
|
|
91
165
|
options: OtelLoggerAdapterOptions | null,
|
|
@@ -101,6 +175,7 @@ export function createOtelLoggerAdapter(
|
|
|
101
175
|
resource: options.resource,
|
|
102
176
|
});
|
|
103
177
|
provider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter));
|
|
178
|
+
registerOtelFlushHandler(() => provider.forceFlush());
|
|
104
179
|
|
|
105
180
|
// Register globally so `@opentelemetry/api-logs` consumers (if any)
|
|
106
181
|
// also pick it up. Idempotent — safe across multiple worker reloads.
|
|
@@ -111,10 +186,12 @@ export function createOtelLoggerAdapter(
|
|
|
111
186
|
}
|
|
112
187
|
|
|
113
188
|
const otelLogger = provider.getLogger(options.name ?? "@decocms/start");
|
|
189
|
+
const minSev = SEVERITY[options.minSeverity ?? "warn"].number;
|
|
114
190
|
|
|
115
191
|
return {
|
|
116
192
|
log(level, msg, attrs) {
|
|
117
193
|
const sev = SEVERITY[level];
|
|
194
|
+
if (sev.number < minSev) return;
|
|
118
195
|
otelLogger.emit({
|
|
119
196
|
severityNumber: sev.number,
|
|
120
197
|
severityText: sev.text,
|
|
@@ -229,6 +306,11 @@ export function createOtelMeterAdapter(
|
|
|
229
306
|
readers: [reader],
|
|
230
307
|
views,
|
|
231
308
|
});
|
|
309
|
+
// See `flushHandlers` registry comment — `PeriodicExportingMetricReader`
|
|
310
|
+
// ticks every `exportIntervalMillis` (default 60s); without per-request
|
|
311
|
+
// forceFlush the metric batch typically dies with the Workers isolate
|
|
312
|
+
// before its first scheduled tick.
|
|
313
|
+
registerOtelFlushHandler(() => provider.forceFlush());
|
|
232
314
|
|
|
233
315
|
try {
|
|
234
316
|
metricsApi.setGlobalMeterProvider(provider);
|
package/src/sdk/sampler.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* URL-based head sampler — port of `deco-cx/deco/observability/otel/samplers/urlBased.ts`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* **No longer wired into `instrumentWorker` by default.** As of 4.4.0 the
|
|
5
|
+
* recommended path for trace sampling is Cloudflare's wrangler-level
|
|
6
|
+
* `observability.traces.head_sampling_rate`, which is one global rate per
|
|
7
|
+
* Worker. This module stays as an opt-in escape hatch for sites that need
|
|
8
|
+
* URL-pattern-aware sampling (e.g. always trace `/checkout`, sample
|
|
9
|
+
* homepages at 1%) — those sites must wire OTel themselves outside the
|
|
10
|
+
* default `instrumentWorker` flow.
|
|
11
|
+
*
|
|
12
|
+
* The sampler reads `OTEL_SAMPLING_CONFIG` (base64-encoded JSON) and
|
|
13
|
+
* decides each trace's sample rate based on the matching pattern.
|
|
7
14
|
*
|
|
8
15
|
* Wrapped in `ParentBasedSampler` so a span inherits its parent's sampling
|
|
9
|
-
* decision when one exists (i.e. distributed traces
|
|
16
|
+
* decision when one exists (i.e. distributed traces stay consistent end
|
|
10
17
|
* to end).
|
|
11
18
|
*
|
|
12
19
|
* **Default ratio.** When no `default` is provided in the config (or the env
|
|
@@ -28,6 +35,9 @@
|
|
|
28
35
|
* ]
|
|
29
36
|
* }
|
|
30
37
|
* ```
|
|
38
|
+
*
|
|
39
|
+
* @deprecated Slated for removal in 5.0.0 unless a site declares an active
|
|
40
|
+
* need. Use Cloudflare's `head_sampling_rate` first.
|
|
31
41
|
*/
|
|
32
42
|
|
|
33
43
|
import { type Attributes, type Context, type Link, type SpanKind, trace } from "@opentelemetry/api";
|
|
@@ -189,7 +199,9 @@ export function decodeSamplingConfig(raw: string | undefined): SamplingConfig |
|
|
|
189
199
|
|
|
190
200
|
/**
|
|
191
201
|
* Build a `ParentBasedSampler` rooted at our URL-based sampler.
|
|
192
|
-
*
|
|
202
|
+
* Wire as the `headSampler` for any custom OTel SDK setup (e.g. a site
|
|
203
|
+
* that opts back into `@microlabs/otel-cf-workers` outside the default
|
|
204
|
+
* `instrumentWorker` flow).
|
|
193
205
|
*/
|
|
194
206
|
export function createUrlBasedHeadSampler(config: SamplingConfig | null): Sampler {
|
|
195
207
|
const root = new URLBasedSampler(config ?? {});
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -949,10 +949,13 @@ export function createDecoWorkerEntry(
|
|
|
949
949
|
// via getRuntimeEnv() in sdk/otelAdapters.ts.
|
|
950
950
|
setRuntimeEnv(env);
|
|
951
951
|
|
|
952
|
-
// Wrap inner handler in a single root span
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
//
|
|
952
|
+
// Wrap inner handler in a single root span carrying our normalized
|
|
953
|
+
// path/method attributes. With Cloudflare-managed trace export
|
|
954
|
+
// (`observability.traces.destinations` in wrangler.jsonc), this
|
|
955
|
+
// span — and any `withTracing` spans nested below it — flow to
|
|
956
|
+
// HyperDX via CF's platform-managed OTLP push, since the bridge
|
|
957
|
+
// in `instrumentWorker` configures the `@opentelemetry/api`
|
|
958
|
+
// global tracer for us.
|
|
956
959
|
return withTracing(
|
|
957
960
|
"deco.http.request",
|
|
958
961
|
async () => {
|