Записывайте каждый запуск как спан
PHPСпан, который SDK пишет на каждый запуск и каждое ожидание, — его поля, куда он уходит и как попадает в ваши логи или трассировки.
6 мин чтения
Спан — запись об одном запуске так, как её видит инструмент трассировки: вызов, чем он закончился, время, идентификаторы и деньги — но никогда промпт или ответ модели. SDK пишет по одному спану на каждый запуск и каждое ожидание и передаёт его записывающему, которого вы ему дали. Без записывающего спаны никуда не пишутся.
Дать SDK записывающий
Записывающий ниже пишет каждый спан в лог. Логгер здесь стоит вместо логгера вашего приложения.
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);use Slen\Sdk\SlenConfig;
use Slen\Sdk\SlenSdk;
$slen = SlenSdk::create(new SlenConfig(
apiKey: (string) getenv('SLEN_API_KEY'),
spanRecorder: $recorder,
));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.
| Поле | Атрибут | Что в нём |
|---|---|---|
operation | operation | Имя спана — ниже |
outcome | outcome | ok, refused или error — ниже |
status | status | Статус выполнения или null, когда вызов выбросил ошибку |
durationMs | duration_ms | Время вызова в миллисекундах, вместе с повторами SDK; для ожидания — всё ожидание |
executionId | execution_id | Выполнение |
operationCode | operation_code | Код операции, который назвал запуск; для ожидания — тот, который называет его ответ |
estimated | estimated | Сколько, по оценке, стоили вызовы моделей в запуске, десятичной строкой |
actual | actual | Сколько они стоили, десятичной строкой, когда это известно |
errorCode | error_code | Код ошибки или выполнения, которое не завершилось |
cacheHit | cache_hit | Для выполнения, у которого есть ответ, — взят ли он из кеша. false сохраняется как значение |
[
{ "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.execute | operations.execute(): запуск, который ждёт своего ответа |
operations.execute_async | operations.executeAsync(): запуск, отправленный в фон. Его первый ответ говорит queued, и ничего ещё не списано |
executions.wait | executions.wait(): один спан, когда ожидание заканчивается, сколько бы раз оно ни читало статус |
Никакой другой вызов спан не пишет: ни чтение, ни вызов клиентов, ни вебхук.
Посчитать стоимость выполнения
Одно выполнение может оставить несколько спанов: запуск, каждое ожидание на нём, каждый повтор его ключа идемпотентности. Каждый из них повторяет estimated и actual, которые были у выполнения, когда спан писался. Если их сложить, стоимость получится завышенной. Берите стоимость один раз на каждый execution_id, из его последнего спана.
Отправить спаны по OTLP
У PHP SDK нет экспортёра OTLP. Реализуйте SpanRecorderInterface поверх клиента вашего инструмента трассировки или пишите спаны в лог через Psr3SpanRecorder и пусть их несёт ваш конвейер логов.
Перед завершением процесса
Записывающие PHP SDK пишут каждый спан сразу, поэтому перед завершением процесса отправлять нечего.
Что читать дальше
Журнал — журнал каждого запроса рядом со спанами.
Интеграция с Laravel — спаны, записанные в лог приложения на Laravel.
Тестирование интеграции — тестируйте свой код без сети.