27.07.2026 10 мин чтения

HttpClient молчит в логах? Два фильтра и один FileLogger

Кирилл Новожилов

Кирилл Новожилов

Автор

HttpClient молчит в логах? Два фильтра и один FileLogger
Введение

Вы подключили FileLogger, сделали $http->get(...), открыли файл — пусто. Или в файле есть заголовки, но нет JSON тела, из‑за которого вы и пришли. Или лог внезапно содержит чужие запросы из модулей ядра, а не только ваш сервис.

Это не баг HttpClient. Так устроена связка именованного логгера main.HttpClient, битовой маски HttpDebug и PSR‑3 уровня у FileLogger. Ниже — как это реально работает в ядре (main 26.x) и как включить логирование так, чтобы оно помогало, а не жгло диски и не светило токены.

Сюжет: интеграция «вроде живая», а ответа нет

Типичный вечер:

        $http = new \Bitrix\Main\Web\HttpClient(['socketTimeout' => 5]);
$body = $http->post('https://pay.example/api/charge', $payload);

if ($body === false) {
    // getError() говорит что-то вроде "stream timeout" — и дальше гадание
}

    

Хочется увидеть wire: какой URL ушёл, какие заголовки, что в теле, сколько занял handshake. В голове сразу: «нужен FileLogger». И вот тут начинается развилка, о которой молчит половина гайдов.

Сюрприз 1. Логгер сам по себе ничего не пишет

Внутри запроса живёт не «голый» HttpClient, а Handler (Curl или Socket). Логгер он берёт так:

        // main/lib/Web/Http/Handler.php
$logger = Diag\Logger::create('main.HttpClient', [$this, $this->request]);
$this->setLogger($logger ?? new Log\NullLogger());

return ($this->logger instanceof Log\NullLogger ? null : $this->logger);

    

Нет записи main.HttpClient в секции loggers → фабрика вернула null → стоит NullLoggergetLogger() отдаёт nullтишина. Не ошибка, не «логи сломались». Просто выключено по умолчанию.

💡 Отсюда правило
Хотите видеть все исходящие HTTP из проекта (включая чужие модули) — настраивайте именованный логгер в .settings.php / .settings_extra.php. Хотите только один клиент в своём сервисе — можно повесить логгер точечно на экземпляр (об этом ниже).

Сюрприз 2. Сигнатура конструктора не такая, как в старых примерах

В сети до сих пор встречается что-то вроде:

        // Устарело / не совпадает с текущим ядром
'constructor' => function (\Bitrix\Main\Web\HttpClient $http, $method, $url) { ... }

    

В актуальном коде в closure приходят другие аргументы — ровно те, что передаёт Handler::getLogger():

  1. $this — объект, реализующий DebugInterface (сам handler);
  2. $this->request — PSR‑7 RequestInterface.

Рабочий минимальный конфиг:

        // bitrix/.settings_extra.php (удобно для dev; на prod лучше не держать ALL)
return [
    'loggers' => [
        'value' => [
            'main.HttpClient' => [
                'constructor' => static function (
                    \Bitrix\Main\Web\Http\DebugInterface $debug,
                    \Psr\Http\Message\RequestInterface $request,
                ) {
                    // Без этой строки тела запросов вы, скорее всего, не увидите
                    $debug->setDebugLevel(\Bitrix\Main\Web\HttpDebug::ALL);

                    $dir = $_SERVER['DOCUMENT_ROOT'] . '/../logs';
                    if (!is_dir($dir)) {
                        mkdir($dir, 0750, true);
                    }

                    return new \Bitrix\Main\Diag\FileLogger(
                        $dir . '/http-client.log',
                        5 * 1024 * 1024, // ротация ~5 МБ → *.old
                    );
                },
                'level' => \Psr\Log\LogLevel::DEBUG,
            ],
        ],
        'readonly' => true,
    ],
];

    
⚠️ Обратите внимание
setDebugLevel вызывается на handler’е, который пришёл первым аргументом. Это не декорация — без неё вы останетесь на дефолтной маске.

Сюрприз 3. Два независимых фильтра

Даже когда FileLogger создан, сообщение должно пройти два сита.

Фильтр A — битовая маска HttpDebug

        // main/lib/Web/HttpDebug.php
public const REQUEST_HEADERS  = 0b00000001;
public const REQUEST_BODY     = 0b00000010;
public const RESPONSE_HEADERS = 0b00000100;
public const RESPONSE_BODY    = 0b00001000;
public const CONNECT          = 0b00010000;
public const DIAGNOSTICS      = 0b00100000;
public const ALL              = 0b00111111;

// По умолчанию:
public const DEFAULT = self::CONNECT | self::REQUEST_HEADERS | self::RESPONSE_HEADERS;

    

DEFAULT — это connect + заголовки запроса/ответа. Тел в дефолте нет.
Отсюда классика: «логгер настроил, Authorization вижу, JSON — нет». Нужен REQUEST_BODY / RESPONSE_BODY или сразу ALL.

Фильтр B — PSR‑3 уровень у логгера

Дамп трафика в Handler::log() всегда уходит так:

        $logger->debug($logMessage, $context);

    

