Логирование
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). Его состав задаётся logging.channels.stack.channels; в общем каркасе это daily и elastic. Отдельно настроен канал access-лога:
| Канал | Назначение |
|---|---|
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 / LOG_ELASTICSEARCH_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().WithContext(ctx).Infof("reindex started offers=%d", count)
facades.Log().WithContext(ctx).Errorf("kafka handler %s failed: %v", topicKey, err)
ctx здесь — context.Context из action или Kafka-handler; в HTTP-handler передавайте ctx.Context(). Это сохраняет связь записи с контекстом обработки.
Уровни те же, что у фреймворка: Debug / Info / Warning / Error / Fatal / Panic и их f-варианты. Сообщение лучше делать коротким и стабильным; детали — структурированными полями через With:
facades.Log().WithContext(ctx).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 в список middleware сгенерированного клиента. Оно включается отдельной настройкой http.client_log, независимо от APP_DEBUG:
| Переменная | Смысл |
|---|---|
HTTP_CLIENT_LOG | включить файловый лог исходящих вызовов |
HTTP_CLIENT_LOG_PATH | базовый путь файла (по умолчанию storage/logs/http-client.log) |
Формат лога — пары блоков request/response с методом, именем сервиса, путём, статусом и телами. В имя файла добавляется дата: например, http-client-2026-10-27.log. Ошибка транспорта записывается в блок ответа с текстом ошибки.
Middleware пишет тела целиком и не выполняет автоматического скрытия чувствительных полей. Включайте такой лог в окружении, где допустима запись этих данных, и проверяйте состав запросов интеграции.
Буфер, flush и artisan
Elastic-хендлер не шлёт каждый Info отдельным HTTP-запросом. Сообщения копятся в буфере и сбрасываются при заполнении buffer_size, по таймеру flush_interval или при остановке процесса.
Runner app:log-elastic стартует вместе с web-приложением, если заданы хосты ES, и на shutdown вызывает FlushAll / CloseAll. В bootstrap/app.go artisan-режим подключает observability.StartArtisanSidecars, который вызывает StartArtisanLogging для всех artisan-команд. Эта обвязка запускает flush на сигнал и завершение context: иначе короткий процесс мог бы завершиться раньше, чем буфер ушёл в индекс. В artisan-режиме хендлер ещё и чаще сбрасывает буфер сразу (eager flush), чтобы логи воркера не «залипали» до накопления сотни записей.
Ошибки bulk-отправки пишутся в stderr ([elastic-log] bulk flush...), чтобы сбой логирования был виден даже если сам ES-канал недоступен.
При добавлении собственной долгой команды сохраните подключение общей обвязки и вызов завершения из Shutdown, как в командах app/modules/common/commands. Проверяйте обработку SIGTERM с учётом времени работы команды. Запуск Prometheus для artisan имеет отдельный список команд — см. метрики.