INTEGRITY Документация

Пользовательские спаны

Cloudflare Workers автоматически инструментирует операции платформы, такие как вызовы fetch, чтение KV и запросы D1. Пользовательские спаны позволяют расширить эту видимость на логику вашего приложения, чтобы отслеживать собственные пути выполнения кода наряду со встроенной инструментацией.

API пользовательских spans доступен двумя способами: оба предоставляют одинаковые методы и ведут себя одинаково:

Есть два метода создания спанов:

Включить трассировку

Для пользовательских спанов необходимо включить трассировку для Worker. Если вы ещё этого не сделали, задайте observability.traces.enabled к true в вашем конфигурационный файл Wrangler:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "observability": {
    "traces": {
      "enabled": true
    }
  }
}
[observability.traces]
enabled = true

Создайте пользовательский спан

Используйте tracing.enterSpan() чтобы обернуть участок кода в именованный span. Span автоматически становится дочерним по отношению к тому span, который активен в данный момент, и завершается, когда callback возвращает значение или его promise переходит в разрешенное состояние.

В следующем примере используются оба способа доступа: метод cloudflare:workers импорт и ctx.tracing : чтобы показать, что они взаимозаменяемы:

src/index.js
import { tracing } from "cloudflare:workers";

export default {
	async fetch(request, env, ctx) {
		// Using the import
		return tracing.enterSpan("handleRequest", async (span) => {
			span.setAttribute("url.path", new URL(request.url).pathname);

			const user = await ctx.tracing.enterSpan("auth", async () => {
				// Using ctx.tracing
				return authenticate(request, env);
			});

			return buildResponse(user);
		});
	},
};
src/index.ts
import { tracing } from "cloudflare:workers";

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext) {
		// Using the import
		return tracing.enterSpan("handleRequest", async (span) => {
			span.setAttribute("url.path", new URL(request.url).pathname);

			const user = await ctx.tracing.enterSpan("auth", async () => {
				// Using ctx.tracing
				return authenticate(request, env);
			});

			return buildResponse(user);
		});
	},
};

Справочник по API

tracing.enterSpan(name, callback, ...args)

Создает новый спан и запускает callback внутри него. Span автоматически завершается, когда обратный вызов возвращает результат (синхронно или асинхронно) или выбрасывает исключение.

Параметры:

Параметр Тип Описание
name string Имя спана. Оно отображается в визуализациях трассировки.
callback (span: Span, ...args: A) => T Функция, выполняемая внутри span. Принимает Span объект в качестве первого аргумента, за которым следуют любые дополнительные аргументы, переданные в enterSpan.
...args A Необязательные дополнительные аргументы, передаваемые в колбэк после span параметр.

Возвращает: Возвращаемое значение callback.

Поведение:

// Synchronous callback — span ends when the function returns
const result = tracing.enterSpan("parse", (span) => {
	span.setAttribute("format", "json");
	return JSON.parse(body);
});

// Async callback — span ends when the promise settles
const data = await tracing.enterSpan("fetchData", async (span) => {
	const res = await fetch("https://api.example.com/data");
	span.setAttribute("http.response.status_code", res.status);
	return res.json();
});

// Forwarding arguments
const doubled = tracing.enterSpan("compute", (span, x) => x * 2, 21);

tracing.startActiveSpan(name, callback, ...args)

Создает новый спан, делает его активным, пока callback выполняется и возвращает результат обратного вызова без автоматически завершает span. Вы должны вызвать span.end() явно по завершении операции.

Параметры:

Параметр Тип Описание
name string Имя спана. Оно отображается в визуализациях трассировки.
callback (span: Span, ...args: A) => T Функция, выполняемая, пока span активен. Принимает Span объект в качестве первого аргумента, за которым следуют любые дополнительные аргументы.
...args A Необязательные дополнительные аргументы, передаваемые в колбэк после span параметр.

Возвращает: Возвращаемое значение callback.

Поведение:

Используйте startActiveSpan когда span должен охватывать операцию, выходящую за рамки одного колбэка: например, при инструментировании конвейера потока, где span должен оставаться открытым до полного считывания потока:

src/index.js
import { tracing } from "cloudflare:workers";

