autotel-cloudflare 18.0.0 → 18.2.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/README.md +40 -0
- package/dist/actors.d.ts +7 -7
- package/dist/agents.d.ts +103 -103
- package/dist/{handlers-4X_6aEtg.js → handlers-7NFDRZT2.js} +89 -12
- package/dist/{handlers-BRfAgfP7.d.ts → handlers-CEAD5hVy.d.ts} +3 -1
- package/dist/handlers.d.ts +2 -2
- package/dist/handlers.js +2 -2
- package/dist/index.d.ts +8 -8
- package/dist/index.js +14 -7
- package/dist/native-tracing-Dgw_-i8X.js +67 -0
- package/dist/native.js +1 -1
- package/dist/tail.d.ts +110 -0
- package/dist/tail.js +249 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +0 -2
- package/package.json +18 -14
- package/dist/native-tracing-D0EyAtsF.js +0 -32
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ConfigurationOption } from "autotel-edge";
|
|
2
2
|
//#region src/handlers/durable-objects.d.ts
|
|
3
|
+
/** `exception.type` of the alarm-loop signal. */
|
|
4
|
+
declare const RUNAWAY_ALARM_EXCEPTION = "autotel.RunawayAlarm";
|
|
3
5
|
/**
|
|
4
6
|
* Instrument a Durable Object class
|
|
5
7
|
*
|
|
@@ -91,4 +93,4 @@ declare function instrumentDO<C extends new (...args: any[]) => any>(doClass: C,
|
|
|
91
93
|
*/
|
|
92
94
|
declare function instrumentWorkflow<C extends new (...args: any[]) => any>(workflowClass: C, workflowName: string, config: ConfigurationOption): C;
|
|
93
95
|
//#endregion
|
|
94
|
-
export {
|
|
96
|
+
export { RUNAWAY_ALARM_EXCEPTION as n, instrumentDO as r, instrumentWorkflow as t };
|
package/dist/handlers.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as instrumentDO, t as instrumentWorkflow } from "./handlers-
|
|
2
|
-
export { instrumentDO, instrumentWorkflow };
|
|
1
|
+
import { n as RUNAWAY_ALARM_EXCEPTION, r as instrumentDO, t as instrumentWorkflow } from "./handlers-CEAD5hVy.js";
|
|
2
|
+
export { RUNAWAY_ALARM_EXCEPTION, instrumentDO, instrumentWorkflow };
|
package/dist/handlers.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as instrumentDO, t as instrumentWorkflow } from "./handlers-
|
|
1
|
+
import { n as RUNAWAY_ALARM_EXCEPTION, r as instrumentDO, t as instrumentWorkflow } from "./handlers-7NFDRZT2.js";
|
|
2
2
|
|
|
3
|
-
export { instrumentDO, instrumentWorkflow };
|
|
3
|
+
export { RUNAWAY_ALARM_EXCEPTION, instrumentDO, instrumentWorkflow };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as instrumentQueueProducer, c as instrumentAI, d as instrumentKV, f as instrumentR2, i as instrumentAnalyticsEngine, l as instrumentBindings, n as instrumentRateLimiter, o as instrumentHyperdrive, p as instrumentServiceBinding, r as instrumentImages, s as instrumentVectorize, t as instrumentBrowserRendering, u as instrumentD1 } from "./bindings-BcSPZDWF.js";
|
|
2
|
-
import { n as instrumentDO, t as instrumentWorkflow } from "./handlers-
|
|
2
|
+
import { n as RUNAWAY_ALARM_EXCEPTION, r as instrumentDO, t as instrumentWorkflow } from "./handlers-CEAD5hVy.js";
|
|
3
3
|
import { a as createWorkersLogger, c as getRequestLogger, i as WorkersLoggerOptions, l as getWorkflowLogger, n as ExecutionLogger, o as getActorLogger, r as ExecutionLoggerOptions, s as getQueueLogger, t as ExecutionLogSnapshot } from "./execution-logger-CCn9HGYC.js";
|
|
4
4
|
import { n as isNativeTracingAvailable, t as getNativeTracerFromCtx } from "./native-tracing-DFaGAoR7.js";
|
|
5
5
|
import { ConfigurationOption } from "autotel-edge";
|
|
@@ -27,7 +27,7 @@ export * from "autotel-edge";
|
|
|
27
27
|
* })
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
declare function instrument<E, Q = any, C = any>(handler: ExportedHandler<E, Q, C>, config: ConfigurationOption): ExportedHandler<E, Q, C>;
|
|
30
|
+
export declare function instrument<E, Q = any, C = any>(handler: ExportedHandler<E, Q, C>, config: ConfigurationOption): ExportedHandler<E, Q, C>;
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/wrappers/wrap-module.d.ts
|
|
33
33
|
/**
|
|
@@ -38,7 +38,7 @@ declare function instrument<E, Q = any, C = any>(handler: ExportedHandler<E, Q,
|
|
|
38
38
|
* @param handler The worker handler to wrap
|
|
39
39
|
* @returns Instrumented handler
|
|
40
40
|
*/
|
|
41
|
-
declare function wrapModule<E, Q = any, C = any>(config: ConfigurationOption, handler: ExportedHandler<E, Q, C>): ExportedHandler<E, Q, C>;
|
|
41
|
+
export declare function wrapModule<E, Q = any, C = any>(config: ConfigurationOption, handler: ExportedHandler<E, Q, C>): ExportedHandler<E, Q, C>;
|
|
42
42
|
//#endregion
|
|
43
43
|
//#region src/wrappers/wrap-do.d.ts
|
|
44
44
|
/**
|
|
@@ -49,7 +49,7 @@ declare function wrapModule<E, Q = any, C = any>(config: ConfigurationOption, ha
|
|
|
49
49
|
* @param doClass The Durable Object class to wrap
|
|
50
50
|
* @returns Instrumented Durable Object class
|
|
51
51
|
*/
|
|
52
|
-
declare function wrapDurableObject<T extends DurableObject>(config: ConfigurationOption, doClass: new (state: DurableObjectState, env: any) => T): new (state: DurableObjectState, env: any) => T;
|
|
52
|
+
export declare function wrapDurableObject<T extends DurableObject>(config: ConfigurationOption, doClass: new (state: DurableObjectState, env: any) => T): new (state: DurableObjectState, env: any) => T;
|
|
53
53
|
//#endregion
|
|
54
54
|
//#region src/wrappers/define-worker-fetch.d.ts
|
|
55
55
|
type DefineWorkerFetchOptions = WorkersLoggerOptions;
|
|
@@ -59,7 +59,7 @@ type WorkerFetchHandler<E> = (request: Request, env: E, ctx: ExecutionContext, l
|
|
|
59
59
|
* - the handler is instrumented (spans, propagation, waitUntil export flush)
|
|
60
60
|
* - the handler receives a request-scoped logger as its fourth argument
|
|
61
61
|
*/
|
|
62
|
-
declare function defineWorkerFetch<E = unknown>(config: ConfigurationOption, handler: WorkerFetchHandler<E>, loggerOptions?: DefineWorkerFetchOptions): {
|
|
62
|
+
export declare function defineWorkerFetch<E = unknown>(config: ConfigurationOption, handler: WorkerFetchHandler<E>, loggerOptions?: DefineWorkerFetchOptions): {
|
|
63
63
|
fetch: (request: Request, env: E, ctx: ExecutionContext) => Promise<Response>;
|
|
64
64
|
};
|
|
65
65
|
//#endregion
|
|
@@ -81,7 +81,7 @@ declare function defineWorkerFetch<E = unknown>(config: ConfigurationOption, han
|
|
|
81
81
|
* **Note:** This is called automatically when the library is initialized with
|
|
82
82
|
* `instrumentation.instrumentGlobalFetch: true` (default).
|
|
83
83
|
*/
|
|
84
|
-
declare function instrumentGlobalFetch(): void;
|
|
84
|
+
export declare function instrumentGlobalFetch(): void;
|
|
85
85
|
//#endregion
|
|
86
86
|
//#region src/global/cache.d.ts
|
|
87
87
|
/**
|
|
@@ -100,6 +100,6 @@ declare function instrumentGlobalFetch(): void;
|
|
|
100
100
|
* **Note:** This is called automatically when the library is initialized with
|
|
101
101
|
* `instrumentation.instrumentGlobalCache: true` (default).
|
|
102
102
|
*/
|
|
103
|
-
declare function instrumentGlobalCache(): void;
|
|
103
|
+
export declare function instrumentGlobalCache(): void;
|
|
104
104
|
//#endregion
|
|
105
|
-
export { type DefineWorkerFetchOptions, type ExecutionLogSnapshot, type ExecutionLogger, type ExecutionLoggerOptions, type WorkerFetchHandler, type WorkersLoggerOptions, createWorkersLogger,
|
|
105
|
+
export { type DefineWorkerFetchOptions, type ExecutionLogSnapshot, type ExecutionLogger, type ExecutionLoggerOptions, RUNAWAY_ALARM_EXCEPTION, type WorkerFetchHandler, type WorkersLoggerOptions, createWorkersLogger, getActorLogger, getNativeTracerFromCtx, getQueueLogger, getRequestLogger, getWorkflowLogger, instrumentAI, instrumentAnalyticsEngine, instrumentBindings, instrumentBrowserRendering, instrumentD1, instrumentDO, instrumentHyperdrive, instrumentImages, instrumentKV, instrumentQueueProducer, instrumentR2, instrumentRateLimiter, instrumentServiceBinding, instrumentVectorize, instrumentWorkflow, isNativeTracingAvailable };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { _ as trapArgs, a as asRecord, c as asString, f as member, g as readProperty, i as asNumber, n as asBoolean, r as asFunction, t as applyTrap } from "./values-BC6rdpGG.js";
|
|
2
2
|
import { a as getWorkflowLogger, i as getRequestLogger, n as getActorLogger, r as getQueueLogger, t as createWorkersLogger } from "./execution-logger-6IZS5fEz.js";
|
|
3
|
-
import { n as isNativeTracingAvailable, t as getNativeTracerFromCtx } from "./native-tracing-
|
|
3
|
+
import { n as isNativeTracingAvailable, t as getNativeTracerFromCtx } from "./native-tracing-Dgw_-i8X.js";
|
|
4
4
|
import { a as unwrap, o as wrap, r as proxyExecutionContext, t as toException } from "./exception-D3xOCdBW.js";
|
|
5
5
|
import { t as workerTracer } from "./tracer-z8zwRBVS.js";
|
|
6
6
|
import { a as instrumentKV, c as instrumentImages, d as instrumentHyperdrive, f as instrumentVectorize, i as instrumentD1, l as instrumentAnalyticsEngine, n as instrumentRateLimiter, o as instrumentR2, p as instrumentAI, r as instrumentBindings, s as instrumentServiceBinding, t as instrumentBrowserRendering, u as instrumentQueueProducer } from "./bindings-BK2AmtNc.js";
|
|
7
|
-
import { n as instrumentDO, t as instrumentWorkflow } from "./handlers-
|
|
8
|
-
import { WorkerTracer, WorkerTracerProvider, createInitialiser, ensureGlobalContextManager, getActiveConfig, getServiceForPath, setConfig, shouldInstrumentPath, withNativeTracer } from "autotel-edge";
|
|
7
|
+
import { n as RUNAWAY_ALARM_EXCEPTION, r as instrumentDO, t as instrumentWorkflow } from "./handlers-7NFDRZT2.js";
|
|
8
|
+
import { WorkerTracer, WorkerTracerProvider, createInitialiser, createNativeTraceContext, ensureGlobalContextManager, getActiveConfig, getServiceForPath, runWithNativeTraceContext, setConfig, shouldInstrumentPath, withNativeTracer, withoutNativeTracer } from "autotel-edge";
|
|
9
9
|
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
10
10
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
11
11
|
|
|
@@ -516,6 +516,7 @@ function createHandlerFlow(instrumentation) {
|
|
|
516
516
|
};
|
|
517
517
|
}
|
|
518
518
|
let warnedMissingNative = false;
|
|
519
|
+
let warnedLocalNative = false;
|
|
519
520
|
/**
|
|
520
521
|
* Resolve whether this invocation should use Cloudflare's native tracer.
|
|
521
522
|
*
|
|
@@ -532,6 +533,10 @@ function resolveNativeTracer(config, ctx, trigger) {
|
|
|
532
533
|
warnedMissingNative = true;
|
|
533
534
|
console.warn("[autotel-cloudflare] nativeTracing is 'on' but Cloudflare native tracing was not detected. Enable observability.traces in your Wrangler config (and use a recent compatibility_date). Falling back to the autotel OTLP exporter.");
|
|
534
535
|
}
|
|
536
|
+
if (nativeTracer && mode === "auto" && !warnedLocalNative && config.spanProcessors.length > 0 && trigger instanceof Request && /^(localhost|127\.0\.0\.1|\[::1\])$/.test(new URL(trigger.url).hostname)) {
|
|
537
|
+
warnedLocalNative = true;
|
|
538
|
+
console.warn("[autotel-cloudflare] Cloudflare native tracing is on in wrangler dev, so spans go to the Local Explorer (/cdn-cgi/explorer), not your exporter. To stream to autotel-devtools, set NATIVE_TRACING=off in .dev.vars (or nativeTracing: \"off\").");
|
|
539
|
+
}
|
|
535
540
|
return nativeTracer;
|
|
536
541
|
}
|
|
537
542
|
/**
|
|
@@ -547,7 +552,9 @@ function resolveNativeTracer(config, ctx, trigger) {
|
|
|
547
552
|
function runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer) {
|
|
548
553
|
ensureGlobalContextManager();
|
|
549
554
|
const nativeContext = withNativeTracer(nativeTracer, setConfig(config));
|
|
550
|
-
|
|
555
|
+
const run = () => handlerFn(trigger, env, ctx);
|
|
556
|
+
const root = nativeTracer.getActiveSpan?.();
|
|
557
|
+
return context.with(nativeContext, () => root ? runWithNativeTraceContext(createNativeTraceContext(root, "handler", nativeTracer.correlationId), run) : run());
|
|
551
558
|
}
|
|
552
559
|
/**
|
|
553
560
|
* Create handler proxy
|
|
@@ -559,7 +566,7 @@ function createHandlerProxy(_handler, handlerFn, initialiser, instrumentation) {
|
|
|
559
566
|
const nativeTracer = resolveNativeTracer(config, ctx, trigger);
|
|
560
567
|
if (nativeTracer) return runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer);
|
|
561
568
|
const instrumentedEnv = instrumentBindings(env);
|
|
562
|
-
const configContext = setConfig(config);
|
|
569
|
+
const configContext = withoutNativeTracer(setConfig(config));
|
|
563
570
|
initProvider(config);
|
|
564
571
|
const flowFn = createHandlerFlow(instrumentation);
|
|
565
572
|
return context.with(configContext, () => {
|
|
@@ -589,7 +596,7 @@ function createHandlerProxyWithConfig(_handler, handlerFn, initialiser, createIn
|
|
|
589
596
|
const nativeTracer = resolveNativeTracer(config, ctx, trigger);
|
|
590
597
|
if (nativeTracer) return runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer);
|
|
591
598
|
const instrumentedEnv = instrumentBindings(env);
|
|
592
|
-
const configContext = setConfig(config);
|
|
599
|
+
const configContext = withoutNativeTracer(setConfig(config));
|
|
593
600
|
initProvider(config);
|
|
594
601
|
const flowFn = createHandlerFlow(createInstrumentation(config));
|
|
595
602
|
return context.with(configContext, () => {
|
|
@@ -735,4 +742,4 @@ function defineWorkerFetch(config, handler, loggerOptions = {}) {
|
|
|
735
742
|
}
|
|
736
743
|
|
|
737
744
|
//#endregion
|
|
738
|
-
export { createWorkersLogger, defineWorkerFetch, getActorLogger, getNativeTracerFromCtx, getQueueLogger, getRequestLogger, getWorkflowLogger, instrument, instrumentAI, instrumentAnalyticsEngine, instrumentBindings, instrumentBrowserRendering, instrumentD1, instrumentDO, instrumentGlobalCache, instrumentGlobalFetch, instrumentHyperdrive, instrumentImages, instrumentKV, instrumentQueueProducer, instrumentR2, instrumentRateLimiter, instrumentServiceBinding, instrumentVectorize, instrumentWorkflow, isNativeTracingAvailable, wrapDurableObject, wrapModule };
|
|
745
|
+
export { RUNAWAY_ALARM_EXCEPTION, createWorkersLogger, defineWorkerFetch, getActorLogger, getNativeTracerFromCtx, getQueueLogger, getRequestLogger, getWorkflowLogger, instrument, instrumentAI, instrumentAnalyticsEngine, instrumentBindings, instrumentBrowserRendering, instrumentD1, instrumentDO, instrumentGlobalCache, instrumentGlobalFetch, instrumentHyperdrive, instrumentImages, instrumentKV, instrumentQueueProducer, instrumentR2, instrumentRateLimiter, instrumentServiceBinding, instrumentVectorize, instrumentWorkflow, isNativeTracingAvailable, wrapDurableObject, wrapModule };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { WorkerTracerProvider, setDefaultNativeTracer } from "autotel-edge";
|
|
2
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
3
|
+
import { tracing } from "cloudflare:workers";
|
|
4
|
+
|
|
5
|
+
//#region src/native/native-tracing.ts
|
|
6
|
+
/**
|
|
7
|
+
* Cloudflare native tracing adapter
|
|
8
|
+
*
|
|
9
|
+
* Bridges Cloudflare Workers' built-in custom-span API
|
|
10
|
+
* (`ctx.tracing.enterSpan()` / `import { tracing } from "cloudflare:workers"`)
|
|
11
|
+
* to autotel-edge's runtime-agnostic {@link NativeTracer} seam.
|
|
12
|
+
*
|
|
13
|
+
* When a Worker has tracing enabled (`observability.traces.enabled = true` in
|
|
14
|
+
* Wrangler) the runtime exposes `ctx.tracing`. The handler wrappers detect it,
|
|
15
|
+
* wrap it as a {@link NativeTracer}, and install it into the active context with
|
|
16
|
+
* `withNativeTracer()`. From then on every autotel `span()` / `trace()` /
|
|
17
|
+
* `enterSpan()` call — including deep inside utility functions and libraries —
|
|
18
|
+
* automatically routes to Cloudflare's native tracer and nests inside the
|
|
19
|
+
* platform's trace waterfall (fetch / KV / R2 / D1 / handler spans), exported
|
|
20
|
+
* by Cloudflare to whichever destination is configured in Wrangler.
|
|
21
|
+
*
|
|
22
|
+
* Code outside any wrapper (Durable Object RPC methods, entrypoints, helpers)
|
|
23
|
+
* falls back to the module-level `tracing` export of `cloudflare:workers`,
|
|
24
|
+
* registered below as autotel-edge's default native tracer.
|
|
25
|
+
*/
|
|
26
|
+
function readTracing(carrier) {
|
|
27
|
+
const tracing = carrier?.tracing;
|
|
28
|
+
return typeof tracing?.enterSpan === "function" ? tracing : void 0;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns `true` when Cloudflare native custom-span tracing is available on the
|
|
32
|
+
* given ExecutionContext (i.e. tracing is enabled for this Worker).
|
|
33
|
+
*/
|
|
34
|
+
function isNativeTracingAvailable(ctx) {
|
|
35
|
+
return readTracing(ctx) !== void 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Wrap Cloudflare's `ctx.tracing` as an autotel {@link NativeTracer}, or return
|
|
39
|
+
* `null` when native tracing is unavailable on this context.
|
|
40
|
+
*
|
|
41
|
+
* @param correlationId Optional per-request id (e.g. the `cf-ray` header)
|
|
42
|
+
* surfaced as `ctx.correlationId` and a `correlation.id` span attribute, so
|
|
43
|
+
* logs, custom spans, and the Cloudflare dashboard share one queryable key
|
|
44
|
+
* today — before Cloudflare exposes in-code trace/span ids.
|
|
45
|
+
*/
|
|
46
|
+
function getNativeTracerFromCtx(ctx, correlationId) {
|
|
47
|
+
const tracing = readTracing(ctx);
|
|
48
|
+
if (!tracing) return null;
|
|
49
|
+
return {
|
|
50
|
+
correlationId,
|
|
51
|
+
enterSpan: (name, callback) => tracing.enterSpan(name, callback),
|
|
52
|
+
getActiveSpan: () => tracing.getActiveSpan?.(),
|
|
53
|
+
startSpan: tracing.startSpan && ((name) => tracing.startSpan(name)),
|
|
54
|
+
startActiveSpan: tracing.startActiveSpan && ((name, callback) => tracing.startActiveSpan(name, callback))
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The isolate-wide native tracer from `cloudflare:workers`, or `null` when the
|
|
59
|
+
* runtime has no tracing API. Registered as the default so `trace()`/`span()`
|
|
60
|
+
* nest in the platform waterfall even where no wrapper installed one.
|
|
61
|
+
*/
|
|
62
|
+
const platformNativeTracer = getNativeTracerFromCtx({ tracing });
|
|
63
|
+
setDefaultNativeTracer(platformNativeTracer);
|
|
64
|
+
if (platformNativeTracer) new WorkerTracerProvider([], resourceFromAttributes({})).register();
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
export { isNativeTracingAvailable as n, platformNativeTracer as r, getNativeTracerFromCtx as t };
|
package/dist/native.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as isNativeTracingAvailable, t as getNativeTracerFromCtx } from "./native-tracing-
|
|
1
|
+
import { n as isNativeTracingAvailable, t as getNativeTracerFromCtx } from "./native-tracing-Dgw_-i8X.js";
|
|
2
2
|
|
|
3
3
|
export { getNativeTracerFromCtx, isNativeTracingAvailable };
|
package/dist/tail.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
//#region src/tail.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `autotel-cloudflare/tail`: observability for Workers with no SDK in them.
|
|
4
|
+
*
|
|
5
|
+
* A Tail Worker receives every invocation of the Workers that list it in
|
|
6
|
+
* `tail_consumers`: outcome, uncaught exceptions, console output (with the
|
|
7
|
+
* exception behind each `console.error` arg), the fetch request and response
|
|
8
|
+
* status, the version that ran. This turns each invocation into one OTLP span
|
|
9
|
+
* and its console output into OTLP logs, and sends them to any OTLP endpoint
|
|
10
|
+
* (autotel-devtools, the autotel-mcp collector, a vendor). Issues, grouping,
|
|
11
|
+
* source maps and automations then work exactly as for an instrumented app.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* // tail-worker/src/index.ts
|
|
15
|
+
* import { createTailHandler } from 'autotel-cloudflare/tail';
|
|
16
|
+
* export default {
|
|
17
|
+
* tail: createTailHandler({ endpoint: (env) => env.OTLP_ENDPOINT }),
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
* ```jsonc
|
|
21
|
+
* // the observed Worker's wrangler.jsonc: no code change
|
|
22
|
+
* { "tail_consumers": [{ "service": "my-tail-worker" }] }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Dependency-free on purpose: a tail Worker runs for every invocation of
|
|
26
|
+
* everything it watches, so it should be as small as its job.
|
|
27
|
+
*/
|
|
28
|
+
/** The subset of Cloudflare's `TraceItem` this reads. */
|
|
29
|
+
export interface TailItem {
|
|
30
|
+
event?: {
|
|
31
|
+
request?: {
|
|
32
|
+
method: string;
|
|
33
|
+
url: string;
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
};
|
|
36
|
+
response?: {
|
|
37
|
+
status: number;
|
|
38
|
+
};
|
|
39
|
+
cron?: string;
|
|
40
|
+
scheduledTime?: number | Date;
|
|
41
|
+
queue?: string;
|
|
42
|
+
batchSize?: number;
|
|
43
|
+
rpcMethod?: string;
|
|
44
|
+
} | null;
|
|
45
|
+
eventTimestamp: number | null;
|
|
46
|
+
logs: Array<{
|
|
47
|
+
timestamp: number;
|
|
48
|
+
level: string;
|
|
49
|
+
message: unknown;
|
|
50
|
+
errorInfo?: Array<{
|
|
51
|
+
name: string;
|
|
52
|
+
message: string;
|
|
53
|
+
stack?: string;
|
|
54
|
+
} | null>;
|
|
55
|
+
}>;
|
|
56
|
+
exceptions: Array<{
|
|
57
|
+
timestamp: number;
|
|
58
|
+
name: string;
|
|
59
|
+
message: string;
|
|
60
|
+
stack?: string;
|
|
61
|
+
}>;
|
|
62
|
+
scriptName: string | null;
|
|
63
|
+
entrypoint?: string;
|
|
64
|
+
scriptVersion?: {
|
|
65
|
+
id?: string;
|
|
66
|
+
tag?: string;
|
|
67
|
+
};
|
|
68
|
+
tailAttributes?: Record<string, string | number | boolean>;
|
|
69
|
+
durableObjectId?: string;
|
|
70
|
+
outcome: string;
|
|
71
|
+
truncated?: boolean;
|
|
72
|
+
cpuTime?: number;
|
|
73
|
+
wallTime?: number;
|
|
74
|
+
}
|
|
75
|
+
export interface TailOptions {
|
|
76
|
+
/**
|
|
77
|
+
* `service.name` when the event has no `scriptName`, as in `wrangler dev`.
|
|
78
|
+
* In production every event names the Worker that produced it.
|
|
79
|
+
*/
|
|
80
|
+
serviceName?: string;
|
|
81
|
+
/** Repeats of one log template per invocation before `autotel.LogFlood`. `0` disables. Default 100. */
|
|
82
|
+
logFloodThreshold?: number;
|
|
83
|
+
/** Alarm runs per Durable Object within `windowMs` before `autotel.RunawayAlarm`. `false` disables. */
|
|
84
|
+
runawayAlarm?: {
|
|
85
|
+
maxRuns: number;
|
|
86
|
+
windowMs: number;
|
|
87
|
+
} | false;
|
|
88
|
+
}
|
|
89
|
+
/** Collapse ids and numbers so one loop's lines share a template. */
|
|
90
|
+
export declare function logTemplate(message: unknown): string;
|
|
91
|
+
export interface OtlpPayloads {
|
|
92
|
+
traces: {
|
|
93
|
+
resourceSpans: unknown[];
|
|
94
|
+
};
|
|
95
|
+
logs: {
|
|
96
|
+
resourceLogs: unknown[];
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Pure conversion: Cloudflare tail events → OTLP/JSON traces and logs. */
|
|
100
|
+
export declare function tailItemsToOtlp(items: TailItem[], options?: TailOptions, now?: number): OtlpPayloads;
|
|
101
|
+
export interface TailHandlerOptions<Env> extends TailOptions {
|
|
102
|
+
/** OTLP/HTTP base URL, e.g. `https://collector.example` (posts to `/v1/traces` and `/v1/logs`). */
|
|
103
|
+
endpoint: string | ((env: Env) => string | undefined);
|
|
104
|
+
headers?: Record<string, string> | ((env: Env) => Record<string, string> | undefined);
|
|
105
|
+
}
|
|
106
|
+
/** A Worker `tail` handler that ships every observed invocation as OTLP. */
|
|
107
|
+
export declare function createTailHandler<Env = Record<string, unknown>>(options: TailHandlerOptions<Env>): (events: TailItem[], env: Env, ctx: {
|
|
108
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
109
|
+
}) => Promise<void>;
|
|
110
|
+
//#endregion
|
package/dist/tail.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
//#region src/tail.ts
|
|
2
|
+
/**
|
|
3
|
+
* `autotel-cloudflare/tail`: observability for Workers with no SDK in them.
|
|
4
|
+
*
|
|
5
|
+
* A Tail Worker receives every invocation of the Workers that list it in
|
|
6
|
+
* `tail_consumers`: outcome, uncaught exceptions, console output (with the
|
|
7
|
+
* exception behind each `console.error` arg), the fetch request and response
|
|
8
|
+
* status, the version that ran. This turns each invocation into one OTLP span
|
|
9
|
+
* and its console output into OTLP logs, and sends them to any OTLP endpoint
|
|
10
|
+
* (autotel-devtools, the autotel-mcp collector, a vendor). Issues, grouping,
|
|
11
|
+
* source maps and automations then work exactly as for an instrumented app.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* // tail-worker/src/index.ts
|
|
15
|
+
* import { createTailHandler } from 'autotel-cloudflare/tail';
|
|
16
|
+
* export default {
|
|
17
|
+
* tail: createTailHandler({ endpoint: (env) => env.OTLP_ENDPOINT }),
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
* ```jsonc
|
|
21
|
+
* // the observed Worker's wrangler.jsonc: no code change
|
|
22
|
+
* { "tail_consumers": [{ "service": "my-tail-worker" }] }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Dependency-free on purpose: a tail Worker runs for every invocation of
|
|
26
|
+
* everything it watches, so it should be as small as its job.
|
|
27
|
+
*/
|
|
28
|
+
/** `exception.type` autotel uses for its log-flood and runaway-alarm reports. */
|
|
29
|
+
const LOG_FLOOD = "autotel.LogFlood";
|
|
30
|
+
const RUNAWAY_ALARM = "autotel.RunawayAlarm";
|
|
31
|
+
function anyValue(value) {
|
|
32
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
33
|
+
if (typeof value === "number") return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
34
|
+
return { stringValue: typeof value === "string" ? value : JSON.stringify(value) };
|
|
35
|
+
}
|
|
36
|
+
function attrs(record) {
|
|
37
|
+
return Object.entries(record).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => ({
|
|
38
|
+
key,
|
|
39
|
+
value: anyValue(value)
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
const nanos = (ms) => `${Math.round(ms)}000000`;
|
|
43
|
+
function hex(bytes) {
|
|
44
|
+
const buffer = new Uint8Array(bytes);
|
|
45
|
+
crypto.getRandomValues(buffer);
|
|
46
|
+
return [...buffer].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
47
|
+
}
|
|
48
|
+
/** Join the caller's trace when the request carried a W3C `traceparent`. */
|
|
49
|
+
function parentOf(headers) {
|
|
50
|
+
const header = headers?.traceparent ?? headers?.Traceparent;
|
|
51
|
+
const match = header && /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/.exec(header);
|
|
52
|
+
return match ? {
|
|
53
|
+
traceId: match[1],
|
|
54
|
+
parentSpanId: match[2]
|
|
55
|
+
} : void 0;
|
|
56
|
+
}
|
|
57
|
+
function format(message) {
|
|
58
|
+
return (Array.isArray(message) ? message : [message]).map((part) => typeof part === "string" ? part : JSON.stringify(part)).join(" ").slice(0, 2e3);
|
|
59
|
+
}
|
|
60
|
+
/** Collapse ids and numbers so one loop's lines share a template. */
|
|
61
|
+
function logTemplate(message) {
|
|
62
|
+
return format(message).replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<uuid>").replaceAll(/\b(?=[0-9a-f]*\d)[0-9a-f]{8,}\b/gi, "<hex>").replaceAll(/\d+/g, "<n>");
|
|
63
|
+
}
|
|
64
|
+
const SEVERITY = {
|
|
65
|
+
debug: [5, "DEBUG"],
|
|
66
|
+
log: [9, "INFO"],
|
|
67
|
+
info: [9, "INFO"],
|
|
68
|
+
warn: [13, "WARN"],
|
|
69
|
+
error: [17, "ERROR"]
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Alarm runs per Durable Object, per isolate: best effort, as long as a tail
|
|
73
|
+
* isolate lives. Keyed by when each alarm *ran* (the invocation's timestamp),
|
|
74
|
+
* never when the tail received it: events arrive batched, late and out of order.
|
|
75
|
+
*/
|
|
76
|
+
const alarmRuns = /* @__PURE__ */ new Map();
|
|
77
|
+
function alarmTimestamp(item, now) {
|
|
78
|
+
const scheduled = item.event?.scheduledTime;
|
|
79
|
+
return item.eventTimestamp ?? (scheduled instanceof Date ? scheduled.getTime() : scheduled) ?? now;
|
|
80
|
+
}
|
|
81
|
+
function runawayAlarm(item, options, now) {
|
|
82
|
+
const limits = options.runawayAlarm ?? {
|
|
83
|
+
maxRuns: 10,
|
|
84
|
+
windowMs: 6e4
|
|
85
|
+
};
|
|
86
|
+
if (!limits || !item.durableObjectId || item.event?.scheduledTime === void 0 || item.event.cron !== void 0) return;
|
|
87
|
+
const at = alarmTimestamp(item, now);
|
|
88
|
+
const state = alarmRuns.get(item.durableObjectId) ?? { runs: [] };
|
|
89
|
+
const newest = Math.max(at, ...state.runs);
|
|
90
|
+
state.runs = [...state.runs, at].filter((t) => newest - t < 2 * limits.windowMs).sort((a, b) => a - b).slice(-1e3);
|
|
91
|
+
if (alarmRuns.size > 1e3 && !alarmRuns.has(item.durableObjectId)) alarmRuns.clear();
|
|
92
|
+
alarmRuns.set(item.durableObjectId, state);
|
|
93
|
+
let busiest = 0;
|
|
94
|
+
for (const startAt of state.runs) {
|
|
95
|
+
if (startAt > at || at - startAt >= limits.windowMs) continue;
|
|
96
|
+
const count = state.runs.filter((t) => t >= startAt && t - startAt < limits.windowMs).length;
|
|
97
|
+
busiest = Math.max(busiest, count);
|
|
98
|
+
}
|
|
99
|
+
if (busiest <= limits.maxRuns) return void 0;
|
|
100
|
+
if (state.reportedAt !== void 0 && Math.abs(at - state.reportedAt) < limits.windowMs) return;
|
|
101
|
+
state.reportedAt = at;
|
|
102
|
+
return `Durable Object ${item.durableObjectId} alarm ran ${busiest} times in ${limits.windowMs / 1e3}s`;
|
|
103
|
+
}
|
|
104
|
+
function spanName(item) {
|
|
105
|
+
const event = item.event;
|
|
106
|
+
if (event?.request) try {
|
|
107
|
+
return `${event.request.method} ${new URL(event.request.url).pathname}`;
|
|
108
|
+
} catch {
|
|
109
|
+
return event.request.method;
|
|
110
|
+
}
|
|
111
|
+
if (event?.cron !== void 0) return `scheduled ${event.cron}`;
|
|
112
|
+
if (event?.queue) return `queue ${event.queue}`;
|
|
113
|
+
if (event?.rpcMethod) return `rpc ${event.rpcMethod}`;
|
|
114
|
+
if (item.durableObjectId && event?.scheduledTime !== void 0) return "alarm";
|
|
115
|
+
return item.entrypoint ?? "invocation";
|
|
116
|
+
}
|
|
117
|
+
/** Pure conversion: Cloudflare tail events → OTLP/JSON traces and logs. */
|
|
118
|
+
function tailItemsToOtlp(items, options = {}, now = Date.now()) {
|
|
119
|
+
const resourceSpans = [];
|
|
120
|
+
const resourceLogs = [];
|
|
121
|
+
const threshold = options.logFloodThreshold ?? 100;
|
|
122
|
+
for (const item of items) {
|
|
123
|
+
const start = item.eventTimestamp ?? item.logs[0]?.timestamp ?? now;
|
|
124
|
+
const end = start + Math.max(item.wallTime ?? 0, 0);
|
|
125
|
+
const parent = parentOf(item.event?.request?.headers);
|
|
126
|
+
const traceId = parent?.traceId ?? hex(16);
|
|
127
|
+
const spanId = hex(8);
|
|
128
|
+
const status = item.event?.response?.status;
|
|
129
|
+
const failed = item.outcome !== "ok" || item.exceptions.length > 0;
|
|
130
|
+
const exceptionEvent = (time, name, message, stack) => ({
|
|
131
|
+
name: "exception",
|
|
132
|
+
timeUnixNano: nanos(time),
|
|
133
|
+
attributes: attrs({
|
|
134
|
+
"exception.type": name,
|
|
135
|
+
"exception.message": message,
|
|
136
|
+
"exception.stacktrace": stack
|
|
137
|
+
})
|
|
138
|
+
});
|
|
139
|
+
const events = item.exceptions.map((e) => exceptionEvent(e.timestamp, e.name, e.message, e.stack));
|
|
140
|
+
const templates = /* @__PURE__ */ new Map();
|
|
141
|
+
for (const log of item.logs) {
|
|
142
|
+
if (log.level === "error") {
|
|
143
|
+
const errors = (log.errorInfo ?? []).filter((e) => e !== null);
|
|
144
|
+
if (errors.length > 0) for (const e of errors) events.push(exceptionEvent(log.timestamp, e.name, e.message, e.stack));
|
|
145
|
+
else events.push(exceptionEvent(log.timestamp, "Error", format(log.message).slice(0, 500)));
|
|
146
|
+
}
|
|
147
|
+
if (threshold > 0) {
|
|
148
|
+
const template = logTemplate(log.message);
|
|
149
|
+
const count = (templates.get(template) ?? 0) + 1;
|
|
150
|
+
templates.set(template, count);
|
|
151
|
+
if (count === threshold + 1) events.push(exceptionEvent(log.timestamp, LOG_FLOOD, `"${template.slice(0, 120)}" logged ${threshold}+ times in one invocation`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const runaway = runawayAlarm(item, options, now);
|
|
155
|
+
if (runaway) events.push(exceptionEvent(start, RUNAWAY_ALARM, runaway));
|
|
156
|
+
const resource = { attributes: attrs({
|
|
157
|
+
"service.name": item.scriptName ?? options.serviceName ?? "worker",
|
|
158
|
+
"service.version": item.scriptVersion?.tag ?? item.scriptVersion?.id,
|
|
159
|
+
"cloudflare.script_version.id": item.scriptVersion?.id,
|
|
160
|
+
"cloud.provider": "cloudflare",
|
|
161
|
+
"cloud.platform": "cloudflare.workers",
|
|
162
|
+
"telemetry.sdk.name": "autotel-cloudflare/tail"
|
|
163
|
+
}) };
|
|
164
|
+
let url;
|
|
165
|
+
try {
|
|
166
|
+
url = item.event?.request ? new URL(item.event.request.url) : void 0;
|
|
167
|
+
} catch {
|
|
168
|
+
url = void 0;
|
|
169
|
+
}
|
|
170
|
+
resourceSpans.push({
|
|
171
|
+
resource,
|
|
172
|
+
scopeSpans: [{
|
|
173
|
+
scope: { name: "autotel-cloudflare/tail" },
|
|
174
|
+
spans: [{
|
|
175
|
+
traceId,
|
|
176
|
+
spanId,
|
|
177
|
+
...parent ? { parentSpanId: parent.parentSpanId } : {},
|
|
178
|
+
name: spanName(item),
|
|
179
|
+
kind: item.event?.request ? 2 : 1,
|
|
180
|
+
startTimeUnixNano: nanos(start),
|
|
181
|
+
endTimeUnixNano: nanos(end),
|
|
182
|
+
attributes: attrs({
|
|
183
|
+
...item.tailAttributes,
|
|
184
|
+
"http.request.method": item.event?.request?.method,
|
|
185
|
+
"url.full": item.event?.request?.url,
|
|
186
|
+
"url.path": url?.pathname,
|
|
187
|
+
"http.response.status_code": status,
|
|
188
|
+
"cloudflare.outcome": item.outcome,
|
|
189
|
+
"cloudflare.entrypoint": item.entrypoint,
|
|
190
|
+
"cloudflare.durable_object.id": item.durableObjectId,
|
|
191
|
+
"cloudflare.cpu_time_ms": item.cpuTime,
|
|
192
|
+
"cloudflare.truncated": item.truncated || void 0,
|
|
193
|
+
"faas.trigger": item.event?.request ? "http" : item.event?.cron === void 0 ? item.event?.queue ? "pubsub" : "other" : "timer"
|
|
194
|
+
}),
|
|
195
|
+
events,
|
|
196
|
+
status: failed ? {
|
|
197
|
+
code: 2,
|
|
198
|
+
message: item.exceptions[0]?.message ?? `outcome: ${item.outcome}`
|
|
199
|
+
} : { code: 0 }
|
|
200
|
+
}]
|
|
201
|
+
}]
|
|
202
|
+
});
|
|
203
|
+
if (item.logs.length > 0) resourceLogs.push({
|
|
204
|
+
resource,
|
|
205
|
+
scopeLogs: [{
|
|
206
|
+
scope: { name: "autotel-cloudflare/tail" },
|
|
207
|
+
logRecords: item.logs.map((log) => {
|
|
208
|
+
const [severityNumber, severityText] = SEVERITY[log.level] ?? [9, "INFO"];
|
|
209
|
+
return {
|
|
210
|
+
timeUnixNano: nanos(log.timestamp),
|
|
211
|
+
severityNumber,
|
|
212
|
+
severityText,
|
|
213
|
+
body: { stringValue: format(log.message) },
|
|
214
|
+
traceId,
|
|
215
|
+
spanId
|
|
216
|
+
};
|
|
217
|
+
})
|
|
218
|
+
}]
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
traces: { resourceSpans },
|
|
223
|
+
logs: { resourceLogs }
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** A Worker `tail` handler that ships every observed invocation as OTLP. */
|
|
227
|
+
function createTailHandler(options) {
|
|
228
|
+
return async (events, env, ctx) => {
|
|
229
|
+
const endpoint = typeof options.endpoint === "function" ? options.endpoint(env) : options.endpoint;
|
|
230
|
+
if (!endpoint || events.length === 0) return;
|
|
231
|
+
const headers = {
|
|
232
|
+
"content-type": "application/json",
|
|
233
|
+
...typeof options.headers === "function" ? options.headers(env) : options.headers
|
|
234
|
+
};
|
|
235
|
+
const { traces, logs } = tailItemsToOtlp(events, options);
|
|
236
|
+
const base = endpoint.replace(/\/+$/, "").replace(/\/v1\/(traces|logs)$/, "");
|
|
237
|
+
const send = (path, body) => fetch(`${base}${path}`, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers,
|
|
240
|
+
body: JSON.stringify(body)
|
|
241
|
+
}).then((res) => {
|
|
242
|
+
if (!res.ok) console.warn(`[autotel-cloudflare/tail] ${path} → HTTP ${res.status}`);
|
|
243
|
+
});
|
|
244
|
+
ctx.waitUntil(Promise.all([send("/v1/traces", traces), logs.resourceLogs.length > 0 ? send("/v1/logs", logs) : void 0]).catch((error) => console.warn("[autotel-cloudflare/tail] export failed", error)));
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
//#endregion
|
|
249
|
+
export { createTailHandler, logTemplate, tailItemsToOtlp };
|
package/dist/testing.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "autotel-edge/testing";
|
|
1
|
+
export type * from "autotel-edge/testing";
|
package/dist/testing.js
CHANGED