Наблюдаемость в serverless: трейсы, логи и история выполнения
К эфемерным контейнерам не подключиться по SSH. Разбираем, как история выполнения на каждый вызов — трейсы, структурированные логи, число повторов и хранение 30 дней — позволяет отладить упавший вебхук, ставить алерты на долю ошибок и отвечать на «что случилось с запросом X?» одним обращением по ключу.
Наблюдаемость serverless начинается там, где заканчивается console.log
Когда функция работает на вашем собственном сервере, у отладки есть привычная опора: зайти по SSH, сделать tail -f на лог-файле, grep по id запроса. Serverless этого лишает. Ваш обработчик выполняется в изолированном контейнере, который создаётся по требованию, может быть переиспользован примерно после 1000 вызовов и вытесняется, как только начинает простаивать. К моменту, когда вы услышите «вебхук Stripe упал в 09:14», контейнера, который его обслуживал, уже нет. Нет файла, чтобы сделать tail, и нет процесса, к которому можно подключиться.
Именно поэтому наблюдаемость (observability) в serverless — это возможность платформы, а не то, что прикручивают потом. Вместо того чтобы писать строки в stdout и надеяться, что их кто-то соберёт, каждый вызов записывается как структурированная запись истории выполнения (execution history) — запись о вызове со статусом, длительностью, входом и выходом, строками логов и числом повторов. Отладка перестаёт быть «grep по N логам контейнеров» и становится «открыть запись этого запуска». Контейнер эфемерен; трейс — нет.
Дальше в этой статье — о том, что именно содержит такая запись в Inquir Compute, как писать логи в обработчике так, чтобы они оставались полезными внутри неё, и как отвечать на два вопроса, которые вы реально задаёте во время инцидента: что случилось с конкретным запросом и растёт ли доля ошибок прямо сейчас.
Что записывает serverless-трейс: модель истории выполнения
Запись о запуске (run record) — это атомарная единица трейсинга serverless здесь. Для каждого вызова — пришёл он из API-шлюза, cron-триггера, вебхука или другого задания — платформа пишет одну запись со следующими полями:
- status — одно из
RUNNING,SUCCEEDED,FAILED,TIMED_OUTилиCANCELLED. Это первое, по чему вы фильтруете. - durationMs — фактическое время выполнения, чтобы видно было медленные запуски.
- logs[] — строки логов, которые выдал ваш обработчик, каждая с меткой
INFO,WARN,ERRORилиDEBUG. - steps[] — для пайплайнов вложенное дерево шагов, у каждого свой input, output, ошибка и длительность.
- attempts[] — по одной записи на попытку, так что число повторов и причина каждого сбоя видны прямо в записи.
- метаданные генерации — для LLM-шагов: модель, провайдер, количество входных/выходных/суммарных токенов и
costUsd.
Страница возможностей показывает компактный trace.json с runId, functionName, status, durationMs, startedAt, массивом logs и output — это ровно та форма, которую вы читаете. Записи хранятся 30 дней, и это ваше практическое окно для «поднять сбои прошлого вторника». Два лимита хранения стоит держать в голове заранее: хранимая запись лога/вызова ограничена примерно 16 КБ, а результирующий JSON — примерно 64 КБ символов. Трейсы — для диагностики, а не для складирования больших полезных нагрузок.
Поскольку каждая запись несёт durationMs и status, история — это ещё и то, откуда берутся агрегированные представления: консоль показывает перцентили задержки (p50/p95/p99) и показатель доли успешных запусков по недавним вызовам.
Структурированное логирование serverless прямо из обработчика
Самая окупаемая привычка в логировании serverless — писать структурированные строки, а не прозу. Обработчик имеет форму (event, context); шлюз передаёт вам событие в стиле API Gateway с полями httpMethod, path, headers, queryStringParameters и body (строка или null). Всё, что вы пишете через console.*, попадает в logs[] этого запуска с уровнем, поэтому небольшая дисциплина окупается каждый раз, когда вы позже читаете трейс.
// stripe-webhook — Node.js 22, handler shape (event, context)
exports.handler = async (event, context) => {
// gateway event: httpMethod, path, headers, body (string | null)
const payload = JSON.parse(event.body ?? '{}');
// console.* is captured into the run's logs[] with a level
console.log(JSON.stringify({ at: 'received', type: payload.type, id: payload.id }));
try {
const order = await fulfill(payload);
console.log(JSON.stringify({ at: 'fulfilled', charge: order.chargeId, amount: order.amount }));
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
} catch (err) {
// console.error → ERROR level; throwing records the run as FAILED
console.error(JSON.stringify({ at: 'fulfill_failed', id: payload.id, error: err.message }));
throw err;
}
};
Окупается это по двум причинам. Во-первых, нормальный return записывает запуск как SUCCEEDED, а выброс исключения — как FAILED, так что поток управления и статус в трейсе согласованы без дополнительного учёта. Во-вторых, чувствительные значения редактируются (маскируются) на пути в логи и трейсы: ключи и шаблоны вроде password, secret, token, api-key и Bearer … затираются, поэтому структурированное логирование целого объекта не утечёт учётку в 30 дней истории. Логируйте идентификаторы и суммы, по которым будете искать; то, что хранить не нужно, платформа вычистит.
Читаем трейс, чтобы отладить упавший вебхук
Теперь про разбор постфактум — ведь к наблюдаемости вообще обращаются ради того, чтобы отлаживать serverless-функции, к которым уже не подключиться. Провайдер сообщает, что доставка не удалась; у вас есть run id (или вы фильтруете историю выполнения по status = FAILED в нужном окне). Вы открываете запись и читаете её сверху вниз:
{
"runId": "run_01hw9m3k7q8fz...",
"functionName": "stripe-webhook",
"status": "FAILED",
"durationMs": 842,
"startedAt": "2026-07-01T09:14:02.880Z",
"attempts": [
{ "n": 1, "status": "FAILED", "error": "HTTP 502 from orders-api" }
],
"logs": [
{ "level": "INFO", "message": "{\"at\":\"received\",\"type\":\"charge.succeeded\",\"id\":\"evt_1P...\"}" },
{ "level": "ERROR", "message": "{\"at\":\"fulfill_failed\",\"id\":\"evt_1P...\",\"error\":\"HTTP 502 from orders-api\"}" }
],
"output": null,
"error": "HTTP 502 from orders-api"
}
Запись отвечает на вопросы по порядку. Он вообще пришёл? Да — запуск есть, startedAt 09:14:02. Мы его приняли? Первая строка INFO показывает, что полезная нагрузка распарсилась как charge.succeeded. Где сломалось? Строка ERROR и поле error верхнего уровня сходятся: нижестоящий orders-api вернул 502. Насколько упорно мы пытались? В attempts[] одна запись — у этой функции не были настроены повторы, поэтому она упала с первой попытки за 842 мс. Это диагностика на пять секунд, и для неё не потребовалось, чтобы контейнер, выполнявший код, всё ещё существовал.
Сравните с миром stdout, где вы бы делали grep по перемешанному выводу всех параллельных вызовов в поисках id запроса, который, может, и не был напечатан. Запись на каждый вызов — это разница между поиском и обращением по адресу.
«Что случилось с запросом X?» становится обращением по ключу
Разбор упавшего вебхука обобщается. Поскольку каждый вызов — это отдельная запись с ключом run id, «что случилось с запросом X?» превращается в прямое обращение по ключу, а не в текстовый поиск по потоку логов. У вас либо есть этот id — верните его в ответе или залогируйте как корреляционное поле, — либо вы фильтруете историю выполнения по функции, статусу и окну времени и открываете нужную запись.
Несколько возможностей строятся прямо на этом:
- Replay (повтор) — заново вызвать записанный запуск с его исходным входом (
POST /observability/runs/:id/replay). Идеально для «а мой фикс действительно обработал эту нагрузку?». Поскольку replay — это новое выполнение, держите обработчики идемпотентными: платформа не обещает exactly-once, поэтому повтор не должен списать деньги дважды. - Cancel (отмена) — остановить запуск, который ещё в статусе
RUNNING. - Сравнение запусков — поставить два запуска рядом и увидеть, что изменилось между успешным и сбойным.
- Живой стриминг — потоки SSE по отдельному запуску и по всем активным позволяют смотреть, как логи приходят в реальном времени; это ближайший аналог
tail -f, когда вы намеренно воспроизводите проблему.
Сдвиг в модели мышления и есть суть: логи serverless больше не пожарный шланг, из которого вы черпаете выборку, — это строки, к которым вы обращаетесь по адресу.
Алерты на долю ошибок и наблюдение за задержкой
Чтение трейсов реактивно. Вторая половина наблюдаемости serverless — узнавать раньше, чем пойдёте искать. Поскольку каждый запуск ложится записью со status и durationMs, платформа может следить за этими агрегатами и оповещать через email, Slack или вебхук. Естественный сигнал — доля ошибок: рост числа FAILED / TIMED_OUT для конкретной функции — это ровно та сигнализация «что-то отрегрессировало», которую вы хотите в два часа ночи, а маршрутизация в вебхук означает, что можно завести оповещение обратно в пайплайн для автоматической разборки.
Задержка — сопутствующий сигнал. durationMs каждой записи сворачивается в перцентильное представление — p50, p95, p99 — которое консоль показывает рядом с долей успешных. p50 говорит о типичном опыте; p99 — о хвосте, который простая синтетическая проверка пропустит. Когда p99 растёт, а p50 держится ровно, вы обычно смотрите на холодный старт или медленный нижестоящий сервис на части запросов — и это можно подтвердить, открыв самые медленные запуски в истории и прочитав длительности их шагов.
Одна честная оговорка: это операционные сигналы, выведенные из вашей истории запусков, а не универсальный продукт для метрик. Вы оповещаете о форме своих выполнений — сбои, таймауты, длительности, — а не определяете произвольные кастомные дашборды.
Трейсинг serverless по шагам пайплайна и SDK-спаны
Для всего, что сложнее одной функции, трейс становится деревом. Запуск пайплайна записывает steps[] как вложенную структуру, и каждый шаг несёт свой input, output, ошибку и длительность, записанные по мере выполнения. Именно это делает многошаговое задание отлаживаемым: когда ночной ETL или пайплайн, запущенный вебхуком, падает, вы узнаёте не только что он упал — вы видите, какой шаг упал, что он получил на вход и что вернул или выбросил, и не передал ли ранний шаг тихо неправильные данные дальше. Если у шага были настроены повторы на узле (maxAttempts, backoffMs, fixed или exponential), каждая попытка отображается, так что видно, сколько раз он отступал по backoff, прежде чем сдаться.
Для LLM-шагов запись дополнительно несёт метаданные генерации — модель, провайдер, входные/выходные/суммарные токены и costUsd — так что трейс агента говорит не только о том, что вернула модель, но и сколько стоил вызов. Удивительная доля расследований «почему этот пайплайн такой дорогой?» разрешается прямо здесь.
Наконец, трейсинг не обязан останавливаться на границе платформы. Приём наблюдаемости принимает W3C traceparent, поэтому спаны, которые ваше приложение или SDK уже испускают, можно сшить в тот же контекст запуска, а не держать в отдельном инструменте. Это и есть часть про «SDK-спаны»: ваша существующая инструментация и записи платформы на каждый вызов описывают одно выполнение, а не два.
Честные ограничения и вывод
Что эта система есть: устойчивая, структурированная история выполнения — записи на каждый вызов со статусом, длительностью, структурированными логами, пошаговыми трейсами, попытками повторов и метаданными стоимости LLM, доступные для запроса 30 дней, стримящиеся вживую и связанные с алертами. Чем она не является — и здесь честность важна:
- Хранение — 30 дней. Если нужна годовая история для аудита, экспортируйте то, что важно; хранилище трейсов — операционное окно, а не архив.
- Записи ограничены по размеру. Хранимая запись лога/вызова — около 16 КБ, результирующий JSON — около 64 КБ символов. Логируйте идентификаторы и сводки, а не целые тела запросов или контексты модели.
- Replay — это перезапуск, а не путешествие во времени. Он заново вызывает с записанным входом; нет exactly-once и нет гарантированного порядка, так что идемпотентность обработчиков остаётся на вас.
- Это наблюдаемость вызовов, а не таймлайн устойчивой оркестрации. Трейсы показывают, что сделало каждое выполнение; это не event-sourced журнал воркфлоу, из которого можно детерминированно возобновиться.
- Это включается опционально. Захват наблюдаемости управляется флагом платформы (
ENABLE_OBSERVABILITY), так что убедитесь, что он включён для вашего воркспейса.
Вывод для старшего инженера: относитесь к записи о запуске как к главному артефакту отладки и проектируйте под неё. Возвращайте или логируйте корреляционный id, пишите структурированный JSON на границах (получено / решено / упало), позвольте платформе редактировать секреты за вас и повесьте алерт на долю ошибок на всё, что смотрит наружу к провайдеру или в расписание. Сделайте так — и эфемерная, без-SSH природа serverless перестанет быть помехой отладке и станет причиной, по которой вы отвечаете на «что случилось с запросом X?» одним обращением по ключу — ровно тем, чего нельзя сделать через grep по stdout.