Перейти к содержимому
Документация

Наблюдаемость и трассировка

Каждый запуск даёт логи, историю выполнения, структурированные трассировки, разбивку задержек и, при необходимости, спаны из SDK — отлаживайте трафик шлюза, пайплайны с запусками по расписанию и фоновые задачи в одном месте.

Каждый вызов (ручной запуск, запрос через API Gateway, задание, шаг пайплайна и т. д.) создаёт трассировку: упорядоченный таймлайн работы оркестратора, при необходимости — ваши спаны из кода, структурированные логи и длительности шагов, чтобы отладить один прогон от начала до конца.

Трассировка привязана к одному идентификатору запуска. Она дополняет метрики на странице Функции: видно, что произошло внутри конкретного выполнения, а не только факт успеха или ошибки.

Где смотреть трассировки#

Выберите удобный экран в зависимости от задачи:

  • Запуски — в списке выберите запуск рабочего пространства и откройте карточку: полная временная шкала, статус, длительность, входные данные или подробности ошибки (если платформа их сохранила).
  • Редактор функции — в правой тестовой панели отображаются недавние запуски этой функции; по записи можно перейти к тому же детальному виду выполнения.
  • В одной трассировке объединены встроенные шаги оркестратора ниже и, при использовании SDK, ваши спаны observe.span и строки observe.log на общей шкале времени.

Что показывает таймлайн#

Шаги показываются по порядку с длительностью каждого. Родительские спаны оборачивают вложенную работу: с одного взгляда видно холодный старт, время обработчика и вложенные вызовы LLM.

При ошибке сохраняются спаны до точки сбоя; у проблемного шага фиксируется сообщение об ошибке, если оно доступно. При успехе отображаются выходные данные там, где среда выполнения их записала, — вывод спана записывают все три среды выполнения: Node.js автоматически (отключается через captureOutput: false), Python через set_output на контексте спана, а Go через значение, переданное в End.

Автоматические шаги оркестратора#

Перед запуском хендлера платформа записывает инфраструктурные шаги для каждого вызова:

  • Build Environment — сборка переменных окружения и конфигурации среды выполнения
  • Setup Layers — подготовка файлов среды выполнения платформы рядом с вашим кодом
  • Create Container — создание Docker-контейнера (только холодный старт)
  • Acquire Container — выбор прогретого контейнера (горячий старт)
  • Execute Function — непосредственное выполнение функции

Вывод в консоль#

Когда наблюдаемость активна для запуска, вывод console в Node.js также попадает в трассировку как строки лога (в дополнение к stdout процесса): console.log и console.info дают уровень INFO, console.warn — WARN, console.error — ERROR, а console.debug — DEBUG. Чтобы явно задать уровень и приложить структурированный объект metadata, удобнее observe.log.

Протокол передачи (__OBSERVABILITY__)#

Пользовательские спаны и структурированные логи выводятся из контейнера в stderr: по одной строке UTF-8 на событие. Строка должна начинаться с префикса __OBSERVABILITY__: (с двоеточием), сразу за ним — один JSON-объект. Оркестратор разбирает поток stderr Docker и извлекает эти строки; ответ функции уходит в stdout, поэтому события observe нужно писать только в stderr.

Используйте имена свойств в camelCase ровно как в таблице ниже. Управляющая плоскость сопоставляет span_end с открытым шагом по паре runId + spanId; дублирующиеся spanId в одном запуске испортят таймлайн.

Вложенность: каждый span_start открывает шаг, а связь «родитель — потомок» задаётся явно: событие несёт parentSpanId, который управляющий узел сопоставляет со spanId родителя. SDK для Node.js и Python отправляют parentSpanId автоматически; среда выполнения, которая его не передаёт (SDK для Go), использует прежнее правило «последний открытый спан». На каждый открытый спан нужен span_end — встроенные SDK отправляют его и при успехе, и при ошибке.

