Обзор
Трейс — одна сквозная операция: входящий запрос, задача очереди, запуск cron. Спан — один замеренный шаг внутри неё: запрос к базе, исходящий HTTP-вызов, рендер. Спаны вкладываются друг в друга и делят id трейса, так что панель может показать всю операцию водопадом.
Для HTTP-сервера большую часть работы делают две интеграции:
- HttpServerIntegration превращает каждый входящий запрос в трейс с корневым спаном
http.serverпо имениGET /users/42— или продолжает трейс вызывающей стороны, если запрос его несёт (см. Распределённый трейсинг); - HttpTracingIntegration записывает каждый исходящий запрос спаном
http.clientпод активным спаном.
Остальная часть страницы — о замерах собственного кода.
Спаны
Оберните код, который нужно замерить, в span() (синхронный) или asyncSpan() (возвращает Promise):
ts
import { asyncSpan, span } from '@buglapse/node'
const report = span('build-report', () => buildReport(rows))
await asyncSpan('db.query', async () => {
return db.query('select * from orders where id = $1', [id])
}, { op: 'db' })Оба возвращают то, что вернул колбэк, и завершают спан, когда он вернул значение, разрешился или бросил исключение. Спан, бросивший исключение, отправляется со статусом error до того, как ошибка пойдёт дальше. op — необязательная категория спана (db, cache, queue.process, ...).
Отдельного вызова «начать трейс» нет: спан, начатый без другого активного спана, становится корнем нового трейса.
Вложенные спаны
Спан, начатый внутри другого, становится его дочерним — id родителя передавать не нужно:
ts
await asyncSpan('checkout', async () => {
await asyncSpan('reserve-stock', () => reserveStock(cart))
await asyncSpan('charge-card', () => chargeCard(order))
})Получится один трейс: checkout с дочерними reserve-stock и charge-card. Node SDK хранит активный спан в AsyncLocalStorage, поэтому вложенность следует асинхронной цепочке вызовов — через await, таймеры и колбэки промисов, — а параллельные запросы никогда не видят спаны друг друга. Два одновременных запроса строят каждый своё дерево.
Статус и данные
Колбэк получает объект спана:
ts
await asyncSpan('sync-inventory', async (span) => {
const result = await syncInventory()
span.setData('inventory.provider', result.provider)
span.metric('inventory.items', result.count)
if (result.partial) span.setStatus('error')
})setStatus('ok' | 'error' | 'cancelled')— переопределяет статус; по умолчанию спанokилиerror, если колбэк бросил исключение.setData(key, value)— прикрепляет к спану любое значение как контекст.metric(name, value)— прикрепляет число, которое рисуется на графике на странице трейса. См. Метрики.
Листовые спаны для параллельной работы
startInactiveSpan() создаёт спан, который никогда не становится активным, — вы завершаете его сами, когда операция закончится. Используйте его для листовых операций, которым не нужны свои дочерние спаны, особенно когда запускаете несколько сразу:
ts
import { startInactiveSpan } from '@buglapse/node'
async function cached<T>(key: string, load: () => Promise<T>): Promise<T> {
const span = startInactiveSpan(`cache ${key}`, { op: 'cache' })
try {
const hit = await redis.get(key)
span.setData('cache.hit', hit !== null)
return hit !== null ? JSON.parse(hit) : await load()
} finally {
span.end()
}
}
await Promise.all([cached('user:42', loadUser), cached('plan:pro', loadPlan)])Родителем он берёт активный спан, так что внутри запроса попадает в трейс запроса. Спаны, начатые внутри него через span()/asyncSpan(), не считают его родителем. end() можно вызывать больше одного раза.
Так HttpTracingIntegration записывает исходящие запросы.
Трейсы для задач и скриптов
Всё, что не является входящим HTTP-запросом, становится трейсом, как только вы оборачиваете его в корневой спан:
ts
import { asyncSpan } from '@buglapse/node'
queue.process('send-invoice', async (job) => {
await asyncSpan(`job ${job.name}`, () => sendInvoice(job.data), { op: 'queue.process' })
})Каждый вызов начинает свой трейс — чтобы вместо этого продолжить трейс того, кто поставил задачу, см. Передача вручную. Код вне любого спана — при запуске, в таймере верхнего уровня — в трейс не входит, и события оттуда не несут trace.id.
Связь других событий с активным спаном
Пока спан активен, каждое событие, пойманное в его асинхронном контексте, — исключение, лог, вложенный спан — получает trace.id и span.id (самого внутреннего активного спана):
ts
import { asyncSpan, captureException, log } from '@buglapse/node'
await asyncSpan('checkout', async () => {
log.info('Starting checkout') // связан с трейсом
try {
await chargeCard(order)
} catch (error) {
captureException(error) // тоже связан с трейсом
}
})Благодаря этому панель связывает исключение или лог с трейсом, в котором оно произошло. С HttpServerIntegration всё, что поймано при обработке запроса, связано с трейсом этого запроса.
Распределённый трейсинг
Запрос, проходящий через несколько сервисов — страницу, API, который она вызывает, сервис за ним, — становится одним трейсом, если каждый сервис передаёт трейс следующему. SDK Buglapse делают это заголовками W3C Trace Context:
traceparent: 00-{trace id}-{span id}-{01|00}— трейс, вызывающий спан и записывается ли трейс;tracestate: buglapse=sr:{rate}— частота сэмплирования, с которой трейс был сохранён, чтобы спаны всех сервисов имели одинаковый вес.
С двумя HTTP-интеграциями больше ничего не нужно:
- HttpTracingIntegration добавляет заголовки к каждому исходящему запросу, указывая его спан
http.client; - HttpServerIntegration читает их из каждого входящего запроса, и её спан
http.serverстановится дочерним для спана вызывающей стороны.
Каждый сервис отправляет данные в свой проект. Проекты одной организации склеиваются при открытии трейса: водопад показывает каждый спан с проектом, который его записал, из какого бы проекта вы его ни открыли. Проекты другой организации не включаются никогда.
Сэмплирование между сервисами
Решает сервис, начавший трейс: продолженный трейс берёт решение вызывающей стороны, а не бросает tracesSampleRate заново, так что трейс записывают все сервисы или ни один — кроме сервиса с tracesSampleRate: 0, который не записывает никогда. Ошибки и логи незаписанного трейса всё равно несут его trace.id.
Частота сэмплирования трейсов проекта применяется сервером по id трейса, так что два проекта с разными частотами сохраняют вложенные множества: трейс, сохранённый более строгим проектом, сохраняет и другой.
Передача вручную
Для всего, что не HTTP-запрос, — сообщения очереди, кадра WebSocket, cron, передающего работу воркеру, — передавайте заголовки сами:
ts
import { asyncSpan, continueTrace, traceHeaders } from '@buglapse/node'
// Отправитель: внутри спана, к которому относится сообщение
await queue.add('send-invoice', { orderId, trace: traceHeaders() })
// Получатель
queue.process('send-invoice', (job) =>
continueTrace(job.data.trace, () =>
asyncSpan('job send-invoice', () => sendInvoice(job.data), { op: 'queue.process' })))traceHeaders() возвращает { traceparent, tracestate } активного спана (или переданного спана — контроллера из startInactiveSpan()). continueTrace(headers, fn) выполняет fn внутри этого трейса: первый спан, начатый в нём, становится дочерним для спана отправителя. Принимает { traceparent, tracestate, sentryTrace, baggage }; если в нём нет ничего пригодного, fn просто выполняется и начинает собственный трейс.
Сервисы на Sentry SDK
Сервис на Sentry SDK, который отправляет в Buglapse, участвует в тех же трейсах:
- Входящие —
continueTrace()(а значит, и HttpServerIntegration) понимает и заголовок Sentrysentry-trace, с частотой сэмплирования изbaggage. Фронтенд на Sentry SDK продолжается в бэкенд на@buglapse/nodeбез настройки. - Исходящие —
init({ propagateSentryTrace: true })добавляетsentry-traceиbaggageрядом сtraceparent/tracestate, для сервиса на Sentry SDK за вашим.
Сэмплирование
tracesSampleRate сохраняет случайную долю трейсов:
ts
init({ dsn: process.env.BUGLAPSE_DSN, tracesSampleRate: 0.1 })Решение принимается один раз на трейс, так что трейс приходит со всеми спанами или не приходит вовсе. Ошибки и логи из несэмплированного трейса всё равно отправляются и несут его trace.id.
Частота сэмплирования трейсов проекта (Настройки → Фильтры) применяется сервером поверх этого, для любого SDK: она тоже решает по traceId, так что трейсы остаются целыми, а изменение действует со следующей пачки без передеплоя. Оставьте tracesSampleRate равным 1 и пользуйтесь настройкой проекта — снижайте опцию SDK, только чтобы сэкономить трафик на своей стороне. Отсеянные спаны не тарифицируются; они показываются как Sampled в Статистике и использовании, а число трейсов на обзоре экстраполируется по сохранённым трейсам.
Квота
Трейсинг тарифицируется по спанам: каждый отправленный спан засчитывается как один в месячный лимит спанов. Сам трейс ничего не стоит — это не отдельное событие, а просто traceId, общий для его спанов. Когда лимит спанов достигнут, новые спаны отклоняются, и трейсинг останавливается до следующего месяца. Метрики спанов едут на своём спане и ничего не стоят — см. Метрики.
Трейсы в панели
Раздел Трейсы показывает по строке на корневой спан. Открытый трейс показывает дерево спанов водопадом (перетаскивание — масштаб, прокрутка — сдвиг) с длительностью, именем и метриками каждого спана. ИИ-клиенты получают то же через MCP-сервер Buglapse — инструмент get-trace.