export default {
	async fetch(request, env, ctx) {
		const body = request.body;
		if (!body) return new Response("No body", { status: 400 });

		// The span is active during the callback, so the pipeThrough
		// operation is correctly nested. The span stays open after
		// the callback returns, until flush() calls span.end().
		const stream = tracing.startActiveSpan("process-stream", (span) => {
			span.setAttribute(
				"request.content_type",
				request.headers.get("content-type") ?? "unknown",
			);

			return body.pipeThrough(
				new TransformStream({
					transform(chunk, controller) {
						// Process each chunk
						controller.enqueue(chunk);
					},
					flush() {
						span.setAttribute("stream.status", "complete");
						span.end();
					},
					cancel() {
						span.setAttribute("stream.status", "cancelled");
						span.end();
					},
				}),
			);
		});

		return new Response(stream);
	},
};
src/index.ts
import { tracing } from "cloudflare:workers";

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext) {
		const body = request.body;
		if (!body) return new Response("No body", { status: 400 });

		// The span is active during the callback, so the pipeThrough
		// operation is correctly nested. The span stays open after
		// the callback returns, until flush() calls span.end().
		const stream = tracing.startActiveSpan("process-stream", (span) => {
			span.setAttribute(
				"request.content_type",
				request.headers.get("content-type") ?? "unknown",
			);

			return body.pipeThrough(
				new TransformStream({
					transform(chunk, controller) {
						// Process each chunk
						controller.enqueue(chunk);
					},
					flush() {
						span.setAttribute("stream.status", "complete");
						span.end();
					},
					cancel() {
						span.setAttribute("stream.status", "cancelled");
						span.end();
					},
				}),
			);
		});

		return new Response(stream);
	},
};

Также можно сохранить ссылку на span для последующего использования без потоков:

let capturedSpan;
const value = tracing.startActiveSpan("manual-operation", (span) => {
	capturedSpan = span;
	span.setAttribute("phase", "started");
	return computeResult();
});

// The span is still open here — you can set more attributes
capturedSpan.setAttribute("phase", "complete");
capturedSpan.end(); // Now the span is submitted

Span

Span объект передаётся в enterSpan и startActiveSpan обратные вызовы. Он предоставляет методы для добавления метаданных к спану и управления его жизненным циклом.

span.setAttribute(key, value)

Задаёт атрибут для span.

Параметр Тип Описание
key string Имя атрибута.
value string | number | boolean | undefined Значение атрибута. Передача undefined не выполняет никаких действий (no-op).

Атрибуты отображаются рядом со спаном в трассировках и экспортах OpenTelemetry.

span.setAttribute("user.plan", "enterprise");
span.setAttribute("item.count", 42);
span.setAttribute("cache.hit", true);

span.isTraced

A readonly boolean указывающий, трассируется ли данный вызов. Если запрос не попал в выборку (в соответствии с вашей head_sampling_rate), isTraced это false и enterSpan по-прежнему выполняет callback, но не записывает никакой телеметрии.

Это можно использовать, чтобы пропустить затратные вычисления атрибутов, когда запрос не трассируется:

tracing.enterSpan("process", (span) => {
	if (span.isTraced) {
		span.setAttribute(
			"request.body.preview",
			JSON.stringify(body).slice(0, 200),
		);
	}
	return processBody(body);
});

span.end()

Завершает спан и передаёт его атрибуты в систему трассировки. Этот метод идемпотентен: повторные вызовы после первого не имеют эффекта. После end() вызывается, span.isTraced возвращает false и любые последующие setAttribute вызовы молча игнорируются, включая вызовы из еще не завершенных асинхронных операций.

let mySpan;
const result = tracing.startActiveSpan("manual-op", (span) => {
	mySpan = span;
	span.setAttribute("step", "processing");
	return doWork();
});

// Later, when the work is truly complete:
mySpan.end(); // Span is submitted
mySpan.end(); // No-op, safe to call again

Вложенные спаны

Spans автоматически вкладываются друг в друга на основе асинхронного контекста JavaScript. Любой enterSpan вызов или операция платформы (например, fetch и env.MY_KV.get()) который выполняется внутри callback, становится дочерним по отношению к охватывающему span.

