autotel-cloudflare 3.0.0 → 4.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/dist/agents.d.ts +626 -123
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +260 -171
- package/dist/agents.js.map +1 -1
- package/package.json +8 -4
- package/src/actors/alarms.ts +0 -225
- package/src/actors/index.ts +0 -36
- package/src/actors/instrument-actor.test.ts +0 -179
- package/src/actors/instrument-actor.ts +0 -574
- package/src/actors/sockets.ts +0 -217
- package/src/actors/storage.ts +0 -263
- package/src/actors/traced-handler.ts +0 -300
- package/src/actors/types.ts +0 -98
- package/src/actors.ts +0 -50
- package/src/agents/index.ts +0 -42
- package/src/agents/otel-observability.test.ts +0 -336
- package/src/agents/otel-observability.ts +0 -474
- package/src/agents/types.ts +0 -167
- package/src/agents.ts +0 -76
- package/src/bindings/ai.test.ts +0 -170
- package/src/bindings/ai.ts +0 -73
- package/src/bindings/analytics-engine.test.ts +0 -175
- package/src/bindings/analytics-engine.ts +0 -78
- package/src/bindings/bindings-cache.test.ts +0 -80
- package/src/bindings/bindings-detection.test.ts +0 -235
- package/src/bindings/bindings-this-binding.test.ts +0 -294
- package/src/bindings/bindings.ts +0 -720
- package/src/bindings/browser-rendering.test.ts +0 -160
- package/src/bindings/browser-rendering.ts +0 -70
- package/src/bindings/common.ts +0 -84
- package/src/bindings/hyperdrive.test.ts +0 -176
- package/src/bindings/hyperdrive.ts +0 -74
- package/src/bindings/images.test.ts +0 -262
- package/src/bindings/images.ts +0 -182
- package/src/bindings/index.ts +0 -20
- package/src/bindings/queue-producer.test.ts +0 -224
- package/src/bindings/queue-producer.ts +0 -105
- package/src/bindings/rate-limiter.test.ts +0 -140
- package/src/bindings/rate-limiter.ts +0 -69
- package/src/bindings/vectorize.test.ts +0 -362
- package/src/bindings/vectorize.ts +0 -86
- package/src/bindings.ts +0 -6
- package/src/events.ts +0 -6
- package/src/execution-logger.test.ts +0 -127
- package/src/execution-logger.ts +0 -112
- package/src/global/cache.test.ts +0 -292
- package/src/global/cache.ts +0 -164
- package/src/global/fetch.test.ts +0 -399
- package/src/global/fetch.ts +0 -136
- package/src/global/index.ts +0 -7
- package/src/handlers/durable-objects.test.ts +0 -524
- package/src/handlers/durable-objects.ts +0 -250
- package/src/handlers/index.ts +0 -6
- package/src/handlers/workflows.test.ts +0 -411
- package/src/handlers/workflows.ts +0 -345
- package/src/handlers.ts +0 -6
- package/src/index.ts +0 -91
- package/src/logger.ts +0 -15
- package/src/native/native-tracing.test.ts +0 -54
- package/src/native/native-tracing.ts +0 -83
- package/src/native.ts +0 -12
- package/src/parse-error.ts +0 -1
- package/src/sampling.ts +0 -6
- package/src/testing.ts +0 -6
- package/src/wrappers/cf-attributes.test.ts +0 -334
- package/src/wrappers/define-worker-fetch.test.ts +0 -102
- package/src/wrappers/define-worker-fetch.ts +0 -71
- package/src/wrappers/index.ts +0 -13
- package/src/wrappers/instrument.integration.test.ts +0 -578
- package/src/wrappers/instrument.ts +0 -806
- package/src/wrappers/native-instrument.test.ts +0 -153
- package/src/wrappers/wrap-do.ts +0 -34
- package/src/wrappers/wrap-module.ts +0 -37
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Durable Objects instrumentation for Cloudflare Workers
|
|
3
|
-
*
|
|
4
|
-
* Note: This file uses Cloudflare Workers types (DurableObjectId, DurableObjectState, etc.)
|
|
5
|
-
* which are globally available via @cloudflare/workers-types when listed in tsconfig.json.
|
|
6
|
-
* These types are devDependencies only - they're not runtime dependencies.
|
|
7
|
-
* At runtime, Cloudflare Workers runtime provides the actual implementations.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
trace,
|
|
12
|
-
context as api_context,
|
|
13
|
-
propagation,
|
|
14
|
-
SpanStatusCode,
|
|
15
|
-
SpanKind,
|
|
16
|
-
} from '@opentelemetry/api';
|
|
17
|
-
import type { ConfigurationOption } from 'autotel-edge';
|
|
18
|
-
import { createInitialiser, setConfig, WorkerTracer } from 'autotel-edge';
|
|
19
|
-
import { wrap } from '../bindings/common';
|
|
20
|
-
|
|
21
|
-
// Durable Object types
|
|
22
|
-
type DOFetchFn = (request: Request) => Response | Promise<Response>;
|
|
23
|
-
type DOAlarmFn = () => void | Promise<void>;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Track cold starts per DO class
|
|
27
|
-
*/
|
|
28
|
-
const coldStarts = new WeakMap<any, boolean>();
|
|
29
|
-
|
|
30
|
-
function isColdStart(doClass: any): boolean {
|
|
31
|
-
if (!coldStarts.has(doClass)) {
|
|
32
|
-
coldStarts.set(doClass, true);
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Instrument a Durable Object fetch method
|
|
40
|
-
*/
|
|
41
|
-
function instrumentDOFetch(
|
|
42
|
-
fetchFn: DOFetchFn,
|
|
43
|
-
id: DurableObjectId,
|
|
44
|
-
doClass: any,
|
|
45
|
-
): DOFetchFn {
|
|
46
|
-
return async function instrumentedFetch(
|
|
47
|
-
this: any,
|
|
48
|
-
request: Request,
|
|
49
|
-
): Promise<Response> {
|
|
50
|
-
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
51
|
-
|
|
52
|
-
// Extract parent context from request headers
|
|
53
|
-
const parentContext = propagation.extract(
|
|
54
|
-
api_context.active(),
|
|
55
|
-
request.headers,
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const url = new URL(request.url);
|
|
59
|
-
const spanName = `DO ${id.name || id.toString()}: ${request.method} ${url.pathname}`;
|
|
60
|
-
|
|
61
|
-
return tracer.startActiveSpan(
|
|
62
|
-
spanName,
|
|
63
|
-
{
|
|
64
|
-
kind: SpanKind.SERVER,
|
|
65
|
-
attributes: {
|
|
66
|
-
'http.request.method': request.method,
|
|
67
|
-
'url.full': request.url,
|
|
68
|
-
'do.id': id.toString(),
|
|
69
|
-
'do.id.name': id.name || '',
|
|
70
|
-
'faas.trigger': 'http',
|
|
71
|
-
'faas.coldstart': isColdStart(doClass),
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
parentContext,
|
|
75
|
-
async (span) => {
|
|
76
|
-
try {
|
|
77
|
-
const response = await fetchFn.call(this, request);
|
|
78
|
-
|
|
79
|
-
span.setAttributes({
|
|
80
|
-
'http.response.status_code': response.status,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
if (response.ok) {
|
|
84
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
85
|
-
} else {
|
|
86
|
-
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return response;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
span.recordException(error as Error);
|
|
92
|
-
span.setStatus({
|
|
93
|
-
code: SpanStatusCode.ERROR,
|
|
94
|
-
message: error instanceof Error ? error.message : String(error),
|
|
95
|
-
});
|
|
96
|
-
throw error;
|
|
97
|
-
} finally {
|
|
98
|
-
span.end();
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
);
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Instrument a Durable Object alarm method
|
|
107
|
-
*/
|
|
108
|
-
function instrumentDOAlarm(
|
|
109
|
-
alarmFn: DOAlarmFn,
|
|
110
|
-
id: DurableObjectId,
|
|
111
|
-
doClass: any,
|
|
112
|
-
): DOAlarmFn {
|
|
113
|
-
return async function instrumentedAlarm(this: any): Promise<void> {
|
|
114
|
-
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
115
|
-
|
|
116
|
-
const spanName = `DO ${id.name || id.toString()}: alarm`;
|
|
117
|
-
|
|
118
|
-
return tracer.startActiveSpan(
|
|
119
|
-
spanName,
|
|
120
|
-
{
|
|
121
|
-
kind: SpanKind.INTERNAL,
|
|
122
|
-
attributes: {
|
|
123
|
-
'do.id': id.toString(),
|
|
124
|
-
'do.id.name': id.name || '',
|
|
125
|
-
'faas.trigger': 'timer',
|
|
126
|
-
'faas.coldstart': isColdStart(doClass),
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
async (span) => {
|
|
130
|
-
try {
|
|
131
|
-
await alarmFn.call(this);
|
|
132
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
133
|
-
} catch (error) {
|
|
134
|
-
span.recordException(error as Error);
|
|
135
|
-
span.setStatus({
|
|
136
|
-
code: SpanStatusCode.ERROR,
|
|
137
|
-
message: error instanceof Error ? error.message : String(error),
|
|
138
|
-
});
|
|
139
|
-
throw error;
|
|
140
|
-
} finally {
|
|
141
|
-
span.end();
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
);
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Instrument a Durable Object instance
|
|
150
|
-
*/
|
|
151
|
-
function instrumentDOInstance(
|
|
152
|
-
doInstance: any,
|
|
153
|
-
state: DurableObjectState,
|
|
154
|
-
_env: any,
|
|
155
|
-
doClass: any,
|
|
156
|
-
): any {
|
|
157
|
-
const instanceHandler: ProxyHandler<any> = {
|
|
158
|
-
get(target, prop) {
|
|
159
|
-
const value = Reflect.get(target, prop);
|
|
160
|
-
|
|
161
|
-
if (prop === 'fetch' && typeof value === 'function') {
|
|
162
|
-
return instrumentDOFetch(value.bind(target), state.id, doClass);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (prop === 'alarm' && typeof value === 'function') {
|
|
166
|
-
return instrumentDOAlarm(value.bind(target), state.id, doClass);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Bind other methods to the target
|
|
170
|
-
if (typeof value === 'function') {
|
|
171
|
-
return value.bind(target);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return value;
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
return wrap(doInstance, instanceHandler);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Instrument a Durable Object class
|
|
183
|
-
*
|
|
184
|
-
* This wraps the DO class to automatically trace all fetch and alarm calls,
|
|
185
|
-
* as well as initialize the telemetry configuration.
|
|
186
|
-
*
|
|
187
|
-
* **Usage:**
|
|
188
|
-
* ```typescript
|
|
189
|
-
* import { DurableObject } from 'cloudflare:workers'
|
|
190
|
-
* import { instrumentDO } from 'autotel-edge'
|
|
191
|
-
*
|
|
192
|
-
* export class Counter extends DurableObject<Env> {
|
|
193
|
-
* async fetch(request: Request) {
|
|
194
|
-
* // Your DO logic here
|
|
195
|
-
* return new Response('OK')
|
|
196
|
-
* }
|
|
197
|
-
* }
|
|
198
|
-
*
|
|
199
|
-
* // Wrap the class before exporting
|
|
200
|
-
* export const CounterDO = instrumentDO(Counter, (env: Env) => ({
|
|
201
|
-
* exporter: {
|
|
202
|
-
* url: env.OTLP_ENDPOINT,
|
|
203
|
-
* headers: { 'x-api-key': env.API_KEY }
|
|
204
|
-
* },
|
|
205
|
-
* service: {
|
|
206
|
-
* name: 'my-durable-object',
|
|
207
|
-
* version: '1.0.0'
|
|
208
|
-
* }
|
|
209
|
-
* }))
|
|
210
|
-
* ```
|
|
211
|
-
*
|
|
212
|
-
* **What you get:**
|
|
213
|
-
* - 🎯 Automatic spans for fetch() calls with HTTP attributes
|
|
214
|
-
* - ⏰ Automatic spans for alarm() calls
|
|
215
|
-
* - 🥶 Cold start tracking
|
|
216
|
-
* - 🔗 Context propagation from incoming requests
|
|
217
|
-
* - ⚡ Automatic span lifecycle management
|
|
218
|
-
*
|
|
219
|
-
* @param doClass - The Durable Object class to instrument
|
|
220
|
-
* @param config - Configuration or configuration function
|
|
221
|
-
* @returns Instrumented Durable Object class
|
|
222
|
-
*/
|
|
223
|
-
export function instrumentDO<C extends new (state: DurableObjectState, env: any) => any>(
|
|
224
|
-
doClass: C,
|
|
225
|
-
config: ConfigurationOption,
|
|
226
|
-
): C {
|
|
227
|
-
const initialiser = createInitialiser(config);
|
|
228
|
-
|
|
229
|
-
const classHandler: ProxyHandler<C> = {
|
|
230
|
-
construct(target, [state, env]: [DurableObjectState, any]) {
|
|
231
|
-
// Initialize config for this DO instance
|
|
232
|
-
const trigger = {
|
|
233
|
-
id: state.id.toString(),
|
|
234
|
-
name: state.id.name,
|
|
235
|
-
};
|
|
236
|
-
const doConfig = initialiser(env, trigger);
|
|
237
|
-
const context = setConfig(doConfig);
|
|
238
|
-
|
|
239
|
-
// Create the DO instance within the config context
|
|
240
|
-
const doInstance = api_context.with(context, () => {
|
|
241
|
-
return new target(state, env);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Instrument the instance
|
|
245
|
-
return instrumentDOInstance(doInstance, state, env, doClass);
|
|
246
|
-
},
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
return wrap(doClass, classHandler);
|
|
250
|
-
}
|
package/src/handlers/index.ts
DELETED
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { instrumentWorkflow } from './workflows';
|
|
3
|
-
import { getWorkflowLogger } from '../execution-logger';
|
|
4
|
-
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
|
|
5
|
-
|
|
6
|
-
describe('Workflow Instrumentation', () => {
|
|
7
|
-
let mockTracer: any;
|
|
8
|
-
let mockSpan: any;
|
|
9
|
-
let getTracerSpy: any;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
mockSpan = {
|
|
13
|
-
spanContext: () => ({
|
|
14
|
-
traceId: 'test-trace-id',
|
|
15
|
-
spanId: 'test-span-id',
|
|
16
|
-
traceFlags: 1,
|
|
17
|
-
}),
|
|
18
|
-
setAttribute: vi.fn(),
|
|
19
|
-
setAttributes: vi.fn(),
|
|
20
|
-
setStatus: vi.fn(),
|
|
21
|
-
recordException: vi.fn(),
|
|
22
|
-
end: vi.fn(),
|
|
23
|
-
isRecording: () => true,
|
|
24
|
-
updateName: vi.fn(),
|
|
25
|
-
addEvent: vi.fn(),
|
|
26
|
-
addLink: vi.fn(),
|
|
27
|
-
addLinks: vi.fn(),
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
mockTracer = {
|
|
31
|
-
startActiveSpan: vi.fn((name, options, context, fn) => {
|
|
32
|
-
if (typeof options === 'function') return options(mockSpan);
|
|
33
|
-
if (typeof context === 'function') return context(mockSpan);
|
|
34
|
-
if (typeof fn === 'function') return fn(mockSpan);
|
|
35
|
-
return Promise.resolve();
|
|
36
|
-
}),
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
getTracerSpy.mockRestore();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('instrumentWorkflow()', () => {
|
|
47
|
-
it('should wrap workflow class constructor', () => {
|
|
48
|
-
class TestWorkflow {
|
|
49
|
-
constructor(public ctx: any, public env: any) {}
|
|
50
|
-
async run() {}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
|
|
54
|
-
service: { name: 'test' },
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
expect(Instrumented).toBeDefined();
|
|
58
|
-
expect(typeof Instrumented).toBe('function');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should create workflow instance with instrumented run()', () => {
|
|
62
|
-
class TestWorkflow {
|
|
63
|
-
constructor(public ctx: any, public env: any) {}
|
|
64
|
-
async run() { return 'done'; }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
|
|
68
|
-
service: { name: 'test' },
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const instance = new Instrumented({}, {});
|
|
72
|
-
|
|
73
|
-
expect(instance).toBeDefined();
|
|
74
|
-
expect(typeof instance.run).toBe('function');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should accept static config', () => {
|
|
78
|
-
class TestWorkflow {
|
|
79
|
-
constructor(public ctx: any, public env: any) {}
|
|
80
|
-
async run() {}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
|
|
84
|
-
service: { name: 'test', version: '1.0.0' },
|
|
85
|
-
exporter: { url: 'http://localhost:4318/v1/traces' },
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
expect(Instrumented).toBeDefined();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should accept config function', () => {
|
|
92
|
-
class TestWorkflow {
|
|
93
|
-
constructor(public ctx: any, public env: any) {}
|
|
94
|
-
async run() {}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
interface Env {
|
|
98
|
-
OTLP_ENDPOINT: string;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', (env: Env) => ({
|
|
102
|
-
service: { name: 'test' },
|
|
103
|
-
exporter: { url: env.OTLP_ENDPOINT },
|
|
104
|
-
}));
|
|
105
|
-
|
|
106
|
-
expect(Instrumented).toBeDefined();
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe('run() instrumentation', () => {
|
|
111
|
-
it('should preserve run() return value', async () => {
|
|
112
|
-
const output = { status: 'ok', orderId: 'ord-123' };
|
|
113
|
-
|
|
114
|
-
class TestWorkflow {
|
|
115
|
-
constructor(public ctx: any, public env: any) {}
|
|
116
|
-
async run(event: any, step: any) {
|
|
117
|
-
return output;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'return-test', {
|
|
122
|
-
service: { name: 'test' },
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const instance = new Instrumented({}, {});
|
|
126
|
-
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-return' };
|
|
127
|
-
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
128
|
-
|
|
129
|
-
await expect(instance.run(event, step)).resolves.toEqual(output);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should create span for run() with workflow attributes', async () => {
|
|
133
|
-
class TestWorkflow {
|
|
134
|
-
constructor(public ctx: any, public env: any) {}
|
|
135
|
-
async run(event: any, step: any) {}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
|
|
139
|
-
service: { name: 'test' },
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const instance = new Instrumented({}, {});
|
|
143
|
-
const event = { payload: { foo: 'bar' }, timestamp: new Date(), instanceId: 'wf-123' };
|
|
144
|
-
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
145
|
-
|
|
146
|
-
await instance.run(event, step);
|
|
147
|
-
|
|
148
|
-
expect(mockTracer.startActiveSpan).toHaveBeenCalled();
|
|
149
|
-
|
|
150
|
-
const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
|
|
151
|
-
expect(spanName).toBe('Workflow test-workflow: run');
|
|
152
|
-
|
|
153
|
-
const options = mockTracer.startActiveSpan.mock.calls[0][1];
|
|
154
|
-
expect(options.kind).toBe(SpanKind.INTERNAL);
|
|
155
|
-
expect(options.attributes['workflow.name']).toBe('test-workflow');
|
|
156
|
-
expect(options.attributes['workflow.instance_id']).toBe('wf-123');
|
|
157
|
-
expect(options.attributes['faas.trigger']).toBe('workflow');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should track cold starts', async () => {
|
|
161
|
-
class TestWorkflow {
|
|
162
|
-
constructor(public ctx: any, public env: any) {}
|
|
163
|
-
async run(event: any, step: any) {}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'cold-test', {
|
|
167
|
-
service: { name: 'test' },
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-1' };
|
|
171
|
-
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
172
|
-
|
|
173
|
-
const instance1 = new Instrumented({}, {});
|
|
174
|
-
await instance1.run(event, step);
|
|
175
|
-
const firstOptions = mockTracer.startActiveSpan.mock.calls[0][1];
|
|
176
|
-
expect(firstOptions.attributes['faas.coldstart']).toBe(true);
|
|
177
|
-
|
|
178
|
-
const instance2 = new Instrumented({}, {});
|
|
179
|
-
await instance2.run(event, step);
|
|
180
|
-
const secondOptions = mockTracer.startActiveSpan.mock.calls[1][1];
|
|
181
|
-
expect(secondOptions.attributes['faas.coldstart']).toBe(false);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should handle run() errors', async () => {
|
|
185
|
-
class TestWorkflow {
|
|
186
|
-
constructor(public ctx: any, public env: any) {}
|
|
187
|
-
async run() {
|
|
188
|
-
throw new Error('Workflow failed');
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'error-test', {
|
|
193
|
-
service: { name: 'test' },
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
const instance = new Instrumented({}, {});
|
|
197
|
-
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-err' };
|
|
198
|
-
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
199
|
-
|
|
200
|
-
await expect(instance.run(event, step)).rejects.toThrow('Workflow failed');
|
|
201
|
-
|
|
202
|
-
expect(mockSpan.recordException).toHaveBeenCalled();
|
|
203
|
-
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
204
|
-
code: SpanStatusCode.ERROR,
|
|
205
|
-
message: 'Workflow failed',
|
|
206
|
-
});
|
|
207
|
-
expect(mockSpan.end).toHaveBeenCalled();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('should make getWorkflowLogger() available inside run()', async () => {
|
|
211
|
-
const getActiveSpanSpy = vi
|
|
212
|
-
.spyOn(trace, 'getActiveSpan')
|
|
213
|
-
.mockReturnValue(mockSpan as any);
|
|
214
|
-
|
|
215
|
-
class TestWorkflow {
|
|
216
|
-
constructor(public ctx: any, public env: any) {}
|
|
217
|
-
async run() {
|
|
218
|
-
const log = getWorkflowLogger();
|
|
219
|
-
log.set({ workflow: { id: 'wf-logger' } });
|
|
220
|
-
log.info('workflow started', { provider: { name: 'curvepay' } });
|
|
221
|
-
return log.emitNow({ outcome: 'running' });
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'logger-test', {
|
|
226
|
-
service: { name: 'test' },
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const instance = new Instrumented({}, {});
|
|
230
|
-
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-logger' };
|
|
231
|
-
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
232
|
-
|
|
233
|
-
const snapshot = await instance.run(event, step);
|
|
234
|
-
|
|
235
|
-
expect(snapshot).toMatchObject({
|
|
236
|
-
traceId: 'test-trace-id',
|
|
237
|
-
spanId: 'test-span-id',
|
|
238
|
-
context: {
|
|
239
|
-
workflow: { id: 'wf-logger' },
|
|
240
|
-
outcome: 'running',
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
expect(mockSpan.setAttributes).toHaveBeenCalledWith({
|
|
244
|
-
'workflow.id': 'wf-logger',
|
|
245
|
-
});
|
|
246
|
-
expect(mockSpan.addEvent).toHaveBeenCalledWith('log.info', {
|
|
247
|
-
message: 'workflow started',
|
|
248
|
-
'provider.name': 'curvepay',
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
getActiveSpanSpy.mockRestore();
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe('step.do() instrumentation', () => {
|
|
256
|
-
it('should create span for step.do() calls', async () => {
|
|
257
|
-
const stepDoResult = { paymentId: 'pay_123' };
|
|
258
|
-
const mockStepDo = vi.fn().mockResolvedValue(stepDoResult);
|
|
259
|
-
|
|
260
|
-
class TestWorkflow {
|
|
261
|
-
constructor(public ctx: any, public env: any) {}
|
|
262
|
-
async run(event: any, step: any) {
|
|
263
|
-
return await step.do('process payment', async () => stepDoResult);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'step-test', {
|
|
268
|
-
service: { name: 'test' },
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const instance = new Instrumented({}, {});
|
|
272
|
-
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-step' };
|
|
273
|
-
const step = {
|
|
274
|
-
do: vi.fn(async (_name: string, cb: () => Promise<any>) => cb()),
|
|
275
|
-
sleep: vi.fn(),
|
|
276
|
-
sleepUntil: vi.fn(),
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
await instance.run(event, step);
|
|
280
|
-
|
|
281
|
-
// Should have spans for both run() and step.do()
|
|
282
|
-
expect(mockTracer.startActiveSpan.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
283
|
-
|
|
284
|
-
const stepSpanCall = mockTracer.startActiveSpan.mock.calls[1];
|
|
285
|
-
expect(stepSpanCall[0]).toBe('Workflow step-test: process payment');
|
|
286
|
-
expect(stepSpanCall[1].attributes['workflow.step.name']).toBe('process payment');
|
|
287
|
-
expect(stepSpanCall[1].attributes['workflow.name']).toBe('step-test');
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('should handle step.do() errors', async () => {
|
|
291
|
-
class TestWorkflow {
|
|
292
|
-
constructor(public ctx: any, public env: any) {}
|
|
293
|
-
async run(event: any, step: any) {
|
|
294
|
-
await step.do('failing step', async () => {
|
|
295
|
-
throw new Error('Step failed');
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'step-err', {
|
|
301
|
-
service: { name: 'test' },
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
const instance = new Instrumented({}, {});
|
|
305
|
-
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-step-err' };
|
|
306
|
-
const step = {
|
|
307
|
-
do: vi.fn(async (_name: string, cb: () => Promise<any>) => cb()),
|
|
308
|
-
sleep: vi.fn(),
|
|
309
|
-
sleepUntil: vi.fn(),
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
await expect(instance.run(event, step)).rejects.toThrow('Step failed');
|
|
313
|
-
|
|
314
|
-
expect(mockSpan.recordException).toHaveBeenCalled();
|
|
315
|
-
expect(mockSpan.end).toHaveBeenCalled();
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
describe('step.sleep() instrumentation', () => {
|
|
320
|
-
it('should create span for step.sleep() calls', async () => {
|
|
321
|
-
class TestWorkflow {
|
|
322
|
-
constructor(public ctx: any, public env: any) {}
|
|
323
|
-
async run(event: any, step: any) {
|
|
324
|
-
await step.sleep('wait for settlement', '2 hours');
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'sleep-test', {
|
|
329
|
-
service: { name: 'test' },
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
const instance = new Instrumented({}, {});
|
|
333
|
-
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-sleep' };
|
|
334
|
-
const step = {
|
|
335
|
-
do: vi.fn(),
|
|
336
|
-
sleep: vi.fn().mockResolvedValue(undefined),
|
|
337
|
-
sleepUntil: vi.fn(),
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
await instance.run(event, step);
|
|
341
|
-
|
|
342
|
-
const sleepSpanCall = mockTracer.startActiveSpan.mock.calls[1];
|
|
343
|
-
expect(sleepSpanCall[0]).toBe('Workflow sleep-test: sleep wait for settlement');
|
|
344
|
-
expect(sleepSpanCall[1].attributes['workflow.sleep.name']).toBe('wait for settlement');
|
|
345
|
-
expect(sleepSpanCall[1].attributes['workflow.sleep.duration']).toBe('2 hours');
|
|
346
|
-
expect(sleepSpanCall[1].attributes['workflow.name']).toBe('sleep-test');
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it('should create span for step.sleepUntil() calls', async () => {
|
|
350
|
-
const wakeAt = new Date('2026-04-09T10:00:00.000Z');
|
|
351
|
-
|
|
352
|
-
class TestWorkflow {
|
|
353
|
-
constructor(public ctx: any, public env: any) {}
|
|
354
|
-
async run(event: any, step: any) {
|
|
355
|
-
await step.sleepUntil('wait until tomorrow', wakeAt);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'sleep-until-test', {
|
|
360
|
-
service: { name: 'test' },
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
const instance = new Instrumented({}, {});
|
|
364
|
-
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-sleep-until' };
|
|
365
|
-
const step = {
|
|
366
|
-
do: vi.fn(),
|
|
367
|
-
sleep: vi.fn(),
|
|
368
|
-
sleepUntil: vi.fn().mockResolvedValue(undefined),
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
await instance.run(event, step);
|
|
372
|
-
|
|
373
|
-
const sleepUntilSpanCall = mockTracer.startActiveSpan.mock.calls[1];
|
|
374
|
-
expect(sleepUntilSpanCall[0]).toBe(
|
|
375
|
-
'Workflow sleep-until-test: sleepUntil wait until tomorrow',
|
|
376
|
-
);
|
|
377
|
-
expect(sleepUntilSpanCall[1].attributes['workflow.sleep.name']).toBe(
|
|
378
|
-
'wait until tomorrow',
|
|
379
|
-
);
|
|
380
|
-
expect(sleepUntilSpanCall[1].attributes['workflow.sleep.until']).toBe(
|
|
381
|
-
'2026-04-09T10:00:00.000Z',
|
|
382
|
-
);
|
|
383
|
-
expect(sleepUntilSpanCall[1].attributes['workflow.name']).toBe(
|
|
384
|
-
'sleep-until-test',
|
|
385
|
-
);
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
describe('this binding', () => {
|
|
390
|
-
it('should preserve this context in run()', async () => {
|
|
391
|
-
class TestWorkflow {
|
|
392
|
-
private value = 42;
|
|
393
|
-
constructor(public ctx: any, public env: any) {}
|
|
394
|
-
async run(event: any, step: any) {
|
|
395
|
-
return this.value;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const Instrumented = instrumentWorkflow(TestWorkflow, 'this-test', {
|
|
400
|
-
service: { name: 'test' },
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
const instance = new Instrumented({}, {});
|
|
404
|
-
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-this' };
|
|
405
|
-
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
406
|
-
|
|
407
|
-
// Should not throw — this.value should be accessible
|
|
408
|
-
await expect(instance.run(event, step)).resolves.not.toThrow();
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
});
|