Записывайте каждый запуск как спан

PHP

Спан, который SDK пишет на каждый запуск и каждое ожидание, — его поля, куда он уходит и как попадает в ваши логи или трассировки.

6 мин чтения

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

Дать SDK записывающий

Записывающий ниже пишет каждый спан в лог. Логгер здесь стоит вместо логгера вашего приложения.

PHP
use Psr\Log\AbstractLogger;
use Slen\Sdk\Observability\Psr3SpanRecorder;

// Stands in for the PSR-3 logger of your application: it prints the fields this page talks about.
$logger = new class extends AbstractLogger {
    public function log($level, string|\Stringable $message, array $context = []): void
    {
        echo "{$message}: {$context['operation']} {$context['outcome']} {$context['status']} {$context['operation_code']} {$context['execution_id']} {$context['estimated']}", PHP_EOL;
    }
};

$recorder = new Psr3SpanRecorder($logger);
PHP
use Slen\Sdk\SlenConfig;
use Slen\Sdk\SlenSdk;

$slen = SlenSdk::create(new SlenConfig(
    apiKey: (string) getenv('SLEN_API_KEY'),
    spanRecorder: $recorder,
));
PHP
use Slen\Sdk\Modules\Operations\Options\ExecuteOptions;

$execution = $slen->operations()->execute(
    'generate_email',
    ['messages' => [['role' => 'user', 'content' => 'Write a short follow-up email to Ada.']]],
    new ExecuteOptions(idempotencyKey: 'order-61:email'),
);

Запуск пишет один спан, и логгер печатает:

slen.span: operations.execute ok completed generate_email 74438200973594710 0.00061590

В SDK есть такие записывающие:

  • Psr3SpanRecorder пишет спан сообщением slen.span с его полями в контексте через логгер PSR-3, на уровне info, если вы не назвали другой. Неизвестный уровень отклоняется с InvalidArgumentException при сборке записывающего.

  • RecordingSpanRecorder держит спаны в памяти, для теста.

Чтобы отправлять спаны куда-то ещё, реализуйте SpanRecorderInterface: у него один метод, record(). В приложении на Laravel записывающий выбирает файл конфигурации SDK — Интеграция с Laravel.

Записывающего вызывают, пока ваш вызов ещё возвращается к вам. То, что он выбрасывает, проглатывается: сломанный записывающий никогда не ломает запуск. Держите record() быстрым и отправляйте спаны дальше в фоне.

Поля спана

У спана десять полей, одинаковых в обоих SDK и в этом порядке. Строка лога и спан OpenTelemetry несут их как атрибуты, под именами второй колонки, и опускают поле, равное null.

ПолеАтрибутЧто в нём
operationoperationИмя спана — ниже
outcomeoutcomeok, refused или error — ниже
statusstatusСтатус выполнения или null, когда вызов выбросил ошибку
durationMsduration_msВремя вызова в миллисекундах, вместе с повторами SDK; для ожидания — всё ожидание
executionIdexecution_idВыполнение
operationCodeoperation_codeКод операции, который назвал запуск; для ожидания — тот, который называет его ответ
estimatedestimatedСколько, по оценке, стоили вызовы моделей в запуске, десятичной строкой
actualactualСколько они стоили, десятичной строкой, когда это известно
errorCodeerror_codeКод ошибки или выполнения, которое не завершилось
cacheHitcache_hitДля выполнения, у которого есть ответ, — взят ли он из кеша. false сохраняется как значение
JSON
[
  { "operation": "operations.execute", "outcome": "ok", "status": "completed", "duration_ms": 1843, "execution_id": "74438200973594710", "operation_code": "generate_email", "estimated": "0.00061590", "cache_hit": false },
  { "operation": "operations.execute", "outcome": "refused", "duration_ms": 96, "operation_code": "generate_email", "error_code": "budget_exceeded" },
  { "operation": "executions.wait", "outcome": "error", "status": "failed", "duration_ms": 30512, "execution_id": "74438200973594800", "operation_code": "summarize_report", "error_code": "flow_runtime_error", "cache_hit": false }
]

Ни одно поле не может хранить то, что было сказано модели. Это правило типа: у спана нет поля для сообщения, промпта или ответа.

Чем закончился запуск

  • ok — запуск завершился или ещё не закончился. Ожидание, у которого вышло время на незаконченном выполнении, тоже ok.

  • refused — Slen сказал «нет»: любой 402, политика или заблокированный клиент, или флоу, который закончился со статусом rejected. Это решение, а не поломка.

  • error — всё остальное: сеть, провайдер, неверный запрос, отклонённый ключ, лимит запросов ключа — и выполнение, которое закончилось со статусом failed или uncertain.

Имена спанов

ИмяКто пишет
operations.executeoperations.execute(): запуск, который ждёт своего ответа
operations.execute_asyncoperations.executeAsync(): запуск, отправленный в фон. Его первый ответ говорит queued, и ничего ещё не списано
executions.waitexecutions.wait(): один спан, когда ожидание заканчивается, сколько бы раз оно ни читало статус

Никакой другой вызов спан не пишет: ни чтение, ни вызов клиентов, ни вебхук.

Посчитать стоимость выполнения

Одно выполнение может оставить несколько спанов: запуск, каждое ожидание на нём, каждый повтор его ключа идемпотентности. Каждый из них повторяет estimated и actual, которые были у выполнения, когда спан писался. Если их сложить, стоимость получится завышенной. Берите стоимость один раз на каждый execution_id, из его последнего спана.

Отправить спаны по OTLP

У PHP SDK нет экспортёра OTLP. Реализуйте SpanRecorderInterface поверх клиента вашего инструмента трассировки или пишите спаны в лог через Psr3SpanRecorder и пусть их несёт ваш конвейер логов.

Перед завершением процесса

Записывающие PHP SDK пишут каждый спан сразу, поэтому перед завершением процесса отправлять нечего.

Что читать дальше