Разделы

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

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

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

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

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

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

  • Выполнения — в списке выберите запуск воркспейса и откройте карточку: полный таймлайн, статус, длительность, полезная нагрузка или детали ошибки (если платформа их сохранила).
  • Редактор функции — в правой тестовой панели отображаются недавние запуски этой функции; по записи можно перейти к тому же детальному виду выполнения.
  • В одной трассировке объединены встроенные шаги оркестратора ниже и, при использовании 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.warnWARN, console.errorERROR, а console.debugDEBUG. Чтобы явно задать уровень и приложить структурированный объект 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-событий (пользовательские)

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

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

Оркестратор сопоставляет строку spanType с сохраняемым типом шага. activityactivity; generation, llm, aigeneration (такие шаги всегда остаются в трассе, даже если очень короткие); tooltool; retrieval, database, dbactivity; остальные известные имена (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.js
// 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.py
# 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.go
// 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
}

Распределённая трассировка (<code>traceparent</code>)

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

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

Поля LLM в трассах (control plane)

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

observability.service.ts (host)
// 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

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

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