Ошибки сети (curl_error, socket fail) — через $logger->error(...).

Если в .settings.php поставить 'level' => LogLevel::ERROR, вы получите только аварии, а «красивый» wire‑дамп отфильтруется. Для отладки интеграции нужен именно DEBUG.

ℹ️ Итого пустой файл часто значит не «FileLogger сломан», а «маска не та» или «уровень слишком высокий».

Сюрприз 4. Первая строка лога — это не HTTP, а backtrace

Перед первым куском трафика handler пишет шапку:

        ----------
2026-07-27 … - example.test
/local/modules/vendor.pay/lib/Service/ChargeService.php:84
...

    

В коде:

        $headMessage = "\n{delimiter}\n{date} - {host}\n{trace}";
$headContext = ['trace' => Diag\Helper::getBackTrace(10, DEBUG_BACKTRACE_IGNORE_ARGS, 5)];
$logger->debug($headMessage, $headContext);

    

То есть лог отвечает не только на «что ушло в сеть», но и на кто вызвал HttpClient. На проекте, где одинаковый URL дергают три модуля, это ценнее самого тела ответа.

Дальше — маркеры в духе:

  • ***CONNECT to …
  • >>>REQUEST / заголовки
  • тело (если маска позволяет)
  • <<<RESPONSE / заголовки / тело
  • ***TIME connect=…, handshake=…, request=…, total=… при флаге DIAGNOSTICS

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

Два способа включить логи — и когда какой

Кейс A Глобально через main.HttpClient (для охоты)

Ловит все клиенты: ваш код, ядро, соседний модуль, агент. Удобно, когда непонятно, кто вообще ходит наружу. Страшно на проде: шум, объём, секреты.

Держите такой блок в .settings_extra.php на dev/stage и не копируйте его в production‑конфиг.

Кейс B Точечно на экземпляре (для одного сервиса)

        use Bitrix\Main\Diag\FileLogger;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\HttpDebug;
use Psr\Log\LogLevel;

$logger = new FileLogger('/var/log/bitrix/pay-http.log', 2 * 1024 * 1024);
$logger->setLevel(LogLevel::DEBUG);

$http = new HttpClient([
    'socketTimeout' => 5,
    'streamTimeout' => 20,
    'debugLevel'    => HttpDebug::ALL, // можно и через setDebugLevel()
]);
$http->setLogger($logger);

$body = $http->post('https://pay.example/api/charge', $payload);

    

Как это стыкуется с ядром: в createHandler() логгер и маска копируются на handler только если у клиента уже установлен $this->logger. Иначе handler пойдёт в фабрику main.HttpClient.

💡 Практический вывод
  • setLogger на экземпляре — локальный override, настройки loggers для этого клиента не нужны;
  • нет setLogger — решает только main.HttpClient из конфига.

Сюрприз 5. Куда нельзя писать файл

Пример из документации вида DOCUMENT_ROOT . '/http.log' опасен ровно настолько, насколько звучит: положили рядом с index.php — и при неудачном nginx/access получили утечку Authorization, тел вебхуков, персональных данных.

Мини-чеклист:

  1. Каталог вне document root (/var/log/bitrix, ../logs относительно сайта).
  2. Права: пишет пользователь PHP‑FPM, читает только ops/разработчик.
  3. На время отладки — ALL; после поимки бага — выключить или сузить маску до DEFAULT.
  4. Ротация: второй аргумент FileLogger (байты). 0 — без ротации, файл растёт пока не упрётесь в диск.
  5. Помнить: один общий http-client.log на нагруженном стенде быстро смешает запросы; для глубокой отладки иногда удобнее суффикс от spl_object_hash($request).

Короткий сценарий «починить за 5 минут»

  1. Добавить main.HttpClient в .settings_extra.php как выше (HttpDebug::ALL + LogLevel::DEBUG).
  2. Убедиться, что каталог логов существует и writable для php.
  3. Повторить проблемный запрос.
  4. В шапке лога найти backtrace — понять, тот ли это код.
  5. По ***TIME понять, сеть это или ответ API.
  6. Увидеть тело/статус — починить интеграцию.
  7. Удалить или закомментировать логгер на prod. Оставленный ALL — это не «мониторинг», это архив секретов.

Что забрать с собой

Ожидание Реальность в ядре
«Положил FileLogger — увижу HTTP» Нужен ещё main.HttpClient или setLogger + маска
«Раз логгер есть, будет всё» DEFAULT без тел; дамп идёт через debug()
«Конструктор получает HttpClient» Сейчас: DebugInterface + RequestInterface
«В логе только мой сервис» Глобальный логгер видит весь сайт
«Файл в www/ удобно» Удобно и атакующему

FileLogger здесь — не «ещё один способ file_put_contents», а приёмник для уже встроенного отладочного канала HttpClient. Настроили два фильтра правильно — интеграция перестаёт быть чёрным ящиком. Настроили неправильно — получаете либо пустой файл, либо свалку с токенами.

Опубликовано 2 дня назад

Комментарии (0)

Пожалуйста, войдите в аккаунт, чтобы оставить комментарий

Оставить комментарий

Похожие статьи

Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…
Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти