Логи отвечают на вопрос «что именно произошло с этим запросом». Чтобы они помогали, а не просто занимали диск, их пишут структурно, связывают с трейсами и хранят ровно столько, сколько нужно.
Структурные логи
Строка ERROR failed to charge card for user 42 удобна человеку, но её трудно фильтровать. Структурный лог в JSON:
{"ts":"2026-10-15T10:01:02.345Z","level":"error","service":"billing","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","user_id":42,"msg":"charge failed","error":"card declined","duration_ms":312}
Теперь можно найти все ошибки сервиса, все события одного пользователя или запроса, посчитать среднее duration_ms.
Правила:
- Одно событие, одна строка (многострочные стектрейсы сериализуют в поле).
- Постоянные поля: время в UTC с миллисекундами, уровень, сервис, версия, окружение,
trace_id. - Сообщение
msgпостоянное, детали в отдельных полях: не"user 42 not found", а"msg":"user not found","user_id":42. Тогда события группируются. - В контейнерах писать в stdout, сбор и доставку делает агент.
Уровни
- ERROR: что-то не сделано и требует внимания.
- WARN: необычное, но обработанное (повтор удался, деградация).
- INFO: значимые бизнес-события и жизненный цикл (старт, конфиг, завершение задачи).
- DEBUG: детали для отладки, в проде выключен или включается точечно.
Ошибка, которую обработали и вернули клиенту 404, обычно не ERROR. Если ERROR сыпется тысячами в норме, им перестают верить.
Корреляция
trace_id (или request_id) в каждой строке связывает логи одного запроса во всех сервисах. OpenTelemetry SDK кладёт идентификаторы трейса в контекст логгера автоматически. В Grafana из строки лога можно перейти к трейсу, а из трейса к его логам.
Доставка
Агент на узле читает stdout контейнеров или файлы и отправляет в хранилище: Fluent Bit, Vector, Grafana Alloy, Promtail (устарел, его заменяет Alloy), Filebeat, OpenTelemetry Collector. Агент буферизует при недоступности хранилища, добавляет метки (namespace, pod, node) и может отбрасывать мусор.
Loki
Loki индексирует только метки потока ({app="api", env="prod", namespace="billing"}), а содержимое строк хранит сжатыми кусками в объектном хранилище. Поэтому он дешёв и прост, но полнотекстовый поиск по большому периоду медленнее, чем в Elasticsearch.
LogQL:
{app="api", env="prod"} |= "error" # строки с подстрокой
{app="api"} | json | status >= 500 | line_format "{{.msg}}" # разобрать JSON и фильтровать по полю
sum by (app) (rate({env="prod"} |= "panic" [5m])) # метрика из логов
count_over_time({app="api"} | json | level="error" [1h])
Главное правило Loki: метки должны иметь низкую кардинальность. user_id или trace_id в метках создают миллионы потоков и убивают Loki. Такие значения остаются в теле строки и ищутся фильтрами.
Elasticsearch и OpenSearch (ELK)
Классический стек: Elasticsearch (хранилище с полнотекстовым инвертированным индексом), Logstash или Beats для сбора и обработки, Kibana для поиска. OpenSearch это открытый форк Elasticsearch и Kibana, который ведёт AWS и сообщество.
Плюсы: быстрый полнотекстовый поиск и сложная аналитика по полям. Минусы: индексирование всего дорого по CPU, памяти и диску, кластер требует внимания (шарды, реплики, mapping, переполнение полей). Для управления жизненным циклом используют ILM (горячие, тёплые, холодные узлы и удаление по сроку).
Ещё вариант для больших объёмов: ClickHouse как хранилище логов, часто в РФ.
Хранение и стоимость
- Разный ретеншн: DEBUG и access-логи дни, бизнес-события и аудит месяцы и годы, часто в отдельном хранилище.
- Сэмплирование однотипных событий (каждый сотый успешный запрос, но все ошибки).
- Мусор отбрасывать на агенте: health-check запросы, шумные библиотеки.
- Логи не заменяют метрики: считать RPS по логам дорого. Метрики из логов уместны для редких событий.
Персональные данные
Логи с телефонами, паспортами, токенами и номерами карт превращают хранилище логов в базу персональных данных со всеми требованиями 152-ФЗ, а утечка логов становится утечкой ПД. Маскируйте на стороне приложения (или на агенте), не логируйте тела запросов целиком, токены и пароли никогда.
Частые ошибки
- Текстовые логи без структуры, разный формат в каждом сервисе.
- Нет
trace_id: невозможно собрать картину запроса. trace_idили ID пользователя в метках Loki.- ERROR на каждую ожидаемую ситуацию.
- Пароли и персональные данные в логах.
Что спросят на собесе
- Зачем структурные логи и какие поля обязательны.
- Чем Loki отличается от Elasticsearch и когда что выбрать.
- Как связать логи разных сервисов одного запроса.
- Как снизить стоимость логов.
- Что нельзя писать в логи.