autotel-cloudflare 2.12.0 → 2.14.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/actors.js +1 -1
- package/dist/bindings.d.ts +113 -1
- package/dist/bindings.js +2 -2
- package/dist/chunk-4UG2QCPQ.js +1060 -0
- package/dist/chunk-4UG2QCPQ.js.map +1 -0
- package/dist/{chunk-5NL62W4L.js → chunk-O4IYKWPJ.js} +8 -3
- package/dist/chunk-O4IYKWPJ.js.map +1 -0
- package/dist/{chunk-ADWSZ5GY.js → chunk-WDNZVVRW.js} +8 -9
- package/dist/chunk-WDNZVVRW.js.map +1 -0
- package/dist/handlers.d.ts +5 -14
- package/dist/handlers.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +34 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bindings/ai.test.ts +156 -0
- package/src/bindings/ai.ts +71 -0
- package/src/bindings/analytics-engine.test.ts +160 -0
- package/src/bindings/analytics-engine.ts +78 -0
- package/src/bindings/bindings-detection.test.ts +235 -0
- package/src/bindings/bindings.ts +98 -47
- package/src/bindings/browser-rendering.test.ts +144 -0
- package/src/bindings/browser-rendering.ts +70 -0
- package/src/bindings/common.ts +9 -0
- package/src/bindings/hyperdrive.test.ts +154 -0
- package/src/bindings/hyperdrive.ts +74 -0
- package/src/bindings/images.test.ts +229 -0
- package/src/bindings/images.ts +182 -0
- package/src/bindings/index.ts +8 -0
- package/src/bindings/queue-producer.test.ts +192 -0
- package/src/bindings/queue-producer.ts +105 -0
- package/src/bindings/rate-limiter.test.ts +124 -0
- package/src/bindings/rate-limiter.ts +69 -0
- package/src/bindings/vectorize.test.ts +340 -0
- package/src/bindings/vectorize.ts +86 -0
- package/src/handlers/workflows.test.ts +325 -0
- package/src/handlers/workflows.ts +51 -41
- package/src/index.ts +8 -0
- package/src/wrappers/cf-attributes.test.ts +275 -0
- package/src/wrappers/instrument.ts +38 -0
- package/dist/chunk-5NL62W4L.js.map +0 -1
- package/dist/chunk-ADWSZ5GY.js.map +0 -1
- package/dist/chunk-UPQE3J4I.js +0 -520
- package/dist/chunk-UPQE3J4I.js.map +0 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { instrumentWorkflow } from './workflows';
|
|
3
|
+
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
|
|
4
|
+
|
|
5
|
+
describe('Workflow Instrumentation', () => {
|
|
6
|
+
let mockTracer: any;
|
|
7
|
+
let mockSpan: any;
|
|
8
|
+
let getTracerSpy: any;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockSpan = {
|
|
12
|
+
spanContext: () => ({
|
|
13
|
+
traceId: 'test-trace-id',
|
|
14
|
+
spanId: 'test-span-id',
|
|
15
|
+
traceFlags: 1,
|
|
16
|
+
}),
|
|
17
|
+
setAttribute: vi.fn(),
|
|
18
|
+
setAttributes: vi.fn(),
|
|
19
|
+
setStatus: vi.fn(),
|
|
20
|
+
recordException: vi.fn(),
|
|
21
|
+
end: vi.fn(),
|
|
22
|
+
isRecording: () => true,
|
|
23
|
+
updateName: vi.fn(),
|
|
24
|
+
addEvent: vi.fn(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
mockTracer = {
|
|
28
|
+
startActiveSpan: vi.fn((name, options, context, fn) => {
|
|
29
|
+
if (typeof options === 'function') return options(mockSpan);
|
|
30
|
+
if (typeof context === 'function') return context(mockSpan);
|
|
31
|
+
if (typeof fn === 'function') return fn(mockSpan);
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
getTracerSpy.mockRestore();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('instrumentWorkflow()', () => {
|
|
44
|
+
it('should wrap workflow class constructor', () => {
|
|
45
|
+
class TestWorkflow {
|
|
46
|
+
constructor(public ctx: any, public env: any) {}
|
|
47
|
+
async run() {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
|
|
51
|
+
service: { name: 'test' },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(Instrumented).toBeDefined();
|
|
55
|
+
expect(typeof Instrumented).toBe('function');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should create workflow instance with instrumented run()', () => {
|
|
59
|
+
class TestWorkflow {
|
|
60
|
+
constructor(public ctx: any, public env: any) {}
|
|
61
|
+
async run() { return 'done'; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
|
|
65
|
+
service: { name: 'test' },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const instance = new Instrumented({}, {});
|
|
69
|
+
|
|
70
|
+
expect(instance).toBeDefined();
|
|
71
|
+
expect(typeof instance.run).toBe('function');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should accept static config', () => {
|
|
75
|
+
class TestWorkflow {
|
|
76
|
+
constructor(public ctx: any, public env: any) {}
|
|
77
|
+
async run() {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
|
|
81
|
+
service: { name: 'test', version: '1.0.0' },
|
|
82
|
+
exporter: { url: 'http://localhost:4318/v1/traces' },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(Instrumented).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should accept config function', () => {
|
|
89
|
+
class TestWorkflow {
|
|
90
|
+
constructor(public ctx: any, public env: any) {}
|
|
91
|
+
async run() {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface Env {
|
|
95
|
+
OTLP_ENDPOINT: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', (env: Env) => ({
|
|
99
|
+
service: { name: 'test' },
|
|
100
|
+
exporter: { url: env.OTLP_ENDPOINT },
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
expect(Instrumented).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('run() instrumentation', () => {
|
|
108
|
+
it('should preserve run() return value', async () => {
|
|
109
|
+
const output = { status: 'ok', orderId: 'ord-123' };
|
|
110
|
+
|
|
111
|
+
class TestWorkflow {
|
|
112
|
+
constructor(public ctx: any, public env: any) {}
|
|
113
|
+
async run(event: any, step: any) {
|
|
114
|
+
return output;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'return-test', {
|
|
119
|
+
service: { name: 'test' },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const instance = new Instrumented({}, {});
|
|
123
|
+
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-return' };
|
|
124
|
+
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
125
|
+
|
|
126
|
+
await expect(instance.run(event, step)).resolves.toEqual(output);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should create span for run() with workflow attributes', async () => {
|
|
130
|
+
class TestWorkflow {
|
|
131
|
+
constructor(public ctx: any, public env: any) {}
|
|
132
|
+
async run(event: any, step: any) {}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
|
|
136
|
+
service: { name: 'test' },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const instance = new Instrumented({}, {});
|
|
140
|
+
const event = { payload: { foo: 'bar' }, timestamp: new Date(), instanceId: 'wf-123' };
|
|
141
|
+
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
142
|
+
|
|
143
|
+
await instance.run(event, step);
|
|
144
|
+
|
|
145
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalled();
|
|
146
|
+
|
|
147
|
+
const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
|
|
148
|
+
expect(spanName).toBe('Workflow test-workflow: run');
|
|
149
|
+
|
|
150
|
+
const options = mockTracer.startActiveSpan.mock.calls[0][1];
|
|
151
|
+
expect(options.kind).toBe(SpanKind.INTERNAL);
|
|
152
|
+
expect(options.attributes['workflow.name']).toBe('test-workflow');
|
|
153
|
+
expect(options.attributes['workflow.instance_id']).toBe('wf-123');
|
|
154
|
+
expect(options.attributes['faas.trigger']).toBe('workflow');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should track cold starts', async () => {
|
|
158
|
+
class TestWorkflow {
|
|
159
|
+
constructor(public ctx: any, public env: any) {}
|
|
160
|
+
async run(event: any, step: any) {}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'cold-test', {
|
|
164
|
+
service: { name: 'test' },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-1' };
|
|
168
|
+
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
169
|
+
|
|
170
|
+
const instance1 = new Instrumented({}, {});
|
|
171
|
+
await instance1.run(event, step);
|
|
172
|
+
const firstOptions = mockTracer.startActiveSpan.mock.calls[0][1];
|
|
173
|
+
expect(firstOptions.attributes['faas.coldstart']).toBe(true);
|
|
174
|
+
|
|
175
|
+
const instance2 = new Instrumented({}, {});
|
|
176
|
+
await instance2.run(event, step);
|
|
177
|
+
const secondOptions = mockTracer.startActiveSpan.mock.calls[1][1];
|
|
178
|
+
expect(secondOptions.attributes['faas.coldstart']).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle run() errors', async () => {
|
|
182
|
+
class TestWorkflow {
|
|
183
|
+
constructor(public ctx: any, public env: any) {}
|
|
184
|
+
async run() {
|
|
185
|
+
throw new Error('Workflow failed');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'error-test', {
|
|
190
|
+
service: { name: 'test' },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const instance = new Instrumented({}, {});
|
|
194
|
+
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-err' };
|
|
195
|
+
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
196
|
+
|
|
197
|
+
await expect(instance.run(event, step)).rejects.toThrow('Workflow failed');
|
|
198
|
+
|
|
199
|
+
expect(mockSpan.recordException).toHaveBeenCalled();
|
|
200
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
201
|
+
code: SpanStatusCode.ERROR,
|
|
202
|
+
message: 'Workflow failed',
|
|
203
|
+
});
|
|
204
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('step.do() instrumentation', () => {
|
|
209
|
+
it('should create span for step.do() calls', async () => {
|
|
210
|
+
const stepDoResult = { paymentId: 'pay_123' };
|
|
211
|
+
const mockStepDo = vi.fn().mockResolvedValue(stepDoResult);
|
|
212
|
+
|
|
213
|
+
class TestWorkflow {
|
|
214
|
+
constructor(public ctx: any, public env: any) {}
|
|
215
|
+
async run(event: any, step: any) {
|
|
216
|
+
return await step.do('process payment', async () => stepDoResult);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'step-test', {
|
|
221
|
+
service: { name: 'test' },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const instance = new Instrumented({}, {});
|
|
225
|
+
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-step' };
|
|
226
|
+
const step = {
|
|
227
|
+
do: vi.fn(async (_name: string, cb: () => Promise<any>) => cb()),
|
|
228
|
+
sleep: vi.fn(),
|
|
229
|
+
sleepUntil: vi.fn(),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
await instance.run(event, step);
|
|
233
|
+
|
|
234
|
+
// Should have spans for both run() and step.do()
|
|
235
|
+
expect(mockTracer.startActiveSpan.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
236
|
+
|
|
237
|
+
const stepSpanCall = mockTracer.startActiveSpan.mock.calls[1];
|
|
238
|
+
expect(stepSpanCall[0]).toBe('Workflow step-test: process payment');
|
|
239
|
+
expect(stepSpanCall[1].attributes['workflow.step.name']).toBe('process payment');
|
|
240
|
+
expect(stepSpanCall[1].attributes['workflow.name']).toBe('step-test');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should handle step.do() errors', async () => {
|
|
244
|
+
class TestWorkflow {
|
|
245
|
+
constructor(public ctx: any, public env: any) {}
|
|
246
|
+
async run(event: any, step: any) {
|
|
247
|
+
await step.do('failing step', async () => {
|
|
248
|
+
throw new Error('Step failed');
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'step-err', {
|
|
254
|
+
service: { name: 'test' },
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const instance = new Instrumented({}, {});
|
|
258
|
+
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-step-err' };
|
|
259
|
+
const step = {
|
|
260
|
+
do: vi.fn(async (_name: string, cb: () => Promise<any>) => cb()),
|
|
261
|
+
sleep: vi.fn(),
|
|
262
|
+
sleepUntil: vi.fn(),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
await expect(instance.run(event, step)).rejects.toThrow('Step failed');
|
|
266
|
+
|
|
267
|
+
expect(mockSpan.recordException).toHaveBeenCalled();
|
|
268
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('step.sleep() instrumentation', () => {
|
|
273
|
+
it('should create span for step.sleep() calls', async () => {
|
|
274
|
+
class TestWorkflow {
|
|
275
|
+
constructor(public ctx: any, public env: any) {}
|
|
276
|
+
async run(event: any, step: any) {
|
|
277
|
+
await step.sleep('wait for settlement', '2 hours');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'sleep-test', {
|
|
282
|
+
service: { name: 'test' },
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const instance = new Instrumented({}, {});
|
|
286
|
+
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-sleep' };
|
|
287
|
+
const step = {
|
|
288
|
+
do: vi.fn(),
|
|
289
|
+
sleep: vi.fn().mockResolvedValue(undefined),
|
|
290
|
+
sleepUntil: vi.fn(),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
await instance.run(event, step);
|
|
294
|
+
|
|
295
|
+
const sleepSpanCall = mockTracer.startActiveSpan.mock.calls[1];
|
|
296
|
+
expect(sleepSpanCall[0]).toBe('Workflow sleep-test: sleep wait for settlement');
|
|
297
|
+
expect(sleepSpanCall[1].attributes['workflow.sleep.name']).toBe('wait for settlement');
|
|
298
|
+
expect(sleepSpanCall[1].attributes['workflow.sleep.duration']).toBe('2 hours');
|
|
299
|
+
expect(sleepSpanCall[1].attributes['workflow.name']).toBe('sleep-test');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('this binding', () => {
|
|
304
|
+
it('should preserve this context in run()', async () => {
|
|
305
|
+
class TestWorkflow {
|
|
306
|
+
private value = 42;
|
|
307
|
+
constructor(public ctx: any, public env: any) {}
|
|
308
|
+
async run(event: any, step: any) {
|
|
309
|
+
return this.value;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const Instrumented = instrumentWorkflow(TestWorkflow, 'this-test', {
|
|
314
|
+
service: { name: 'test' },
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const instance = new Instrumented({}, {});
|
|
318
|
+
const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-this' };
|
|
319
|
+
const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
|
|
320
|
+
|
|
321
|
+
// Should not throw — this.value should be accessible
|
|
322
|
+
await expect(instance.run(event, step)).resolves.not.toThrow();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -14,25 +14,48 @@ import {
|
|
|
14
14
|
SpanStatusCode,
|
|
15
15
|
SpanKind,
|
|
16
16
|
} from '@opentelemetry/api';
|
|
17
|
-
import type { ConfigurationOption } from 'autotel-edge';
|
|
17
|
+
import type { ConfigurationOption, WorkflowTrigger } from 'autotel-edge';
|
|
18
18
|
import { createInitialiser, setConfig, WorkerTracer } from 'autotel-edge';
|
|
19
19
|
import { wrap } from '../bindings/common';
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Workflow types matching the Cloudflare Workers Workflows API.
|
|
23
|
+
* @see https://developers.cloudflare.com/workflows/
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface WorkflowEvent<T = unknown> {
|
|
27
|
+
payload: Readonly<T>;
|
|
28
|
+
timestamp: Date;
|
|
29
|
+
instanceId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface WorkflowStepConfig {
|
|
33
|
+
retries?: {
|
|
34
|
+
limit: number;
|
|
35
|
+
delay?: string | number;
|
|
36
|
+
backoff?: 'constant' | 'linear' | 'exponential';
|
|
37
|
+
};
|
|
38
|
+
timeout?: string | number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface WorkflowStep {
|
|
42
|
+
do<T>(name: string, callback: () => Promise<T>): Promise<T>;
|
|
43
|
+
do<T>(name: string, config: WorkflowStepConfig, callback: () => Promise<T>): Promise<T>;
|
|
44
|
+
sleep(name: string, duration: string | number): Promise<void>;
|
|
45
|
+
sleepUntil(name: string, timestamp: Date | number): Promise<void>;
|
|
46
|
+
}
|
|
24
47
|
|
|
25
48
|
type WorkflowRunFn = (
|
|
26
|
-
event: WorkflowEvent
|
|
49
|
+
event: Readonly<WorkflowEvent>,
|
|
27
50
|
step: WorkflowStep,
|
|
28
|
-
) => Promise<
|
|
51
|
+
) => Promise<unknown> | void;
|
|
29
52
|
|
|
30
53
|
/**
|
|
31
54
|
* Track cold starts per Workflow class
|
|
32
55
|
*/
|
|
33
|
-
const coldStarts = new WeakMap<
|
|
56
|
+
const coldStarts = new WeakMap<object, boolean>();
|
|
34
57
|
|
|
35
|
-
function isColdStart(workflowClass:
|
|
58
|
+
function isColdStart(workflowClass: object): boolean {
|
|
36
59
|
if (!coldStarts.has(workflowClass)) {
|
|
37
60
|
coldStarts.set(workflowClass, true);
|
|
38
61
|
return true;
|
|
@@ -41,7 +64,7 @@ function isColdStart(workflowClass: any): boolean {
|
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
/**
|
|
44
|
-
* Proxy the step object to instrument step.do() calls
|
|
67
|
+
* Proxy the step object to instrument step.do() and step.sleep() calls
|
|
45
68
|
*/
|
|
46
69
|
function instrumentWorkflowStep(
|
|
47
70
|
step: WorkflowStep,
|
|
@@ -55,7 +78,7 @@ function instrumentWorkflowStep(
|
|
|
55
78
|
if (prop === 'do' && typeof value === 'function') {
|
|
56
79
|
return new Proxy(value, {
|
|
57
80
|
apply: (fnTarget, thisArg, args) => {
|
|
58
|
-
const [stepName] = args as [string,
|
|
81
|
+
const [stepName] = args as [string, ...unknown[]];
|
|
59
82
|
|
|
60
83
|
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
61
84
|
|
|
@@ -148,13 +171,13 @@ function instrumentWorkflowStep(
|
|
|
148
171
|
function instrumentWorkflowRun(
|
|
149
172
|
runFn: WorkflowRunFn,
|
|
150
173
|
workflowName: string,
|
|
151
|
-
workflowClass:
|
|
174
|
+
workflowClass: object,
|
|
152
175
|
): WorkflowRunFn {
|
|
153
176
|
return async function instrumentedRun(
|
|
154
|
-
this:
|
|
155
|
-
event: WorkflowEvent
|
|
177
|
+
this: unknown,
|
|
178
|
+
event: Readonly<WorkflowEvent>,
|
|
156
179
|
step: WorkflowStep,
|
|
157
|
-
): Promise<
|
|
180
|
+
): Promise<unknown> {
|
|
158
181
|
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
159
182
|
|
|
160
183
|
// Instrument the step object to track individual operations
|
|
@@ -168,17 +191,16 @@ function instrumentWorkflowRun(
|
|
|
168
191
|
kind: SpanKind.INTERNAL,
|
|
169
192
|
attributes: {
|
|
170
193
|
'workflow.name': workflowName,
|
|
194
|
+
'workflow.instance_id': event.instanceId,
|
|
171
195
|
'faas.trigger': 'workflow',
|
|
172
196
|
'faas.coldstart': isColdStart(workflowClass),
|
|
173
|
-
// Add workflow event attributes if available
|
|
174
|
-
...(event?.workflowId && { 'workflow.id': event.workflowId }),
|
|
175
|
-
...(event?.runId && { 'workflow.run_id': event.runId }),
|
|
176
197
|
},
|
|
177
198
|
},
|
|
178
199
|
async (span) => {
|
|
179
200
|
try {
|
|
180
|
-
await runFn.call(this, event, instrumentedStep);
|
|
201
|
+
const result = await runFn.call(this, event, instrumentedStep);
|
|
181
202
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
203
|
+
return result;
|
|
182
204
|
} catch (error) {
|
|
183
205
|
span.recordException(error as Error);
|
|
184
206
|
span.setStatus({
|
|
@@ -198,17 +220,17 @@ function instrumentWorkflowRun(
|
|
|
198
220
|
* Instrument a Workflow instance
|
|
199
221
|
*/
|
|
200
222
|
function instrumentWorkflowInstance(
|
|
201
|
-
workflowInstance:
|
|
223
|
+
workflowInstance: Record<string, unknown>,
|
|
202
224
|
workflowName: string,
|
|
203
|
-
workflowClass:
|
|
204
|
-
):
|
|
205
|
-
const instanceHandler: ProxyHandler<
|
|
225
|
+
workflowClass: object,
|
|
226
|
+
): Record<string, unknown> {
|
|
227
|
+
const instanceHandler: ProxyHandler<Record<string, unknown>> = {
|
|
206
228
|
get(target, prop) {
|
|
207
229
|
const value = Reflect.get(target, prop);
|
|
208
230
|
|
|
209
231
|
if (prop === 'run' && typeof value === 'function') {
|
|
210
232
|
return instrumentWorkflowRun(
|
|
211
|
-
value.bind(target),
|
|
233
|
+
value.bind(target) as WorkflowRunFn,
|
|
212
234
|
workflowName,
|
|
213
235
|
workflowClass,
|
|
214
236
|
);
|
|
@@ -235,12 +257,12 @@ function instrumentWorkflowInstance(
|
|
|
235
257
|
* **Usage:**
|
|
236
258
|
* ```typescript
|
|
237
259
|
* import { WorkflowEntrypoint } from 'cloudflare:workers'
|
|
238
|
-
* import { instrumentWorkflow } from 'autotel-
|
|
260
|
+
* import { instrumentWorkflow } from 'autotel-cloudflare/handlers'
|
|
239
261
|
*
|
|
240
|
-
*
|
|
262
|
+
* class MyWorkflow extends WorkflowEntrypoint {
|
|
241
263
|
* async run(event, step) {
|
|
242
264
|
* await step.do('submit payment', async () => {
|
|
243
|
-
* return await submitToPaymentProcessor(event.
|
|
265
|
+
* return await submitToPaymentProcessor(event.payload.payment)
|
|
244
266
|
* })
|
|
245
267
|
*
|
|
246
268
|
* await step.sleep('wait for feedback', '2 days')
|
|
@@ -249,9 +271,8 @@ function instrumentWorkflowInstance(
|
|
|
249
271
|
* }
|
|
250
272
|
* }
|
|
251
273
|
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* CheckoutWorkflow,
|
|
274
|
+
* export const CheckoutWorkflow = instrumentWorkflow(
|
|
275
|
+
* MyWorkflow,
|
|
255
276
|
* 'checkout-workflow',
|
|
256
277
|
* (env: Env) => ({
|
|
257
278
|
* exporter: {
|
|
@@ -266,14 +287,6 @@ function instrumentWorkflowInstance(
|
|
|
266
287
|
* )
|
|
267
288
|
* ```
|
|
268
289
|
*
|
|
269
|
-
* **What you get:**
|
|
270
|
-
* - 🎯 Automatic spans for workflow.run() execution
|
|
271
|
-
* - 📋 Automatic spans for each step.do() operation
|
|
272
|
-
* - ⏸️ Automatic spans for step.sleep() operations
|
|
273
|
-
* - 🔄 Automatic retry tracking (via step.do retries)
|
|
274
|
-
* - 🥶 Cold start tracking
|
|
275
|
-
* - ⚡ Automatic span lifecycle management
|
|
276
|
-
*
|
|
277
290
|
* @param workflowClass - The WorkflowEntrypoint class to instrument
|
|
278
291
|
* @param workflowName - The name of the workflow (used in span names)
|
|
279
292
|
* @param config - Configuration or configuration function
|
|
@@ -293,9 +306,7 @@ export function instrumentWorkflow<
|
|
|
293
306
|
// Extract env from constructor args (typically last arg)
|
|
294
307
|
const env = args[args.length - 1] || {};
|
|
295
308
|
|
|
296
|
-
|
|
297
|
-
// Use Request as trigger type since workflows don't have a standard Trigger type yet
|
|
298
|
-
const trigger = new Request('https://workflow.local/run');
|
|
309
|
+
const trigger: WorkflowTrigger = { type: 'workflow', name: workflowName };
|
|
299
310
|
const workflowConfig = initialiser(env, trigger);
|
|
300
311
|
const context = setConfig(workflowConfig);
|
|
301
312
|
|
|
@@ -315,4 +326,3 @@ export function instrumentWorkflow<
|
|
|
315
326
|
|
|
316
327
|
return wrap(workflowClass, classHandler);
|
|
317
328
|
}
|
|
318
|
-
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,14 @@ export {
|
|
|
51
51
|
instrumentD1,
|
|
52
52
|
instrumentServiceBinding,
|
|
53
53
|
instrumentBindings,
|
|
54
|
+
instrumentAI,
|
|
55
|
+
instrumentVectorize,
|
|
56
|
+
instrumentHyperdrive,
|
|
57
|
+
instrumentQueueProducer,
|
|
58
|
+
instrumentAnalyticsEngine,
|
|
59
|
+
instrumentImages,
|
|
60
|
+
instrumentRateLimiter,
|
|
61
|
+
instrumentBrowserRendering,
|
|
54
62
|
} from './bindings';
|
|
55
63
|
|
|
56
64
|
// Global instrumentations
|