Кабинет

Журнал запросов

Фильтры, экспорт, срок хранения и то, почему тело успешного ответа восстанавливается, а не хранится.

На этой странице · 6

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

Фильтры

Экран логов и GET /api/logs понимают один и тот же набор параметров.

Параметры запроса

ПараметрЗначенияПо умолчанию
period1h, 24h, 7d, 30d24h
servicebitrix24, ozon, wildberriesвсе
statusall, 200, 400, 500 или точный кодall
apiKeyIdидентификатор ключавсе
qподстрока в пути, без учёта регистрапусто
limit1–10015
offsetсмещение страницы0

Значения 200, 400 и 500 — это группы 2xx, 4xx и 5xx, а не точные коды; любое другое число фильтрует по точному коду. Период всегда считается назад от текущего момента, произвольного интервала «с… по…» нет.

Кроме строк ответ несёт сводку за выбранный период и почасовые бакеты для диаграммы:

{"total": 5, "errorRate": 40, "avgLatencyMs": 119, "p95LatencyMs": 264}

Ошибкой считается код 400 и выше. Перцентиль p95 считается по выборке из не более чем 5000 длительностей — точный перцентиль в SQL здесь избыточен.

Свои моки фильтром по сервису не выбираются

Запросы к /custom/* пишутся с сервисом custom, а фильтр «Сервис» предлагает только три кода демо-API. Эти строки видны, когда фильтр не выставлен.

Экспорт

GET /api/logs/export отдаёт CSV с фильтрами по периоду и сервису — остальные фильтры экрана в выгрузку не переносятся. Разделитель — точка с запятой, в начале файла BOM: без него Excel открывает кириллицу как мусор. Потолок выгрузки — 100 000 строк, файл называется apistend-logs-ГГГГ-ММ-ДД.csv.

Время;Сервис;Метод;Эндпоинт;Код;Задержка, мс;Размер, байт;Ключ;ID запроса
2026-09-09T10:15:55.880Z;ozon;POST;/v3/posting/fbs/list;200;181;4218;Мобильное приложение;f0c9ea91aae8abf9

Тел запросов и ответов в выгрузке нет — только то, что видно в таблице.

Срок хранения

Записи журнала живут 30 дней. Это не оценка, а обещание из интерфейса: в «Опасной зоне» на экране ключей написано «История запросов сохранится в логах 30 дней», и уборщик исполняет ровно его. Срок задаётся переменной APISTEND_LOG_RETENTION_DAYS.

Уборка запускается через полминуты после старта процесса и дальше раз в час. Удаление идёт пачками по 5000 строк с паузой 200 мс между ними: одна большая DELETE по таблице на десятки миллионов строк держала бы блокировки и мешала записи новых логов. Что уборка сделала в последний раз, видно в /health:

{"retention": {"retentionDays": 30, "lastRun": {"at": "2026-09-09T09:43:16.459Z", "deleted": 0}}}

Прореживание под нагрузкой

Строки журнала копятся в памяти и уходят в базу пачкой — раз в секунду или по достижении двухсот строк. Синхронная запись на каждый вызов мока съела бы весь бюджет задержки раньше, чем сам мок.

Одного буфера под настоящей нагрузкой мало. Замер, по которому выбран порог: при 3800 строках в секунду запись в PostgreSQL блокирует событийный цикл и роняет пропускную способность шлюза с 21 700 до 4500 запросов в секунду, а p95 задержки поднимает с 5,7 до 107 мс. Поэтому выше APISTEND_LOG_SAMPLE_RPS (по умолчанию 300 строк в секунду на процесс) включается выборка: пишется доля запросов, а не все.

Что прореживается

ЗапросыПоведение
успешныепрореживаются, доля падает пропорционально скорости, но не ниже 0,5 %
ошибки 4xx и 5xxпишутся всегда — ради них журнал и открывают
429` и `503прореживаются наравне с успешными

429 и 503 — исключение из исключения: это эмуляция лимитов боевого API, и приходят они лавиной. В замере на 50 000 запросов их было 32 000; логировать каждый — значит уронить шлюз ровно тогда, когда клиент и так упёрся в лимит.

Молча терять записи нельзя, поэтому доля выборки видна и в ответе GET /api/logs, и в /health:

{"logBuffer": {"pending": 0, "written": 4, "dropped": 0, "sampledOut": 0, "sampleRate": 1}}

sampleRate: 1 означает, что пишется всё. sampledOut — сколько записей не попало в журнал из-за выборки, dropped — сколько потеряно на переполнении буфера (20 000 строк): лучше потерять запись, чем уронить процесс по памяти.

Журнал целиком отключается переменной APISTEND_DISABLE_REQUEST_LOG=1 — она нужна нагрузочным замерам, чтобы мерить шлюз, а не диск.

Карточка запроса

GET /api/logs/:id принимает и внутренний идентификатор, и публичный req_…. Перед любым чтением журнала буфер сбрасывается в базу — иначе только что сделанный запрос не появился бы в списке, и это выглядело бы как потеря данных.

{
  "publicId": "f0c9ea91aae8abf9",
  "serviceCode": "ozon",
  "endpoint": "/v3/posting/fbs/list",
  "statusCode": 200,
  "scenario": "success",
  "responseSource": "example",
  "upstreamUrl": "https://api-seller.ozon.ru/v3/posting/fbs/list",
  "clientIp": "127.0.0.1",
  "responseBodyReproduced": true
}

Почему тело успешного ответа не хранится

Ответ мок-шлюза детерминирован: он выводится из метода каталога и объёма данных песочницы. Хранить его — значит писать десятки мегабайт в секунду ради данных, которые вычисляются за микросекунды. Поэтому при чтении карточки тело собирается заново тем же движком, что сформировал его при запросе, а признак responseBodyReproduced честно говорит, что тело восстановлено, а не прочитано из журнала. Заголовки ответа восстанавливаются вместе с телом по той же причине.

Что хранится по-настоящему:

  • тело запроса — данные пользователя, восстановить их неоткуда (усечённо, до 8000 символов, с замаскированными ключами);
  • тело ответа с ошибкой — в конверте ошибки есть идентификатор запроса и метка времени, такой ответ невоспроизводим;
  • ответы Bitrix24 в контексте установленного приложения — они зависят от состояния портала.

У своих моков тело не восстанавливается

Записи с сервисом custom движок восстановить не может: их тело задаёте вы, и в момент чтения карточки оно уже могло измениться. В карточке такого запроса responseBody пуст, а responseBodyReproduced равен false.

Восстановление всегда идёт по сценарию success и без параметров строки запроса: у записи с другим сценарием тело ошибки лежит в журнале и берётся оттуда.