Skip to content

Commit df9779b

Browse files
[Instrumentation] Add spans for Images Binding (#5236)
Wraps spans around the two methods that perform I/O: images_output cloudflare.binding.type cloudflare.images.options.format cloudflare.images.options.quality cloudflare.images.options.background cloudflare.images.options.anim cloudflare.images.options.transforms cloudflare.images.error.code error.type images_info cloudflare.binding.type cloudflare.images.options.encoding cloudflare.images.result.format cloudflare.images.result.file_size cloudflare.images.result.width cloudflare.images.result.height cloudflare.images.error.code error.type
1 parent c9565ad commit df9779b

File tree

3 files changed

+240
-199
lines changed

3 files changed

+240
-199
lines changed

src/cloudflare/internal/images-api.ts

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

1111
type Fetcher = {
1212
fetch: typeof fetch;
@@ -122,40 +122,51 @@ class ImageTransformerImpl implements ImageTransformer {
122122
async output(
123123
options: ImageOutputOptions
124124
): Promise<ImageTransformationResult> {
125-
const formData = new StreamableFormData();
125+
return await withSpan('images_output', async (span) => {
126+
span.setAttribute('cloudflare.binding.type', 'Images');
127+
const formData = new StreamableFormData();
126128

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

130-
this.#serializeTransforms(formData);
132+
this.#serializeTransforms(formData, span);
131133

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

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

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

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

156-
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);
157167

158-
return new TransformationResultImpl(response);
168+
return new TransformationResultImpl(response);
169+
});
159170
}
160171

161172
#consume(): void {
@@ -169,7 +180,7 @@ class ImageTransformerImpl implements ImageTransformer {
169180
this.#consumed = true;
170181
}
171182

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

175186
// image 0 is the canvas, so the first draw_image has index 1
@@ -211,6 +222,16 @@ class ImageTransformerImpl implements ImageTransformer {
211222
}
212223

213224
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+
}
214235
formData.append('transforms', JSON.stringify(transforms));
215236
}
216237
}
@@ -235,15 +256,18 @@ class ImagesBindingImpl implements ImagesBinding {
235256
options?: ImageInputOptions
236257
): Promise<ImageInfoResponse> {
237258
return await withSpan('images_info', async (span) => {
259+
span.setAttribute('cloudflare.binding.type', 'Images');
238260
const body = new StreamableFormData();
239261

262+
span.setAttribute(
263+
'cloudflare.images.options.encoding',
264+
options?.encoding ?? 'base64'
265+
);
240266
const decodedStream =
241267
options?.encoding === 'base64'
242268
? stream.pipeThrough(createBase64DecoderTransformStream())
243269
: stream;
244270

245-
span.setAttribute('cloudflare.images.info.encoding', options?.encoding);
246-
247271
body.append('image', decodedStream, { type: 'file' });
248272

249273
const response = await this.#fetcher.fetch(
@@ -257,22 +281,23 @@ class ImagesBindingImpl implements ImagesBinding {
257281
}
258282
);
259283

260-
await throwErrorIfErrorResponse('INFO', response);
284+
await throwErrorIfErrorResponse('INFO', response, span);
261285

262286
const r = (await response.json()) as RawInfoResponse;
263287

264-
span.setAttribute('cloudflare.images.info.format', r.format);
288+
span.setAttribute('cloudflare.images.result.format', r.format);
265289

266290
if ('file_size' in r) {
267-
span.setAttribute('cloudflare.images.info.file_size', r.file_size);
268-
span.setAttribute('cloudflare.images.info.width', r.width);
269-
span.setAttribute('cloudflare.images.info.height', r.height);
270-
return {
291+
const ret = {
271292
fileSize: r.file_size,
272293
width: r.width,
273294
height: r.height,
274295
format: r.format,
275296
};
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;
276301
}
277302

278303
return r;
@@ -302,24 +327,29 @@ class ImagesErrorImpl extends Error implements ImagesError {
302327

303328
async function throwErrorIfErrorResponse(
304329
operation: string,
305-
response: Response
330+
response: Response,
331+
span: Span
306332
): Promise<void> {
307333
const statusHeader = response.headers.get('cf-images-binding') || '';
308334

309335
const match = /err=(\d+)/.exec(statusHeader);
310336

311337
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);
312341
throw new ImagesErrorImpl(
313-
`IMAGES_${operation}_${await response.text()}`.trim(),
342+
`IMAGES_${operation}_${errorMessage}`.trim(),
314343
Number.parseInt(match[1])
315344
);
316345
}
317346

318347
if (response.status > 399) {
348+
const errorMessage = await response.text();
349+
span.setAttribute('cloudflare.images.error.code', '9523');
350+
span.setAttribute('error.type', errorMessage);
319351
throw new ImagesErrorImpl(
320-
`Unexpected error response ${response.status}: ${(
321-
await response.text()
322-
).trim()}`,
352+
`Unexpected error response ${response.status}: ${errorMessage.trim()}`,
323353
9523
324354
);
325355
}

0 commit comments

Comments
 (0)