Логирование
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_access | access-лог входящих запросов (LOG_ELASTICSEARCH_ACCESS_INDEX, схема access) |
single | один файл без ротации (редко нужен явно) |
Сообщения через facades.Log().Info(...) / Errorf(...) без указания канала идут в default → stack → файл и 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-канал недоступен.