Снимок входа шага: при приёме, если в span_start задан input, сохраняется он; иначе, когда input пуст, используется metadata (его шлют SDK и Node.js, и Python). Для чисто именованных спанов оба поля можно опустить. Захваченные вход и выход шага усекаются до 32 КиБ (хост и SDK Node.js) или 8 КиБ (SDK Python), с маркером превышения вместо значения.

В собственной среде выполнения можно формировать те же JSON-строки. Некорректный JSON после префикса отбрасывается; неполные строки нужно дописать в следующем выводе, чтобы сохранить правило «один объект на строку».

Формы JSON-событий (пользовательские)#

Поле typeОбязательные и необязательные поля
span_startspan_start — type (литерал), runId, spanId (уникальная строка в запуске), необязательный parentSpanId (охватывающий спан, для явной вложенности), name, spanType, необязательный input (объект JSON), необязательный metadata (объект, Node.js и Python), timestamp (ISO-8601).
span_endspan_end — type, runId, spanId (как при старте), name, spanType, success (логическое), duration (целые миллисекунды), timestamp, необязательный output (JSON-совместимый), необязательный error (строка), если success равно false.
loglog — type, runId, level (например INFO), message (строка), необязательный metadata, timestamp. Если level нет, на сервере подставляется INFO.

Как сохраняется spanType#

Оркестратор сопоставляет строку spanType с сохраняемым типом шага. activity → activity; generation, llm, ai → generation (такие шаги всегда остаются в трассировке, даже если очень короткие); tool → tool; retrieval, database, db → activity; остальные известные имена (orchestrator, wait, timer и т.д.) маппятся напрямую. Неизвестные имена дают обобщённый шаг function.

SDK: пользовательские спаны и логи#

Оборачивайте осмысленные участки кода именованными спанами, чтобы временная шкала отражала структуру приложения. SDK уже есть в образе среды выполнения — не добавляйте его в package.json, requirements.txt или Go-модули.

Среда выполненияИмпорт
Node.js 22global.observe (внедряется автоматически)
Python 3.12from observability import observe
Go 1.22import obs "lambda/runtime/go122/observability"

В Node.js observe.span(name, fn, options) вызывает fn без аргументов и по умолчанию захватывает её возвращаемое значение как выход спана — чтобы отключить, передайте captureOutput: false в options. Для ручного управления (несколько выходов, частичный результат) используйте observe.startSpan(name, options) и затем .end({ output, error }).

В Python используйте with observe.span(...) как контекстный менеджер. Объект из yield поддерживает set_output(...) для необязательного структурированного вывода при закрытии спана.

В Go пакет даёт низкоуровневые StartSpan / End и Log; значение, переданное в End, записывается как выход спана. При необходимости оборачивайте их в свои хелперы для вложенных спанов, как в Node или Python.

Типы спанов (семантика)#

Задайте type (Node) или span_type (Python), чтобы интерфейсы и будущие инструменты могли классифицировать спаны. Все типы проходят через один и тот же конвейер записи.

ТипНазначение
spanОбщая работа: валидация, маппинг, внутренние вспомогательные функции
activityI/O: HTTP-клиенты, БД, очереди, чтение файлов
generationВызовы LLM, эмбеддинги, шаги с тяжёлым токенайзером

Node.js#

index.jsjs
// Node.js — global observe object injected by the runtime
// observe.span(name, fn, options) calls fn with no arguments and captures its return
// value as the span output by default (pass captureOutput: false to opt out).
exports.handler = async (event, context) => {
  return await observe.span(
    'Fetch data',
    async () => {
      const data = await fetchSomething();

      const result = await observe.span(
        'Call LLM',
        async () => callLLM(data),
        { type: 'generation', input: { model: 'gpt-4o' } },
      );

      observe.log('INFO', 'Pipeline complete', { records: data.length });
      return { records: data.length, result };
    },
    { type: 'activity', captureOutput: true },
  );
};

Python#

handler.pypython
# Python — import from the runtime SDK
from observability import observe

async def handler(event, context):
    with observe.span('Fetch data', span_type='activity') as s:
        data = await fetch_something()
        s.set_output({'records': len(data)})

        with observe.span('Call LLM', span_type='generation'):
            result = await call_llm(data)

    observe.log('INFO', 'Pipeline complete', {'records': len(data)})
    return result

