Log export (Русский)
1. Как это работает
Заголовок раздела «1. Как это работает»call_logs (SQLite) → callLogExportSource.getCallLogsForExport(cursor, batchSize) → LogExportRecord[] (набор полей вкладки Logs) → клиент назначения.send(пакет) → advanceLogExportCursor(id, lastRowId, count)- Расписание — одна cron-задача
JobRegistry,log_export, по умолчанию запускаемая по расписанию0 * * * *(ежечасно, UTC). Регистрируется вsrc/lib/initCloudSync.ts; расписание можно переопределить с помощьюOMNIROUTE_LOG_EXPORT_CRON. При каждом запуске последовательно обрабатываются все включённые назначения. - Курсор — неявный
call_logs.rowidSQLite, сохраняемый отдельно для каждого назначения вlog_export_destinations.cursor_row_id.timestampнамеренно не используется в качестве курсора: вызывающие стороны могут передавать собственное значение, поэтому медленный запрос может быть записан после более быстрого, который начался позже, и курсор по временной метке пропустил бы его. - Пакетная обработка —
batch_sizeстрок на запрос (по умолчанию 500),max_rows_per_runстрок за запуск (по умолчанию 10000), чтобы большой объём накопившихся данных обрабатывался за несколько запусков, а не блокировал один из них. - Доставка — курсор продвигается только после успешного завершения
send(). При сбое отправки пакета курсор остаётся на прежнем месте, поэтому те же строки будут отправлены повторно при следующем запуске. Гарантируется доставка не менее одного раза с устранением дубликатов на стороне назначения, а не настоящая доставка строго один раз: в BigQuery каждая строка идентифицируется по идентификатору журнала вызовов, который учитывается на основе максимальных усилий в пределах собственного окна дедупликации. - Защита от перекрытия запусков — cron-запуск и
POST .../runмогут сработать одновременно. Назначение, которое уже обрабатывается, пропускается вместо повторной обработки (skipped: trueв результате запуска), поэтому параллельный запуск не может повторно отправить пакет или переместить курсор назад. - Восстановление после очистки — если
cursor_row_idоказывается большеMAX(rowid)(вся таблица была очищена, а нумерация строк началась заново), средство запуска сбрасывает курсор на 0, а не перестаёт навсегда видеть новые записи.
Полезные нагрузки (промпты и ответы)
Заголовок раздела «Полезные нагрузки (промпты и ответы)»По умолчанию экспортируются только сводные поля, отображаемые в списке Logs. Включение параметра
Экспорт промптов и ответов (includeBodies) дополнительно экспортирует данные, которые панель подробностей
Logs показывает для каждого вызова:
| Поле | Содержимое |
|---|---|
request_body / response_body |
Полезные нагрузки вызова в том виде, как их отображает панель |
pipeline_route_decision |
Какую цель и модель выбрал маршрутизатор |
pipeline_client_request |
Исходный запрос в точности в том виде, как его отправил клиент |
pipeline_openai_request |
Запрос после преобразования во внутренний формат OpenAI |
pipeline_provider_request |
Запрос, фактически отправленный вышестоящему сервису, в диалекте поставщика |
pipeline_provider_response |
Необработанный ответ вышестоящего сервиса |
pipeline_client_response |
Ответ, возвращённый вызывающей стороне |
pipeline_error |
Подробности ошибки конвейера для неудачного вызова |
bodies_truncated |
Истина, если любое из указанных выше полей достигло maxBodyBytes |
Это содержимое промптов, поэтому оно по умолчанию отключено и намеренно настраивается отдельно для каждого назначения.
Экспортируется именно то, что отображает панель, поскольку оба механизма читают данные через getCallLogById: полезные нагрузки
уже очищены от персональных данных и отредактированы для удаления секретов при записи, а для вызова, выполненного с API-ключом
noLog, полезная нагрузка вообще не сохраняется, поэтому экспортировать нечего.
Полезные нагрузки для каждой строки считываются из артефакта в файловой системе, поэтому гидратация выполняется только для назначений, которые её запросили. Если артефакт строки отсутствует или повреждён, экспортируется её сводка с полезными нагрузками, равными null, вместо сбоя всего пакета и блокировки курсора.
maxBodyBytes (по умолчанию 262144) ограничивает размер каждого поля. Более длинные полезные нагрузки обрезаются, а не
отбрасываются — даже обрезанный промпт позволяет понять, «что было запрошено», — а строка помечается флагом
bodies_truncated. Потоковые дельты по отдельным фрагментам не экспортируются; собранный ответ уже содержится
в pipeline_provider_response и pipeline_client_response.
2. Файлы
Заголовок раздела «2. Файлы»| Компонент | Расположение |
|---|---|
| Контракт назначения | src/lib/logExport/types.ts |
| Реестр | src/lib/logExport/registry.ts |
| Обработка секретов | src/lib/logExport/secrets.ts |
| Исполнитель (цикл курсора) | src/lib/logExport/runner.ts |
| Представление API | src/lib/logExport/presenter.ts |
| Назначение BigQuery | src/lib/logExport/destinations/bigquery.ts |
| Аутентификация Google SA | src/lib/logExport/googleServiceAccount.ts |
| Источник журнала вызовов | src/lib/usage/callLogExportSource.ts |
| Хранилище | src/lib/db/logExportDestinations.ts |
| Задание cron | src/lib/jobs/logExportJob.ts |
| Слой REST | src/app/api/log-export/ |
| Страница панели управления | src/app/(dashboard)/dashboard/log-export/ |
Схема: src/lib/db/migrations/170_log_export_destinations.sql.
3. REST API
Заголовок раздела «3. REST API»Все маршруты требуют аутентификации управления (requireManagementAuth). Секреты никогда не возвращаются:
сохранённый секрет возвращается как литерал __stored__, а отправка этого значения обратно при обновлении
сохраняет существующие учётные данные.
Для создания или обновления назначения, тип которого объявляет секрет, требуется
STORAGE_ENCRYPTION_KEY. Без него encrypt() незаметно пропускает данные без изменений, поэтому операция записи
отклоняется с кодом 400, чтобы учётные данные не попали в SQLite в виде открытого текста (та же проверка
применяется вебхуком Telegram).
| Метод | Путь | Назначение |
|---|---|---|
GET |
/api/log-export/types |
Типы назначений и списки их полей конфигурации |
GET |
/api/log-export/destinations |
Список назначений (секреты скрыты) |
POST |
/api/log-export/destinations |
Создать назначение |
GET |
/api/log-export/destinations/{id} |
Получить одно назначение |
PUT |
/api/log-export/destinations/{id} |
Обновить имя / состояние / конфигурацию / пакетирование |
DELETE |
/api/log-export/destinations/{id} |
Удалить |
POST |
/api/log-export/destinations/{id}/test |
Проверить учётные данные без записи |
POST |
/api/log-export/destinations/{id}/run |
Выполнить выгрузку сейчас тем же способом, что и запланированный запуск |
GET |
/api/log-export/status |
Состояние cron, последние запуски, очередь для каждой цели |
Именно GET /api/log-export/types делает пользовательский интерфейс универсальным: форма панели управления создаётся на основе
возвращённых описателей полей, поэтому добавление нового назначения не требует изменения пользовательского интерфейса.
4. Назначение BigQuery
Заголовок раздела «4. Назначение BigQuery»Ключи конфигурации (type: "bigquery"):
| Ключ | Примечания |
|---|---|
projectId |
Проект GCP, содержащий набор данных |
datasetId |
[A-Za-z0-9_]+ |
tableId |
[A-Za-z0-9_]+ |
location |
Используется только при необходимости создать набор данных (по умолчанию EU) |
serviceAccountJson |
Ключ сервисного аккаунта. Секрет: зашифрован при хранении и никогда не возвращается |
autoCreate |
Создать набор данных и таблицу при первом экспорте (по умолчанию true) |
Сервисному аккаунту требуется разрешение bigquery.tables.updateData для целевой таблицы, а также
bigquery.datasets.create / bigquery.tables.create, если включён autoCreate.
Настроенный пакет является единицей курсора, а не HTTP: send() разбивает его на вызовы insertAll
не более чем по 500 строк, поэтому большой batch_size не может привести к превышению ограничения BigQuery в 10 МБ на запрос.
При временных статусах (408/429/500/502/503/504) выполняется до трёх повторных попыток с экспоненциальной
задержкой и повторным использованием тех же insertIds; ошибки аутентификации и схемы приводят к исключению при первой попытке,
а не расходуют время запуска.
Таблица, созданная несколько мгновений назад, ещё не видна потоковой конечной точке, которая отвечает кодом 404 в течение нескольких секунд. Такой ответ 404 приводит к повторным попыткам, но только если таблица была создана во время этого запуска — если таблица действительно отсутствует, операция сразу завершается ошибкой. Обратите внимание: повторное создание таблицы с именем недавно удалённой таблицы приводит к тому, что BigQuery в течение нескольких минут отклоняет потоковые вставки; это особенность удаления с последующим повторным созданием, поэтому вместо удаления и повторного добавления таблицы лучше использовать новое имя.
Частичная ошибка возвращается как HTTP 200 с непустым массивом insertErrors[]. Это рассматривается как
ошибка и приводит к исключению, благодаря чему курсор не продвигается дальше строк, которые BigQuery не
принял; такое поведение закреплено тестом tests/unit/log-export-bigquery.test.ts.
Для передачи используется обычный REST: самоподписанное утверждение RS256 обменивается на токен доступа по адресу
https://oauth2.googleapis.com/token, после чего строки отправляются в tabledata.insertAll. Google SDK
не подключается. Токены доступа кешируются внутри процесса для каждой пары (сервисный аккаунт, область доступа).
Созданная таблица содержит по одному столбцу для каждого поля вкладки Logs, а также exported_at, и организована
с учётом того, как фактически выполняются запросы к журналам вызовов:
- Разбиение по дням на основе
timestamp, поэтому запрос с ограничением по дате сканирует только соответствующие дни. - Кластеризация по
api_key_name,provider,model,status(в указанном порядке), поэтому фильтрация по тому, кто выполнил вызов, куда он был направлен или завершился ли он ошибкой, исключает блоки внутри каждого раздела. BigQuery допускает не более четырёх столбцов кластеризации, и их порядок важен: фильтр только поapi_key_nameисключает блоки, а фильтр только поstatus— нет. - Необязательный срок хранения разделов через
partitionExpirationDays(0 сохраняет всё), применяемый при создании таблицы.
Обе настройки применяются при создании. Существующая таблица сохраняет уже имеющуюся структуру, поэтому, если вы хотите применить их, укажите в качестве назначения новый идентификатор таблицы.
tests/unit/log-export-bigquery.test.ts проверяет, что маппер и схема таблицы остаются
синхронизированными, поэтому новый столбец журнала вызовов не может быть незаметно отброшен при экспорте.
Пакеты разбиваются на части с учётом как количества строк, так и объёма сериализованных данных в байтах. Одного количества строк недостаточно, когда экспортируются полезные нагрузки: 500 строк с промптами могут занимать десятки мегабайт, а insertAll отклоняет запросы размером более 10 МБ. Часть закрывается при достижении 500 строк или 9 МБ — в зависимости от того, что произойдёт раньше.
5. Добавление места назначения
Заголовок раздела «5. Добавление места назначения»- Создайте
src/lib/logExport/destinations/<name>.ts, экспортирующийLogExportDestinationType: Zod-схемуconfigSchema, массив дескрипторовfieldsдля пользовательского интерфейса,secretFieldsиcreateClient(config), возвращающийtest()/prepare()/send(records). - Добавьте его в массив
DESTINATIONSвsrc/lib/logExport/registry.ts. - Напишите тесты в
tests/unit/.
Это всё необходимое изменение: хранилище, задание cron, REST-слой, шифрование секретов и форма панели управления используют реестр.
Два правила для нового места назначения:
send()обязан выбрасывать исключение при частичном сбое. Успешное завершение означает: «в месте назначения есть эти строки», после чего курсор навсегда перемещается за них.- Место назначения, принимающее предоставленный пользователем URL, перед выполнением запроса должно проверить его с помощью
parseAndValidateWebhookUrl(src/shared/network/outboundUrlGuardPolicy.ts), как это делают вебхуки. Для BigQuery это не требуется: его хосты заданы константами.
6. Эксплуатация
Заголовок раздела «6. Эксплуатация»- Панель управления: Интеграции → Экспорт журналов. Добавьте место назначения, запустите Тест, чтобы проверить учётные данные без записи строк, а затем включите его.
- Очередь необработанных данных: на каждой карточке места назначения отображаются количество ожидающих строк и курсор;
GET /api/log-export/statusвозвращает те же показатели, а также данные о последних 20 запусках задания. - Сбой одного места назначения не приводит к сбою остальных — сводка запуска записывает статус каждого места назначения
в
last_status/last_error, а журнал запусков задания сохраняет сводные данные. - При удалении места назначения удаляется его курсор. При повторном добавлении обработка начинается с самого старого сохранённого
журнала вызовов, из-за чего строки, которые уже могут находиться в месте назначения, отправляются повторно. В BigQuery значение
insertIdкаждой строки устраняет такие дубликаты только в пределах собственного окна дедупликации BigQuery, поэтому предпочтительнее отключить место назначения, а не удалять его.
HagiCode
HagiCode — агентная среда разработки со структурированными процессами, параллельным выполнением несколькими агентами и интерфейсами Hero Dungeon.
Превращайте идеи в полезное ПО с более умным, быстрым и увлекательным агентным рабочим процессом.

- SmartСтруктурированные процессы превращают намерение в исполнимый путь от идеи до готового изменения.
- EfficientМультиагентные процессы параллельно продвигают исследование, реализацию и проверку.
- FunHero Dungeon делает длительную совместную разработку наглядной и увлекательной.