src/index.js
import { tracing } from "cloudflare:workers";

async function handleOrder(env, orderId) {
	return tracing.enterSpan("handleOrder", async (span) => {
		span.setAttribute("order.id", orderId);

		// This KV read is automatically a child of "handleOrder"
		const order = await env.ORDERS_KV.get(orderId, "json");

		// This nested span is also a child of "handleOrder"
		const total = tracing.enterSpan("calculateTotal", (innerSpan) => {
			innerSpan.setAttribute("item.count", order.items.length);
			return order.items.reduce((sum, item) => sum + item.price, 0);
		});

		// This fetch is a child of "handleOrder"
		await fetch("https://api.example.com/notify", {
			method: "POST",
			body: JSON.stringify({ orderId, total }),
		});

		return new Response(JSON.stringify({ orderId, total }));
	});
}
src/index.ts
import { tracing } from "cloudflare:workers";

async function handleOrder(env: Env, orderId: string) {
	return tracing.enterSpan("handleOrder", async (span) => {
		span.setAttribute("order.id", orderId);

		// This KV read is automatically a child of "handleOrder"
		const order = await env.ORDERS_KV.get(orderId, "json");

		// This nested span is also a child of "handleOrder"
		const total = tracing.enterSpan("calculateTotal", (innerSpan) => {
			innerSpan.setAttribute("item.count", order.items.length);
			return order.items.reduce(
				(sum: number, item: any) => sum + item.price,
				0,
			);
		});

		// This fetch is a child of "handleOrder"
		await fetch("https://api.example.com/notify", {
			method: "POST",
			body: JSON.stringify({ orderId, total }),
		});

		return new Response(JSON.stringify({ orderId, total }));
	});
}
Каскадная диаграмма трассировки, показывающая пользовательские спаны рядом с автоматической инструментацией KV и fetch

Логирование в спанах

console.log() и другие методы console создают события логирования, которые автоматически привязываются к текущему активному спану. Это значит, что вывод логов внутри enterSpan или startActiveSpan обратный вызов связывается с этим спаном в ваших трассировках и экспорте OpenTelemetry.

tracing.enterSpan("processPayment", async (span) => {
	console.log("Starting payment processing"); // attributed to "processPayment"
	const result = await chargeCard(token, amount);
	console.log("Payment complete", result.id); // also attributed to "processPayment"
});

Типы TypeScript

Полные объявления типов для custom spans API:

declare module "cloudflare:workers" {
	namespace tracing {
		function enterSpan<T, A extends unknown[]>(
			name: string,
			callback: (span: Span, ...args: A) => T,
			...args: A
		): T;

		function startActiveSpan<T, A extends unknown[]>(
			name: string,
			callback: (span: Span, ...args: A) => T,
			...args: A
		): T;
	}

	class Span {
		readonly isTraced: boolean;
		setAttribute(
			key: string,
			value: string | number | boolean | undefined,
		): void;
		end(): void;
	}
}

Тот же API доступен в контексте обработчика как ctx.tracing, с теми же типами.

Выбор между enterSpan и startActiveSpan

enterSpan startActiveSpan
Span завершается Автоматически, когда колбэк возвращает значение, выбрасывает исключение или его promise переходит в завершённое состояние Вручную, при вызове span.end()
Активная область контекста Во время обратного вызова Во время обратного вызова
Сценарий использования Большая часть инструментирования: синхронная и асинхронная работа, укладывающаяся в один колбэк Операции, которые продолжаются дольше, чем сам колбэк, например потоковые конвейеры
Обработка ошибок Span автоматически завершается при выбросе исключения Span остаётся открытым при выбросе исключения: вызовите span.end() или полагаться на защитный механизм среды выполнения (runtime backstop)

Оба метода делают спан активным родительским контекстом только во время выполнения колбэка. После того как коллбэк завершает работу, span перестаёт быть активным родителем. При использовании enterSpan, это различие не имеет значения, так как спан тоже завершается. При использовании startActiveSpan, спан остаётся открытым, но перестаёт быть родительским контекстом: новые спаны, созданные после возврата из обратного вызова, не являются дочерними по отношению к этому спану.

Ограничения

Об остальных ограничениях трассировки см. в известные ограничения страницу.