← Cloudflare Workers / workers / observability / traces
Пользовательские спаны
Cloudflare Workers автоматически инструментирует операции платформы, такие как вызовы fetch, чтение KV и запросы D1. Пользовательские спаны позволяют расширить эту видимость на логику вашего приложения, чтобы отслеживать собственные пути выполнения кода наряду со встроенной инструментацией.
API пользовательских spans доступен двумя способами: оба предоставляют одинаковые методы и ведут себя одинаково:
import { tracing } from "cloudflare:workers": работает в любом месте кодовой базы, включая вспомогательные функции, библиотеки и модули, у которых нет доступа к контексту обработчика.ctx.tracing: доступен вExecutionContextпередаётся в обработчик, что удобно, если вы уже работаете внутри обработчика.
Есть два метода создания спанов:
enterSpan(): создаёт span, который автоматически завершается, когда callback возвращает значение или его промис разрешается. Используйте это в большинстве случаев для инструментирования.startActiveSpan(): создаёт span, который нужно завершить вручную, вызвавspan.end(). Используйте это, когда span должен существовать дольше, чем callback, например при инструментировании потоков или других долгоживущих операций.
Включить трассировку
Для пользовательских спанов необходимо включить трассировку для 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 : чтобы показать, что они взаимозаменяемы:
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);
});
},
};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.
Поведение:
- Новый спан становится дочерним по отношению к тому спану, который в данный момент активен в асинхронном контексте. Если активного спана нет, он становится дочерним по отношению к корневому спану запроса.
- Вложенные
enterSpanвызовы и спаны, создаваемые средой выполнения (например,fetchили операции с KV), выполняемые внутри обратного вызова, автоматически становятся дочерними элементами этого спана. - Span завершается, когда обратный вызов возвращает значение синхронно, выбрасывает исключение синхронно, либо когда возвращённый им промис выполняется или отклоняется.
// 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.
Поведение:
- В отличие от
enterSpan, спан не автоматически завершается, когда обратный вызов возвращает значение или выбрасывает исключение. Вы сами отвечаете за вызовspan.end(). - Если вы забудете вызвать
span.end(), спан всё равно отправляется при уничтожении объекта спана, принадлежащего запросу, в качестве защитного механизма. Не полагайтесь на это поведение, всегда вызывайтеspan.end()явно.
Используйте startActiveSpan когда span должен охватывать операцию, выходящую за рамки одного колбэка: например, при инструментировании конвейера потока, где span должен оставаться открытым до полного считывания потока:
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);
},
};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 submittedSpan
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 вызовы молча игнорируются, включая вызовы из еще не завершенных асинхронных операций.
- Для спанов, созданных с помощью
enterSpan, вам не нужно вызыватьend(): среда выполнения вызывает его автоматически. Вызовend()самостоятельно безопасен, но не имеет эффекта, так как среда выполнения уже завершила span. - Для спанов, созданных с помощью
startActiveSpan, вы обязательно вызовend()чтобы отправить span.
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.
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 }));
});
}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 }));
});
}
Логирование в спанах
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, спан остаётся открытым, но перестаёт быть родительским контекстом: новые спаны, созданные после возврата из обратного вызова, не являются дочерними по отношению к этому спану.
Ограничения
- Ручное связывание родитель-потомок не требуется. Отношения «родитель-потомок» определяются автоматически на основе асинхронного контекста JavaScript.
- Нет
setAttributes(массовую установку) пока нет. Используйте отдельныеsetAttributeвызовы. Массовая установка запланирована для будущего релиза. - Нет
spanContext()(идентификаторы trace/span) пока нет. Доступ к идентификаторам трассировки и спанов для ручного распространения между границами запланирован в одном из будущих релизов. - Нет
setOutcomeпока нет. Возможность задавать статус результата span запланирована для будущего релиза.
Об остальных ограничениях трассировки см. в известные ограничения страницу.