Go#

main.gogo
// Go — import the runtime observability package
import obs "lambda/runtime/go122/observability"

func Handler(event, ctx map[string]interface{}) (interface{}, error) {
    span := obs.StartSpan("Fetch data")
    data, err := fetchSomething()
    if err != nil {
        span.End(nil, err.Error())
        return nil, err
    }
    span.End(map[string]interface{}{"records": len(data)}, "")

    obs.Log("INFO", "Pipeline complete", map[string]interface{}{"records": len(data)})
    return data, nil
}

Распределённая трассировка (traceparent)#

Если клиент передаёт заголовок W3C traceparent при синхронном вызове, потоковом вызове или запросе через API Gateway, платформа может связать корневой спан запуска с внешней трассировкой (если для развёртывания включена распределённая трассировка). Сам заголовок не сохраняется; в метаданных запуска сохраняются upstreamTraceId и upstreamParentSpanId для связи в интерфейсе.

Чтобы связать с вашим сервисом, пробрасывайте тот же traceparent, что используете дальше по цепочке. Для асинхронных заданий можно задать context.__traceparent или context.traceparent в теле invoke-async; HTTP-заголовок копируется в context.__traceparent, если в теле ещё не указана связь. Некорректные или отсутствующие значения игнорируются.

Поля LLM в трассировках (управляющий узел)#

Интеграции, вызывающие ObservabilityService на хосте, могут добавлять к шагам метаданные generation: модель, провайдер, число токенов, оценка стоимости и необязательные параметры. Тип шага generation для LLM-работы всегда остаётся в трассировке, даже если шаг очень короткий. Любой шаг может задать retainInTrace: true, чтобы не отфильтровываться по минимальной длительности.

observability.service.ts (host)typescript
// Control plane (TypeScript) — ObservabilityService
const step = observability.addStep(runId, {
  name: 'OpenAI chat',
  type: 'generation',
  input: { messages },
  generation: { model: 'gpt-4o', provider: 'openai' },
});

observability.completeStep(runId, step.id, completion, undefined, durationMs, {
  generation: {
    inputTokens: usage.prompt_tokens,
    outputTokens: usage.completion_tokens,
    totalTokens: usage.total_tokens,
    costUsd: 0.0012,
  },
});

// Keep a very fast step in the trace (optional):
observability.addStep(runId, {
  name: 'Fast cache hit',
  type: 'activity',
  retainInTrace: true,
});

При завершении шага передайте шестой аргумент в completeStep — { generation: { … } }, — чтобы дописать использование токенов и стоимость после ответа модели. Значения проходят через тот же конвейер скрытия секретов, что и остальные данные трассировок.

Карточка выполнения#

На вкладке трассировки запуска:

  • Compare — вход и выход выбранного шага рядом, в двух колонках JSON.
  • Generation — при наличии метаданных generation показывается строка с моделью, провайдером, токенами и стоимостью.
  • Поиск — фильтрует дерево по имени шага и по полям generation.
  • Upstream trace — если traceparent был принят, отображается метка с идентификатором внешней трассировки.

Структурированные логи#

observe.log(level, message, metadata) пишет структурированную запись в трассировку (не только в stdout). Уровень — строка без учёта регистра, например INFO, WARN, ERROR, DEBUG.

Трассировки хранятся 30 дней по умолчанию.

Повторные попытки и dead-letter#

Асинхронные вызовы выполняются в надёжной очереди на Postgres. Задача может задать maxAttempts; неудачные попытки повторяются с экспоненциальной задержкой. Доставка — at-least-once, а не exactly-once, поэтому делайте обработчики идемпотентными; ключ идемпотентности убирает дубли при повторной отправке той же задачи.

Когда попытки исчерпаны, задача переходит в состояние dead-letter (DEAD), а не исчезает. На странице «Выполнения» можно просмотреть упавшие запуски, повторить запуск (по одному или пакетом) и вернуть в очередь (requeue) задачу из dead-letter.