Skip to content

Commit 3527891

Browse files
authored
Merge pull request #5395 from cloudflare/mar/stw-tracing-module-2
Reland tracing infrastructure for bindings with fix.
2 parents 8baaa31 + 0037e9c commit 3527891

26 files changed

+2340
-168
lines changed

src/cloudflare/internal/d1-api.ts

Lines changed: 339 additions & 101 deletions
Large diffs are not rendered by default.

src/cloudflare/internal/images-api.ts

Lines changed: 99 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createBase64DecoderTransformStream,
77
createBase64EncoderTransformStream,
88
} from 'cloudflare-internal:streaming-base64';
9+
import { withSpan, type Span } from 'cloudflare-internal:tracing-helpers';
910

1011
type Fetcher = {
1112
fetch: typeof fetch;
@@ -121,40 +122,51 @@ class ImageTransformerImpl implements ImageTransformer {
121122
async output(
122123
options: ImageOutputOptions
123124
): Promise<ImageTransformationResult> {
124-
const formData = new StreamableFormData();
125+
return await withSpan('images_output', async (span) => {
126+
span.setAttribute('cloudflare.binding.type', 'Images');
127+
const formData = new StreamableFormData();
125128

126-
this.#consume();
127-
formData.append('image', this.#stream, { type: 'file' });
129+
this.#consume();
130+
formData.append('image', this.#stream, { type: 'file' });
128131

129-
this.#serializeTransforms(formData);
132+
this.#serializeTransforms(formData, span);
130133

131-
formData.append('output_format', options.format);
132-
if (options.quality !== undefined) {
133-
formData.append('output_quality', options.quality.toString());
134-
}
134+
span.setAttribute('cloudflare.images.options.format', options.format);
135+
formData.append('output_format', options.format);
135136

136-
if (options.background !== undefined) {
137-
formData.append('background', options.background);
138-
}
137+
if (options.quality !== undefined) {
138+
span.setAttribute('cloudflare.images.options.quality', options.quality);
139+
formData.append('output_quality', options.quality.toString());
140+
}
139141

140-
if (options.anim !== undefined) {
141-
formData.append('anim', options.anim.toString());
142-
}
142+
if (options.background !== undefined) {
143+
span.setAttribute(
144+
'cloudflare.images.options.background',
145+
options.background
146+
);
147+
formData.append('background', options.background);
148+
}
143149

144-
const response = await this.#fetcher.fetch(
145-
'https://js.images.cloudflare.com/transform',
146-
{
147-
method: 'POST',
148-
headers: {
149-
'content-type': formData.contentType(),
150-
},
151-
body: formData.stream(),
150+
if (options.anim !== undefined) {
151+
span.setAttribute('cloudflare.images.options.anim', options.anim);
152+
formData.append('anim', options.anim.toString());
152153
}
153-
);
154154

155-
await throwErrorIfErrorResponse('TRANSFORM', response);
155+
const response = await this.#fetcher.fetch(
156+
'https://js.images.cloudflare.com/transform',
157+
{
158+
method: 'POST',
159+
headers: {
160+
'content-type': formData.contentType(),
161+
},
162+
body: formData.stream(),
163+
}
164+
);
165+
166+
await throwErrorIfErrorResponse('TRANSFORM', response, span);
156167

157-
return new TransformationResultImpl(response);
168+
return new TransformationResultImpl(response);
169+
});
158170
}
159171

160172
#consume(): void {
@@ -168,7 +180,7 @@ class ImageTransformerImpl implements ImageTransformer {
168180
this.#consumed = true;
169181
}
170182

171-
#serializeTransforms(formData: StreamableFormData): void {
183+
#serializeTransforms(formData: StreamableFormData, span: Span): void {
172184
const transforms: (TargetedTransform | DrawCommand)[] = [];
173185

174186
// image 0 is the canvas, so the first draw_image has index 1
@@ -210,6 +222,16 @@ class ImageTransformerImpl implements ImageTransformer {
210222
}
211223

212224
walkTransforms(0, this.#transforms);
225+
226+
// The transforms are a set of operations which are applied to the image in order.
227+
// Attaching an attribute as JSON is a little odd, but I'm not sure if there is
228+
// a better way to do this.
229+
if (transforms.length > 0) {
230+
span.setAttribute(
231+
'cloudflare.images.options.transforms',
232+
JSON.stringify(transforms)
233+
);
234+
}
213235
formData.append('transforms', JSON.stringify(transforms));
214236
}
215237
}
@@ -233,40 +255,53 @@ class ImagesBindingImpl implements ImagesBinding {
233255
stream: ReadableStream<Uint8Array>,
234256
options?: ImageInputOptions
235257
): Promise<ImageInfoResponse> {
236-
const body = new StreamableFormData();
258+
return await withSpan('images_info', async (span) => {
259+
span.setAttribute('cloudflare.binding.type', 'Images');
260+
const body = new StreamableFormData();
237261

238-
const decodedStream =
239-
options?.encoding === 'base64'
240-
? stream.pipeThrough(createBase64DecoderTransformStream())
241-
: stream;
242-
243-
body.append('image', decodedStream, { type: 'file' });
262+
span.setAttribute(
263+
'cloudflare.images.options.encoding',
264+
options?.encoding ?? 'base64'
265+
);
266+
const decodedStream =
267+
options?.encoding === 'base64'
268+
? stream.pipeThrough(createBase64DecoderTransformStream())
269+
: stream;
270+
271+
body.append('image', decodedStream, { type: 'file' });
272+
273+
const response = await this.#fetcher.fetch(
274+
'https://js.images.cloudflare.com/info',
275+
{
276+
method: 'POST',
277+
headers: {
278+
'content-type': body.contentType(),
279+
},
280+
body: body.stream(),
281+
}
282+
);
244283

245-
const response = await this.#fetcher.fetch(
246-
'https://js.images.cloudflare.com/info',
247-
{
248-
method: 'POST',
249-
headers: {
250-
'content-type': body.contentType(),
251-
},
252-
body: body.stream(),
253-
}
254-
);
284+
await throwErrorIfErrorResponse('INFO', response, span);
255285

256-
await throwErrorIfErrorResponse('INFO', response);
286+
const r = (await response.json()) as RawInfoResponse;
257287

258-
const r = (await response.json()) as RawInfoResponse;
288+
span.setAttribute('cloudflare.images.result.format', r.format);
259289

260-
if ('file_size' in r) {
261-
return {
262-
fileSize: r.file_size,
263-
width: r.width,
264-
height: r.height,
265-
format: r.format,
266-
};
267-
}
290+
if ('file_size' in r) {
291+
const ret = {
292+
fileSize: r.file_size,
293+
width: r.width,
294+
height: r.height,
295+
format: r.format,
296+
};
297+
span.setAttribute('cloudflare.images.result.file_size', ret.fileSize);
298+
span.setAttribute('cloudflare.images.result.width', ret.width);
299+
span.setAttribute('cloudflare.images.result.height', ret.height);
300+
return ret;
301+
}
268302

269-
return r;
303+
return r;
304+
});
270305
}
271306

