Перейти к основному содержимому

Логирование

Go-сервисы Ensi пишут логи через штатный Log-фасад Goravel (facades.Log()), а доставку в Elasticsearch и формат документов, совместимый с индексами платформы, обеспечивает адаптер в app/adapters/logging. По смыслу это тот же стек, что и в PHP (каналы, уровни, файлы + централизованный поиск), только без Monolog: каналы описаны в config/logging.go, а custom-драйвер elastic / elastic_access буферизует документы и отправляет их bulk-запросом.

Код разделён так:

  • конфиг каналов и Elasticsearch-соединения — config/logging.go;
  • хендлеры, буфер, flush и runner — app/adapters/logging;
  • access-лог входящих HTTP — middleware HttpRequestLog в app/common/http/middleware;
  • подробный лог исходящих HTTP-клиентов в файл — app/common/http/clientmiddleware;
  • корректное завершение буферов у artisan — обвязка в app/adapters/observability.

Локальный просмотр docker-логов контейнера описан в просмотре логов на локальной машине. Ниже — что происходит внутри сервиса: куда пишутся сообщения, как устроены каналы и как разработчику логировать свою логику.

Каналы и куда что попадает

Канал по умолчанию задаётся LOG_CHANNEL (обычно stack). stack пишет сразу в два направления:

КаналНазначение
dailyтекстовые файлы storage/logs/goravel-YYYY-MM-DD.log (ротация по дням)
elasticдокументы в индекс приложения (LOG_ELASTICSEARCH_INDEX, схема app)
elastic_accessaccess-лог входящих запросов (LOG_ELASTICSEARCH_ACCESS_INDEX, схема access)
singleодин файл без ротации (редко нужен явно)

Сообщения через facades.Log().Info(...) / Errorf(...) без указания канала идут в defaultstack → файл и Elasticsearch (если хосты заданы). Access-лог middleware пишет только в elastic_access, чтобы не смешивать nginx-подобные поля с прикладными сообщениями.

Если LOG_ELASTICSEARCH_HOSTS пуст, elastic-каналы поднимают noop-хендлер: запись в ES просто не происходит, файловый daily при этом продолжает работать. Так удобно жить локально без кластера логов.

Основные переменные:

ПеременнаяСмысл
LOG_CHANNELканал по умолчанию (stack)
LOG_LEVELминимальный уровень (debug, info, warning, error, …)
LOG_ELASTICSEARCH_HOSTSхосты ES через запятую; пусто — без отправки
LOG_ELASTICSEARCH_USERNAME / PASSWORDучётные данные, если нужны
LOG_ELASTICSEARCH_SSL_VERIFICATIONпроверка TLS
LOG_ELASTICSEARCH_INDEXиндекс прикладных логов
LOG_ELASTICSEARCH_ACCESS_INDEXиндекс access-логов
LOG_ELASTICSEARCH_BUFFER_SIZEразмер буфера перед flush (по умолчанию 100)
LOG_ELASTICSEARCH_FLUSH_INTERVALпериодический flush (по умолчанию 1s)

Как писать логи в коде

Для прикладных сообщений достаточно фасада:

facades.Log().Infof("reindex started offers=%d", count)
facades.Log().Errorf("kafka handler %s failed: %v", topicKey, err)

Уровни те же, что у фреймворка: Debug / Info / Warning / Error / Fatal / Panic и их f-варианты. Сообщение лучше делать коротким и стабильным; детали — структурированными полями через With:

facades.Log().With(map[string]any{
"topic": msg.Topic,
"partition": msg.Partition,
"offset": msg.Offset,
"entity_id": id,
}).Errorf("sync failed: %v", err)

Отдельный канал выбирают через Channel, когда сообщение не должно идти в обычный app-стек. Типичный случай — access; свой доменный канал заводят редко и только если для него уже есть индекс/маппинг или осознанно нужен только файл.

facades.Log().Channel("elastic_access").With(fields).Info("")

В access-middleware сообщение часто пустое: смысл несут поля (request, status, request_time, …), а не текст.

Поля документа в Elasticsearch

Хендлер раскладывает ключи из With по схеме индекса:

  • известные корневые поля схемы (app, env, level, message, namespace, pod, status, request, …) попадают в корень документа;
  • всё остальное уходит во вложенный объект context.

Схема app (канал elastic) рассчитана на прикладные логи. Схема access (канал elastic_access) — на поля в духе nginx access-лога. Список корневых ключей живёт в app/adapters/logging/elastic_fields.go и согласован с инвентарём индексов devops (ensi-*-app, ensi-*-nginx). Если положить в With неизвестный для схемы ключ — он не потеряется, а окажется в context; это нормальный путь для доменных деталей (entity_id, topic_key, duration_ms).

Зарезервированные ключи @timestamp, level, message, context из With не перетирают служебные поля записи: context при этом аккуратно мержится.

Access-лог входящих HTTP

Middleware HttpRequestLog вешается на глобальный стек в bootstrap/app.go рядом с JSON-body и метриками. На каждый запрос после ответа оно пишет в elastic_access набор полей: метод и путь, статус, время обработки, размер тела, IP, User-Agent, а также namespace / pod / service из окружения Kubernetes, если они есть.

Отдельно логировать «запрос пришёл / ушёл» в прикладном канале обычно не нужно — access уже закрывает этот слой. В facades.Log() из хэндлера или action имеет смысл писать то, чего в access нет: бизнес-решения, причины отказа, идентификаторы сущностей, сбои интеграций.

Лог исходящих HTTP-клиентов

Для удобства разработки запросы/ответы к внешним сервисам логируются в отдельный файл. Делается это навешиванием clientmiddleware.AppendLogging на клиент.
Кроме того нужно через env переменные включить такое логирование.

ПеременнаяСмысл
HTTP_CLIENT_LOGвключить файловый лог исходящих вызовов
HTTP_CLIENT_LOG_PATHбазовый путь файла

Формат лога — пары блоков request/response с методом, именем сервиса, путём, статусом и телами.

Буфер, flush и artisan

Elastic-хендлер не шлёт каждый Info отдельным HTTP-запросом. Сообщения копятся в буфере и сбрасываются при заполнении buffer_size, по таймеру flush_interval или при остановке процесса.

Runner app:log-elastic стартует вместе с web-приложением, если заданы хосты ES, и на shutdown вызывает FlushAll / CloseAll. Для artisan-команд (queue:work, kafka:consume, разовые команды и т.д.) flush на сигнал и завершение context поднимает observability.StartArtisanLogging: иначе короткий процесс мог бы завершиться раньше, чем буфер ушёл в индекс. В artisan-режиме хендлер ещё и чаще сбрасывает буфер сразу (eager flush), чтобы логи воркера не «залипали» до накопления сотни записей.

Ошибки bulk-отправки пишутся в stderr ([elastic-log] bulk flush...), чтобы сбой логирования был виден даже если сам ES-канал недоступен.