autotel-cloudflare 2.19.0 → 3.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/README.md +41 -0
- package/dist/agents.d.ts +626 -123
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +253 -171
- package/dist/agents.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -2
- package/dist/index.js.map +1 -1
- package/dist/native-tracing-D0EyAtsF.js +33 -0
- package/dist/native-tracing-D0EyAtsF.js.map +1 -0
- package/dist/native-tracing-DSo5mody.d.ts +21 -0
- package/dist/native-tracing-DSo5mody.d.ts.map +1 -0
- package/dist/native.d.ts +2 -0
- package/dist/native.js +3 -0
- package/package.json +7 -3
- package/src/agents/agent.ts +327 -0
- package/src/agents/base.ts +26 -0
- package/src/agents/index.ts +5 -1
- package/src/agents/mcp.ts +38 -0
- package/src/agents/observability.ts +145 -0
- package/src/agents/otel-observability.test.ts +165 -232
- package/src/agents/otel-observability.ts +350 -267
- package/src/agents/types.ts +26 -132
- package/src/agents.ts +17 -6
- package/src/index.ts +8 -0
- package/src/native/native-tracing.test.ts +54 -0
- package/src/native/native-tracing.ts +83 -0
- package/src/native.ts +12 -0
- package/src/wrappers/instrument.ts +83 -5
- package/src/wrappers/native-instrument.test.ts +153 -0
package/src/agents/types.ts
CHANGED
|
@@ -2,159 +2,53 @@
|
|
|
2
2
|
* Type definitions for Cloudflare Agents SDK observability integration
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { ConfigurationOption } from 'autotel-edge';
|
|
6
5
|
import type { Attributes } from '@opentelemetry/api';
|
|
6
|
+
import type { ConfigurationOption } from 'autotel-edge';
|
|
7
|
+
import type { AgentObservabilityEvent } from './agent';
|
|
8
|
+
import type { BaseEvent } from './base';
|
|
9
|
+
import type { MCPObservabilityEvent } from './mcp';
|
|
10
|
+
import type {
|
|
11
|
+
ChannelEventMap,
|
|
12
|
+
Observability,
|
|
13
|
+
ObservabilityEvent,
|
|
14
|
+
} from './observability';
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*/
|
|
11
|
-
export interface BaseAgentEvent<
|
|
12
|
-
T extends string,
|
|
13
|
-
Payload extends Record<string, unknown> = Record<string, unknown>
|
|
14
|
-
> {
|
|
15
|
-
type: T;
|
|
16
|
-
/** Unique identifier for the event */
|
|
17
|
-
id: string;
|
|
18
|
-
/** Human-readable message for logging */
|
|
19
|
-
displayMessage: string;
|
|
20
|
-
/** Event payload with type-specific data */
|
|
21
|
-
payload: Payload & Record<string, unknown>;
|
|
22
|
-
/** Timestamp in milliseconds since epoch */
|
|
23
|
-
timestamp: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Agent-specific observability events
|
|
28
|
-
*/
|
|
29
|
-
export type AgentObservabilityEvent =
|
|
30
|
-
| BaseAgentEvent<'state:update', Record<string, unknown>>
|
|
31
|
-
| BaseAgentEvent<
|
|
32
|
-
'rpc',
|
|
33
|
-
{
|
|
34
|
-
method: string;
|
|
35
|
-
streaming?: boolean;
|
|
36
|
-
}
|
|
37
|
-
>
|
|
38
|
-
| BaseAgentEvent<'message:request' | 'message:response', Record<string, unknown>>
|
|
39
|
-
| BaseAgentEvent<'message:clear'>
|
|
40
|
-
| BaseAgentEvent<
|
|
41
|
-
'schedule:create' | 'schedule:execute' | 'schedule:cancel',
|
|
42
|
-
{
|
|
43
|
-
callback: string;
|
|
44
|
-
id: string;
|
|
45
|
-
}
|
|
46
|
-
>
|
|
47
|
-
| BaseAgentEvent<'destroy'>
|
|
48
|
-
| BaseAgentEvent<
|
|
49
|
-
'connect',
|
|
50
|
-
{
|
|
51
|
-
connectionId: string;
|
|
52
|
-
}
|
|
53
|
-
>;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* MCP-specific observability events
|
|
57
|
-
*/
|
|
58
|
-
export type MCPObservabilityEvent =
|
|
59
|
-
| BaseAgentEvent<'mcp:client:preconnect', { serverId: string }>
|
|
60
|
-
| BaseAgentEvent<
|
|
61
|
-
'mcp:client:connect',
|
|
62
|
-
{ url: string; transport: string; state: string; error?: string }
|
|
63
|
-
>
|
|
64
|
-
| BaseAgentEvent<
|
|
65
|
-
'mcp:client:authorize',
|
|
66
|
-
{
|
|
67
|
-
serverId: string;
|
|
68
|
-
authUrl: string;
|
|
69
|
-
clientId?: string;
|
|
70
|
-
}
|
|
71
|
-
>
|
|
72
|
-
| BaseAgentEvent<'mcp:client:discover', Record<string, unknown>>;
|
|
16
|
+
export type { BaseEvent, AgentObservabilityEvent, MCPObservabilityEvent };
|
|
17
|
+
export type { Observability, ObservabilityEvent, ChannelEventMap };
|
|
73
18
|
|
|
74
|
-
|
|
75
|
-
* Union of all observability event types
|
|
76
|
-
*/
|
|
77
|
-
export type ObservabilityEvent = AgentObservabilityEvent | MCPObservabilityEvent;
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Observability interface from Agents SDK
|
|
81
|
-
*/
|
|
82
|
-
export interface Observability {
|
|
83
|
-
/**
|
|
84
|
-
* Emit an event for the Agent's observability implementation to handle.
|
|
85
|
-
* @param event - The event to emit
|
|
86
|
-
* @param ctx - The execution context of the invocation (optional)
|
|
87
|
-
*/
|
|
88
|
-
emit(event: ObservabilityEvent, ctx?: DurableObjectState): void;
|
|
89
|
-
}
|
|
19
|
+
export type ObservabilityExecutionContext = DurableObjectState | ExecutionContext;
|
|
90
20
|
|
|
91
|
-
/**
|
|
92
|
-
* Agent-specific instrumentation options
|
|
93
|
-
*/
|
|
94
21
|
export interface AgentInstrumentationOptions {
|
|
95
|
-
/**
|
|
96
|
-
* Whether to create spans for RPC calls
|
|
97
|
-
* @default true
|
|
98
|
-
*/
|
|
99
22
|
traceRpc?: boolean;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Whether to create spans for schedule operations
|
|
103
|
-
* @default true
|
|
104
|
-
*/
|
|
105
23
|
traceSchedule?: boolean;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
* Whether to create spans for MCP operations
|
|
109
|
-
* @default true
|
|
110
|
-
*/
|
|
24
|
+
traceQueue?: boolean;
|
|
25
|
+
traceSubmissions?: boolean;
|
|
111
26
|
traceMcp?: boolean;
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Whether to create spans for state updates
|
|
115
|
-
* @default false (can be noisy)
|
|
116
|
-
*/
|
|
117
27
|
traceStateUpdates?: boolean;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Whether to create spans for message events
|
|
121
|
-
* @default true
|
|
122
|
-
*/
|
|
123
28
|
traceMessages?: boolean;
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Whether to create spans for connect/destroy lifecycle events
|
|
127
|
-
* @default true
|
|
128
|
-
*/
|
|
129
29
|
traceLifecycle?: boolean;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
30
|
+
traceChat?: boolean;
|
|
31
|
+
traceTranscripts?: boolean;
|
|
32
|
+
traceFibers?: boolean;
|
|
33
|
+
traceToolRecovery?: boolean;
|
|
34
|
+
traceWorkflow?: boolean;
|
|
35
|
+
traceEmail?: boolean;
|
|
134
36
|
attributeExtractor?: (event: ObservabilityEvent) => Attributes;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Custom span name formatter
|
|
138
|
-
*/
|
|
139
37
|
spanNameFormatter?: (event: ObservabilityEvent) => string;
|
|
140
38
|
}
|
|
141
39
|
|
|
142
|
-
/**
|
|
143
|
-
* Configuration for OtelObservability
|
|
144
|
-
*/
|
|
145
40
|
export type OtelObservabilityConfig = ConfigurationOption & {
|
|
146
|
-
/**
|
|
147
|
-
* Agent-specific instrumentation options
|
|
148
|
-
*/
|
|
149
41
|
agents?: AgentInstrumentationOptions;
|
|
150
42
|
};
|
|
151
43
|
|
|
152
|
-
/**
|
|
153
|
-
* Semantic attributes for Agent spans
|
|
154
|
-
*/
|
|
155
44
|
export interface AgentSpanAttributes {
|
|
156
45
|
'agent.event.type': string;
|
|
157
|
-
'agent.event.id'
|
|
46
|
+
'agent.event.id'?: string;
|
|
47
|
+
'agent.class'?: string;
|
|
48
|
+
'agent.instance.name'?: string;
|
|
49
|
+
'agent.display_message'?: string;
|
|
50
|
+
'agent.framework'?: string;
|
|
51
|
+
'gen_ai.agent.name'?: string;
|
|
158
52
|
'agent.rpc.method'?: string;
|
|
159
53
|
'agent.rpc.streaming'?: boolean;
|
|
160
54
|
'agent.schedule.callback'?: string;
|
package/src/agents.ts
CHANGED
|
@@ -47,12 +47,20 @@
|
|
|
47
47
|
* const observability = createOtelObservability({
|
|
48
48
|
* service: { name: 'my-agent' },
|
|
49
49
|
* agents: {
|
|
50
|
-
* traceRpc: true,
|
|
51
|
-
* traceSchedule: true,
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
50
|
+
* traceRpc: true, // RPC and rpc:error
|
|
51
|
+
* traceSchedule: true, // schedule:* events
|
|
52
|
+
* traceQueue: true, // queue:* events
|
|
53
|
+
* traceSubmissions: true, // submission:* events
|
|
54
|
+
* traceMcp: true, // mcp:* events
|
|
55
|
+
* traceStateUpdates: false, // state:update (off by default; noisy)
|
|
56
|
+
* traceMessages: true, // message:* and tool:* events
|
|
57
|
+
* traceChat: true, // chat:* recovery/stream/context events
|
|
58
|
+
* traceTranscripts: true, // chat:transcript:* events
|
|
59
|
+
* traceFibers: true, // fiber:* events
|
|
60
|
+
* traceToolRecovery: true, // agent_tool:* recovery events
|
|
61
|
+
* traceWorkflow: true, // workflow:* events
|
|
62
|
+
* traceEmail: true, // email:* events
|
|
63
|
+
* traceLifecycle: true, // connect/disconnect/destroy
|
|
56
64
|
* }
|
|
57
65
|
* })
|
|
58
66
|
* ```
|
|
@@ -65,12 +73,15 @@ export {
|
|
|
65
73
|
createOtelObservabilityFromEnv,
|
|
66
74
|
OtelObservability,
|
|
67
75
|
} from './agents/otel-observability';
|
|
76
|
+
export { channels, genericObservability, subscribe } from './agents/observability';
|
|
68
77
|
export type {
|
|
78
|
+
BaseEvent,
|
|
69
79
|
OtelObservabilityConfig,
|
|
70
80
|
AgentObservabilityEvent,
|
|
71
81
|
MCPObservabilityEvent,
|
|
72
82
|
ObservabilityEvent,
|
|
73
83
|
Observability,
|
|
84
|
+
ChannelEventMap,
|
|
74
85
|
AgentInstrumentationOptions,
|
|
75
86
|
AgentSpanAttributes,
|
|
76
87
|
} from './agents/types';
|
package/src/index.ts
CHANGED
|
@@ -81,3 +81,11 @@ export {
|
|
|
81
81
|
|
|
82
82
|
// Global instrumentations
|
|
83
83
|
export { instrumentGlobalFetch, instrumentGlobalCache } from './global';
|
|
84
|
+
|
|
85
|
+
// Cloudflare native tracing helpers (auto-wired by the handler wrappers;
|
|
86
|
+
// exported for manual detection). `enterSpan` is re-exported from autotel-edge
|
|
87
|
+
// above via `export * from 'autotel-edge'`.
|
|
88
|
+
export {
|
|
89
|
+
isNativeTracingAvailable,
|
|
90
|
+
getNativeTracerFromCtx,
|
|
91
|
+
} from './native/native-tracing';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isNativeTracingAvailable,
|
|
4
|
+
getNativeTracerFromCtx,
|
|
5
|
+
} from './native-tracing';
|
|
6
|
+
|
|
7
|
+
function ctxWithTracing() {
|
|
8
|
+
const enterSpan = vi.fn((_name: string, cb: (s: any) => unknown) =>
|
|
9
|
+
cb({ isTraced: true, setAttribute: vi.fn() }),
|
|
10
|
+
);
|
|
11
|
+
return { tracing: { enterSpan }, waitUntil() {}, passThroughOnException() {} };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('isNativeTracingAvailable', () => {
|
|
15
|
+
it('is true when ctx.tracing.enterSpan is a function', () => {
|
|
16
|
+
expect(isNativeTracingAvailable(ctxWithTracing())).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('is false for a plain ExecutionContext (tracing disabled / old runtime)', () => {
|
|
20
|
+
expect(isNativeTracingAvailable({ waitUntil() {} })).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('is false for null/undefined or malformed tracing', () => {
|
|
24
|
+
expect(isNativeTracingAvailable(undefined)).toBe(false);
|
|
25
|
+
expect(isNativeTracingAvailable(null)).toBe(false);
|
|
26
|
+
expect(isNativeTracingAvailable({ tracing: {} })).toBe(false);
|
|
27
|
+
expect(isNativeTracingAvailable({ tracing: { enterSpan: 123 } })).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('getNativeTracerFromCtx', () => {
|
|
32
|
+
it('returns null when native tracing is unavailable', () => {
|
|
33
|
+
expect(getNativeTracerFromCtx({ waitUntil() {} })).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('wraps ctx.tracing.enterSpan as a NativeTracer', () => {
|
|
37
|
+
const ctx = ctxWithTracing();
|
|
38
|
+
const tracer = getNativeTracerFromCtx(ctx);
|
|
39
|
+
expect(tracer).not.toBeNull();
|
|
40
|
+
|
|
41
|
+
const result = tracer!.enterSpan('work', (span) => {
|
|
42
|
+
span.setAttribute('k', 'v');
|
|
43
|
+
return 7;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result).toBe(7);
|
|
47
|
+
expect(ctx.tracing.enterSpan).toHaveBeenCalledWith('work', expect.any(Function));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('attaches the supplied correlation id to the tracer', () => {
|
|
51
|
+
const tracer = getNativeTracerFromCtx(ctxWithTracing(), 'ray-xyz');
|
|
52
|
+
expect(tracer?.correlationId).toBe('ray-xyz');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare native tracing adapter
|
|
3
|
+
*
|
|
4
|
+
* Bridges Cloudflare Workers' built-in custom-span API
|
|
5
|
+
* (`ctx.tracing.enterSpan()` / `import { tracing } from "cloudflare:workers"`)
|
|
6
|
+
* to autotel-edge's runtime-agnostic {@link NativeTracer} seam.
|
|
7
|
+
*
|
|
8
|
+
* When a Worker has tracing enabled (`observability.traces.enabled = true` in
|
|
9
|
+
* Wrangler) the runtime exposes `ctx.tracing`. The handler wrappers detect it,
|
|
10
|
+
* wrap it as a {@link NativeTracer}, and install it into the active context with
|
|
11
|
+
* `withNativeTracer()`. From then on every autotel `span()` / `trace()` /
|
|
12
|
+
* `enterSpan()` call — including deep inside utility functions and libraries —
|
|
13
|
+
* automatically routes to Cloudflare's native tracer and nests inside the
|
|
14
|
+
* platform's trace waterfall (fetch / KV / R2 / D1 / handler spans), exported
|
|
15
|
+
* by Cloudflare to whichever destination is configured in Wrangler.
|
|
16
|
+
*
|
|
17
|
+
* No `cloudflare:workers` import is required here: the native tracer travels
|
|
18
|
+
* through autotel's AsyncLocalStorage context, so code without access to `ctx`
|
|
19
|
+
* still picks it up via `getActiveNativeTracer()`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { NativeTracer, NativeSpanHandle } from 'autotel-edge';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cloudflare's native custom-span surface. Declared locally because it is not
|
|
26
|
+
* yet present in `@cloudflare/workers-types`. Structurally compatible with
|
|
27
|
+
* autotel-edge's {@link NativeSpanHandle}.
|
|
28
|
+
*/
|
|
29
|
+
interface CloudflareSpan {
|
|
30
|
+
readonly isTraced: boolean;
|
|
31
|
+
setAttribute(key: string, value: string | number | boolean | undefined): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Cloudflare's `tracing` object, available as `ctx.tracing` on the
|
|
36
|
+
* ExecutionContext and as the `tracing` export of `cloudflare:workers`.
|
|
37
|
+
*/
|
|
38
|
+
interface CloudflareTracing {
|
|
39
|
+
enterSpan<T, A extends unknown[]>(
|
|
40
|
+
name: string,
|
|
41
|
+
callback: (span: CloudflareSpan, ...args: A) => T,
|
|
42
|
+
...args: A
|
|
43
|
+
): T;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type MaybeTracingCarrier = { tracing?: CloudflareTracing } | null | undefined;
|
|
47
|
+
|
|
48
|
+
function readTracing(carrier: unknown): CloudflareTracing | undefined {
|
|
49
|
+
const tracing = (carrier as MaybeTracingCarrier)?.tracing;
|
|
50
|
+
return typeof tracing?.enterSpan === 'function' ? tracing : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns `true` when Cloudflare native custom-span tracing is available on the
|
|
55
|
+
* given ExecutionContext (i.e. tracing is enabled for this Worker).
|
|
56
|
+
*/
|
|
57
|
+
export function isNativeTracingAvailable(ctx: unknown): boolean {
|
|
58
|
+
return readTracing(ctx) !== undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wrap Cloudflare's `ctx.tracing` as an autotel {@link NativeTracer}, or return
|
|
63
|
+
* `null` when native tracing is unavailable on this context.
|
|
64
|
+
*
|
|
65
|
+
* @param correlationId Optional per-request id (e.g. the `cf-ray` header)
|
|
66
|
+
* surfaced as `ctx.correlationId` and a `correlation.id` span attribute, so
|
|
67
|
+
* logs, custom spans, and the Cloudflare dashboard share one queryable key
|
|
68
|
+
* today — before Cloudflare exposes in-code trace/span ids.
|
|
69
|
+
*/
|
|
70
|
+
export function getNativeTracerFromCtx(
|
|
71
|
+
ctx: unknown,
|
|
72
|
+
correlationId?: string,
|
|
73
|
+
): NativeTracer | null {
|
|
74
|
+
const tracing = readTracing(ctx);
|
|
75
|
+
if (!tracing) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
correlationId,
|
|
80
|
+
enterSpan: <T>(name: string, callback: (span: NativeSpanHandle) => T): T =>
|
|
81
|
+
tracing.enterSpan(name, callback as (span: CloudflareSpan) => T),
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/native.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autotel-cloudflare/native
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare native tracing helpers. Most users never need this entry point —
|
|
5
|
+
* the handler wrappers (`instrument`, `wrapModule`, `defineWorkerFetch`,
|
|
6
|
+
* `wrapDurableObject`) auto-detect and wire up native tracing for you. Import
|
|
7
|
+
* from here only when you want to detect native tracing yourself.
|
|
8
|
+
*/
|
|
9
|
+
export {
|
|
10
|
+
isNativeTracingAvailable,
|
|
11
|
+
getNativeTracerFromCtx,
|
|
12
|
+
} from './native/native-tracing';
|
|
@@ -41,10 +41,14 @@ import {
|
|
|
41
41
|
getActiveConfig,
|
|
42
42
|
getServiceForPath,
|
|
43
43
|
shouldInstrumentPath,
|
|
44
|
+
withNativeTracer,
|
|
45
|
+
ensureGlobalContextManager,
|
|
44
46
|
type Initialiser,
|
|
47
|
+
type NativeTracer,
|
|
45
48
|
WorkerTracerProvider,
|
|
46
49
|
WorkerTracer,
|
|
47
50
|
} from 'autotel-edge';
|
|
51
|
+
import { getNativeTracerFromCtx } from '../native/native-tracing';
|
|
48
52
|
import { proxyExecutionContext, unwrap, wrap, type PromiseTracker } from '../bindings/common';
|
|
49
53
|
import { instrumentGlobalFetch } from '../global/fetch';
|
|
50
54
|
import { instrumentGlobalCache } from '../global/cache';
|
|
@@ -528,6 +532,68 @@ function createHandlerFlow<T extends Trigger, E, R>(
|
|
|
528
532
|
};
|
|
529
533
|
}
|
|
530
534
|
|
|
535
|
+
let warnedMissingNative = false;
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Resolve whether this invocation should use Cloudflare's native tracer.
|
|
539
|
+
*
|
|
540
|
+
* - `nativeTracing: 'off'` → never use native (always autotel's OTLP pipeline).
|
|
541
|
+
* - `nativeTracing: 'on'` → require native; warn once and fall back if absent.
|
|
542
|
+
* - `nativeTracing: 'auto'` (default) → use native when `ctx.tracing` exists.
|
|
543
|
+
*/
|
|
544
|
+
function resolveNativeTracer(
|
|
545
|
+
config: ResolvedEdgeConfig,
|
|
546
|
+
ctx: ExecutionContext,
|
|
547
|
+
trigger: Trigger,
|
|
548
|
+
): NativeTracer | null {
|
|
549
|
+
const mode = config.nativeTracing ?? 'auto';
|
|
550
|
+
if (mode === 'off') {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
// Correlation id: prefer Cloudflare's ray id (also stamped on the native root
|
|
554
|
+
// span + carried by the workers logger), so logs/spans/dashboard share a key.
|
|
555
|
+
// Fall back to a per-invocation uuid for non-fetch triggers.
|
|
556
|
+
const rayId = trigger instanceof Request ? trigger.headers.get('cf-ray') : null;
|
|
557
|
+
const correlationId = rayId ?? crypto.randomUUID();
|
|
558
|
+
const nativeTracer = getNativeTracerFromCtx(ctx, correlationId);
|
|
559
|
+
if (!nativeTracer && mode === 'on' && !warnedMissingNative) {
|
|
560
|
+
warnedMissingNative = true;
|
|
561
|
+
console.warn(
|
|
562
|
+
"[autotel-cloudflare] nativeTracing is 'on' but Cloudflare native tracing was not detected. " +
|
|
563
|
+
'Enable observability.traces in your Wrangler config (and use a recent compatibility_date). ' +
|
|
564
|
+
'Falling back to the autotel OTLP exporter.',
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return nativeTracer;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Run a handler in native-tracing mode.
|
|
572
|
+
*
|
|
573
|
+
* Cloudflare already creates the root handler span and instruments platform
|
|
574
|
+
* operations (fetch / KV / R2 / D1 / DO), so autotel defers entirely: no
|
|
575
|
+
* binding proxies (avoids duplicate spans), no provider/exporter, no manual
|
|
576
|
+
* flush. We install the native tracer into the active context so that the
|
|
577
|
+
* user's `span()` / `trace()` / `enterSpan()` calls nest inside Cloudflare's
|
|
578
|
+
* native waterfall and are exported by the platform.
|
|
579
|
+
*/
|
|
580
|
+
function runWithNativeTracing<T extends Trigger, E, R>(
|
|
581
|
+
handlerFn: (trigger: T, env: E, ctx: ExecutionContext) => R | Promise<R>,
|
|
582
|
+
trigger: T,
|
|
583
|
+
env: E,
|
|
584
|
+
ctx: ExecutionContext,
|
|
585
|
+
config: ResolvedEdgeConfig,
|
|
586
|
+
nativeTracer: NativeTracer,
|
|
587
|
+
): R | Promise<R> {
|
|
588
|
+
// Register the context manager (we skip initProvider in native mode) so the
|
|
589
|
+
// native tracer propagates through OTel context into span()/trace() — without
|
|
590
|
+
// it, getActiveNativeTracer() returns null and custom spans never route to
|
|
591
|
+
// enterSpan. Bindings are NOT proxied: Cloudflare instruments them natively.
|
|
592
|
+
ensureGlobalContextManager();
|
|
593
|
+
const nativeContext = withNativeTracer(nativeTracer, setConfig(config));
|
|
594
|
+
return api_context.with(nativeContext, () => handlerFn(trigger, env, ctx));
|
|
595
|
+
}
|
|
596
|
+
|
|
531
597
|
/**
|
|
532
598
|
* Create handler proxy
|
|
533
599
|
*/
|
|
@@ -539,16 +605,22 @@ function createHandlerProxy<T extends Trigger, E, R>(
|
|
|
539
605
|
): (trigger: T, env: E, ctx: ExecutionContext) => ReturnType<typeof handlerFn> {
|
|
540
606
|
return (trigger: T, env: E, ctx: ExecutionContext) => {
|
|
541
607
|
const config = initialiser(env, trigger);
|
|
542
|
-
|
|
608
|
+
|
|
543
609
|
// Check if instrumentation is disabled (useful for local dev)
|
|
544
610
|
if (config.instrumentation.disabled) {
|
|
545
611
|
// Return handler as-is without instrumentation
|
|
546
612
|
return handlerFn(trigger, env, ctx);
|
|
547
613
|
}
|
|
548
|
-
|
|
614
|
+
|
|
615
|
+
// Prefer Cloudflare native tracing when available — defer to the platform.
|
|
616
|
+
const nativeTracer = resolveNativeTracer(config, ctx, trigger);
|
|
617
|
+
if (nativeTracer) {
|
|
618
|
+
return runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer);
|
|
619
|
+
}
|
|
620
|
+
|
|
549
621
|
// Auto-instrument Cloudflare bindings in the environment
|
|
550
622
|
const instrumentedEnv = instrumentBindings(env as Record<string, any>) as E;
|
|
551
|
-
|
|
623
|
+
|
|
552
624
|
const configContext = setConfig(config);
|
|
553
625
|
|
|
554
626
|
// Initialize provider on first call
|
|
@@ -593,10 +665,16 @@ function createHandlerProxyWithConfig<T extends Trigger, E, R>(
|
|
|
593
665
|
return handlerFn(trigger, env, ctx) as ReturnType<typeof handlerFn>;
|
|
594
666
|
}
|
|
595
667
|
}
|
|
596
|
-
|
|
668
|
+
|
|
669
|
+
// Prefer Cloudflare native tracing when available — defer to the platform.
|
|
670
|
+
const nativeTracer = resolveNativeTracer(config, ctx, trigger);
|
|
671
|
+
if (nativeTracer) {
|
|
672
|
+
return runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer) as ReturnType<typeof handlerFn>;
|
|
673
|
+
}
|
|
674
|
+
|
|
597
675
|
// Auto-instrument Cloudflare bindings in the environment
|
|
598
676
|
const instrumentedEnv = instrumentBindings(env as Record<string, any>) as E;
|
|
599
|
-
|
|
677
|
+
|
|
600
678
|
const configContext = setConfig(config);
|
|
601
679
|
|
|
602
680
|
// Initialize provider on first call
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { instrument } from './instrument';
|
|
3
|
+
import { span } from 'autotel-edge';
|
|
4
|
+
import { isWrapped } from '../bindings/common';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A KV-shaped binding so instrumentBindings() would proxy it in the OTLP path.
|
|
8
|
+
* In native mode we must NOT proxy it (Cloudflare traces KV natively) — these
|
|
9
|
+
* tests assert that the handler receives the original, unwrapped binding and
|
|
10
|
+
* that user spans route to ctx.tracing.enterSpan instead of the OTel pipeline.
|
|
11
|
+
*/
|
|
12
|
+
function fakeKv() {
|
|
13
|
+
return {
|
|
14
|
+
get: vi.fn(async () => 'value'),
|
|
15
|
+
put: vi.fn(async () => {}),
|
|
16
|
+
delete: vi.fn(async () => {}),
|
|
17
|
+
list: vi.fn(async () => ({ keys: [] })),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function nativeCtx() {
|
|
22
|
+
const enteredSpans: string[] = [];
|
|
23
|
+
const waitUntil = vi.fn();
|
|
24
|
+
const ctx = {
|
|
25
|
+
waitUntil,
|
|
26
|
+
passThroughOnException() {},
|
|
27
|
+
tracing: {
|
|
28
|
+
enterSpan: vi.fn((name: string, cb: (s: any) => unknown) => {
|
|
29
|
+
enteredSpans.push(name);
|
|
30
|
+
return cb({ isTraced: true, setAttribute: vi.fn() });
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
return { ctx, enteredSpans, waitUntil };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const baseConfig = {
|
|
38
|
+
service: { name: 'native-test' },
|
|
39
|
+
exporter: { url: 'http://localhost:4318/v1/traces' },
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
describe('instrument() with Cloudflare native tracing (auto)', () => {
|
|
43
|
+
it('defers binding instrumentation to the platform (no proxy wrapping)', async () => {
|
|
44
|
+
const { ctx } = nativeCtx();
|
|
45
|
+
const kv = fakeKv();
|
|
46
|
+
let seenBinding: unknown;
|
|
47
|
+
|
|
48
|
+
const handler = instrument(
|
|
49
|
+
{
|
|
50
|
+
async fetch(_req, env: { MY_KV: ReturnType<typeof fakeKv> }) {
|
|
51
|
+
seenBinding = env.MY_KV;
|
|
52
|
+
await env.MY_KV.get('k');
|
|
53
|
+
return new Response('ok');
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
baseConfig,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
await handler.fetch!(new Request('https://x/'), { MY_KV: kv }, ctx as never);
|
|
60
|
+
|
|
61
|
+
// Handler received the ORIGINAL binding, not an autotel proxy.
|
|
62
|
+
expect(seenBinding).toBe(kv);
|
|
63
|
+
expect(isWrapped(seenBinding)).toBe(false);
|
|
64
|
+
// No "KV ...: get" span was created via the native tracer.
|
|
65
|
+
expect(
|
|
66
|
+
ctx.tracing.enterSpan.mock.calls.some(([name]) =>
|
|
67
|
+
String(name).startsWith('KV '),
|
|
68
|
+
),
|
|
69
|
+
).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('surfaces the cf-ray header as a correlation.id span attribute', async () => {
|
|
73
|
+
const attrs: Record<string, unknown> = {};
|
|
74
|
+
const ctx = {
|
|
75
|
+
waitUntil() {},
|
|
76
|
+
passThroughOnException() {},
|
|
77
|
+
tracing: {
|
|
78
|
+
enterSpan: vi.fn((_name: string, cb: (s: any) => unknown) =>
|
|
79
|
+
cb({
|
|
80
|
+
isTraced: true,
|
|
81
|
+
setAttribute(k: string, v: unknown) {
|
|
82
|
+
attrs[k] = v;
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handler = instrument(
|
|
90
|
+
{ async fetch() { return span('work', () => new Response('ok')); } },
|
|
91
|
+
baseConfig,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const req = new Request('https://x/', { headers: { 'cf-ray': 'ray-7f' } });
|
|
95
|
+
await handler.fetch!(req, {}, ctx as never);
|
|
96
|
+
expect(attrs['correlation.id']).toBe('ray-7f');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('routes user span() calls to ctx.tracing.enterSpan', async () => {
|
|
100
|
+
const { ctx, enteredSpans } = nativeCtx();
|
|
101
|
+
|
|
102
|
+
const handler = instrument(
|
|
103
|
+
{
|
|
104
|
+
async fetch() {
|
|
105
|
+
return span('user.work', (s) => {
|
|
106
|
+
s.setAttribute('ok', true);
|
|
107
|
+
return new Response('done');
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
baseConfig,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const res = await handler.fetch!(new Request('https://x/'), {}, ctx as never);
|
|
115
|
+
expect(await res.text()).toBe('done');
|
|
116
|
+
expect(enteredSpans).toContain('user.work');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('does not run the OTLP export flow (no waitUntil) in native mode', async () => {
|
|
120
|
+
const { ctx, waitUntil } = nativeCtx();
|
|
121
|
+
const handler = instrument(
|
|
122
|
+
{ async fetch() { return new Response('ok'); } },
|
|
123
|
+
baseConfig,
|
|
124
|
+
);
|
|
125
|
+
await handler.fetch!(new Request('https://x/'), {}, ctx as never);
|
|
126
|
+
expect(waitUntil).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('instrument() with nativeTracing: "off"', () => {
|
|
131
|
+
it('ignores ctx.tracing and uses the autotel pipeline (proxies bindings)', async () => {
|
|
132
|
+
const { ctx } = nativeCtx();
|
|
133
|
+
const kv = fakeKv();
|
|
134
|
+
let seenBinding: unknown;
|
|
135
|
+
|
|
136
|
+
const handler = instrument(
|
|
137
|
+
{
|
|
138
|
+
async fetch(_req, env: { MY_KV: ReturnType<typeof fakeKv> }) {
|
|
139
|
+
seenBinding = env.MY_KV;
|
|
140
|
+
return new Response('ok');
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{ ...baseConfig, nativeTracing: 'off' },
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
await handler.fetch!(new Request('https://x/'), { MY_KV: kv }, ctx as never);
|
|
147
|
+
|
|
148
|
+
// OTLP path proxies the binding, so the handler sees a wrapped object.
|
|
149
|
+
expect(isWrapped(seenBinding)).toBe(true);
|
|
150
|
+
// Native tracer was never used.
|
|
151
|
+
expect(ctx.tracing.enterSpan).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
});
|