272307
input(
@@ -292,24 +327,29 @@ class ImagesErrorImpl extends Error implements ImagesError {
292327

293328
async function throwErrorIfErrorResponse(
294329
operation: string,
295-
response: Response
330+
response: Response,
331+
span: Span
296332
): Promise<void> {
297333
const statusHeader = response.headers.get('cf-images-binding') || '';
298334

299335
const match = /err=(\d+)/.exec(statusHeader);
300336

301337
if (match && match[1]) {
338+
const errorMessage = await response.text();
339+
span.setAttribute('cloudflare.images.error.code', match[1]);
340+
span.setAttribute('error.type', errorMessage);
302341
throw new ImagesErrorImpl(
303-
`IMAGES_${operation}_${await response.text()}`.trim(),
342+
`IMAGES_${operation}_${errorMessage}`.trim(),
304343
Number.parseInt(match[1])
305344
);
306345
}
307346

308347
if (response.status > 399) {
348+
const errorMessage = await response.text();
349+
span.setAttribute('cloudflare.images.error.code', '9523');
350+
span.setAttribute('error.type', errorMessage);
309351
throw new ImagesErrorImpl(
310-
`Unexpected error response ${response.status}: ${(
311-
await response.text()
312-
).trim()}`,
352+
`Unexpected error response ${response.status}: ${errorMessage.trim()}`,
313353
9523
314354
);
315355
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) 2025 Cloudflare, Inc.
2+
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3+
// https://opensource.org/licenses/Apache-2.0
4+
5+
// TEST-ONLY MODULE - DO NOT USE IN PRODUCTION
6+
// This wrapper exists solely to expose internal tracing utilities for testing.
7+
// It must be in the internal/ directory to be compiled as part of the cloudflare bundle,
8+
// but it should never be used outside of test configurations.
9+
10+
import { withSpan } from 'cloudflare-internal:tracing-helpers';
11+
12+
interface TestWrapper {
13+
withSpan: typeof withSpan;
14+
}
15+
16+
// Wrapper function that provides test utilities for tracing
17+
export default function (_env: unknown): TestWrapper {
18+
return {
19+
// Export withSpan for testing
20+
withSpan,
21+
};
22+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
exports_files(
2+
["instrumentation-test-helper.js"],
3+
visibility = [
4+
"//src/cloudflare/internal/test/d1:__pkg__",
5+
"//src/cloudflare/internal/test/images:__pkg__",
6+
"//src/cloudflare/internal/test/tracing:__pkg__",
7+
"//src/workerd/api:__pkg__",
8+
],
9+
)

src/cloudflare/internal/test/d1/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ wd_test(
77
data = glob(
88
["*.js"],
99
exclude = ["d1-api-test-with-sessions.js"],
10-
),
10+
) + ["//src/cloudflare/internal/test:instrumentation-test-helper.js"],
1111
)
1212

1313
wd_test(

0 commit comments

Comments
 (0)