Как выбрать уровень логирования для приложения: практические рекомендации на 2026 год | AdminWiki

Как выбрать уровень логирования для приложения: практические рекомендации на 2026 год

01 октября 2026 13 мин. чтения
Содержание статьи

Почему выбор уровня логирования - не «поставить INFO и забыть»

Уровень логирования выбирают по трём признакам: стадия жизненного цикла приложения, тип данных в сообщении и действие, которое должен выполнить читатель лога. Отладочные детали нужны на dev-стенде, события жизненного цикла в staging, инциденты в production. Строка, после которой ничего делать не нужно, в логе лишняя, и её уровень завышен.

В 2026 году цена ошибки выше, чем пять лет назад. Объёмы логов растут вместе с трафиком, а хранение и индексация в облачных системах видны в счёте отдельной строкой. GDPR, его национальные аналоги и PCI DSS ограничивают и то, что попадает в лог, и срок хранения этих записей. Конкретные пункты стандартов сверяйте с действующей редакцией: требования обновляются чаще, чем внутренние инструкции команд.

Три сценария, которые регулярно всплывают при разборе инцидентов:

  • DEBUG включили для диагностики и забыли выключить. В лог уходят тела HTTP-запросов, параметры SQL и структуры объектов, вместе с ними пароли, токены и email.
  • INFO используют как универсальный уровень для всего, включая каждый запрос и каждую итерацию цикла. Сигнал тонет в шуме, и полезные сообщения никто не читает.
  • ERROR выставляют для ожидаемых бизнес-ситуаций вроде «пользователь не найден». Дежурный привыкает к ложным алертам и перестаёт реагировать на настоящие.

Дальше разберём семантику уровней, дадим матрицу по стадиям, рабочие конфиги для Python, Java и Go, чек-лист ревью и команды проверки.

Уровни логирования INFO, WARN, ERROR и другие: что они значат на практике

Правило одинаково для всех библиотек: уровень описывает не важность сообщения в глазах автора кода, а реакцию читателя. Если строка не требует действия, её уровень завышен, и алертинг по этому уровню превратится в шум.

УровеньЧисловое значениеКогда писатьДействие читателя
DEBUG10Отладка, разбор конкретного запроса, работа на dev-стендеНикакого, это среда разработки
INFO20Старт и остановка сервиса, миграция, смена конфигурации, успешная транзакцияПонять контекст при расследовании
WARN30Повтор запроса, переход на резервный источник, деградация, истекающий сертификатПроверить, если проблема повторится
ERROR40Операция не выполнена, исключение, недоступность внешней системыРеагировать, при необходимости по алерту
FATAL, CRITICAL50Потеря консистентности данных, состояние, при котором сервис не может продолжать работуНемедленное вмешательство и остановка

Числовые значения совпадают не везде, порядок один и тот же. В Python logging: DEBUG=10, INFO=20, WARNING=30, ERROR=40, CRITICAL=50. В SLF4J (Java): TRACE=0, DEBUG=10, INFO=20, WARN=30, ERROR=40. В Go slog уровни идут с шагом 4: LevelDebug=-4, LevelInfo=0, LevelWarn=4, LevelError=8. Фильтрация работает одинаково: логгер пропускает сообщения с уровнем не ниже установленного порога.

DEBUG: почему он опасен в продакшене

DEBUG пишет всё, что нужно разработчику при отладке: тела запросов, фрагменты ответов внешних API, параметры SQL, значения переменных. Первый риск, утечка данных. ORM с включённым выводом параметров запроса сохранит в файл значения из WHERE и INSERT, включая email, телефон и номера документов. Второй риск, объём. Миллион запросов в сутки при теле около 1 КБ даёт порядка гигабайта логов в день на одном сервисе, и это без учёта ретраев и фоновых задач. Третий риск, скорость: синхронная запись на диск добавляет задержку в обработку каждого запроса.

Рабочее правило: DEBUG живёт на dev-стендах. В production его включают точечно и на время, обычно через динамическую конфигурацию: logback с параметром scan="true" и внешним файлом, Spring Boot Actuator с эндпоинтом /loggers, atomic level в Go. Факт включения DEBUG стоит логировать отдельным сообщением на уровне WARN, чтобы потом было понятно, откуда в логе появились подробности.

Проверка конфигов перед деплоем:

grep -rn 'DEBUG' /etc/app/ /opt/app/config/
grep -rn 'logging.level' /opt/app/application.yml
grep -rn 'LOG_LEVEL' /etc/systemd/system/app.service

INFO: когда он теряет ценность

INFO теряет смысл в двух случаях: сообщение пишется в цикле и сообщение дублирует то, что уже видно на другом уровне. Классический пример: логирование каждого HTTP-запроса на INFO в сервисе с сотнями запросов в секунду. За сутки набегают миллионы строк, а реальные проблемы в них не находятся поиском. Запросы лучше смотреть в access-логах reverse proxy (Nginx) или через sampling, а в приложении оставить значимые события: старт и остановку, применение миграций, смену конфигурации, завершение критичной транзакции.

Готовые критерии, какие события вообще заслуживают записи, разобраны в материале что логировать, а что нет.

В Go удобно опираться на slog.InfoContext и писать INFO только на входе и выходе значимых операций, а не внутри обработчика каждого запроса.

WARN и ERROR: где проходит граница

WARN описывает ситуацию, которая не сломала работу, но требует внимания: повтор после таймаута, переход на резервный источник, падение свободного места, сертификат с истекающим сроком. ERROR означает, что операция не выполнена и без вмешательства не выполнится: исключение, недоступность внешнего сервиса, ошибка записи в базу.

Антипаттерн, который ломает алертинг: бизнес-ошибки на уровне ERROR. Сообщение «пользователь не найден» при неверном логине это ожидаемый результат валидации, ему место на INFO или WARN в зависимости от того, интересует ли вас статистика. Проверка простая: если ошибка ожидаема и обработана кодом, это WARN или INFO; если нет, ERROR. В SLF4J это log.warn для ретраев и log.error для исключений, в Go slog.Warn для деградации и slog.Error для отказов.

Матрица выбора уровня логирования по стадиям: dev, staging, production

СтадияУровень по умолчаниюЧто логироватьЧто не логировать
devDEBUGТела запросов, SQL с параметрами, работу кэша, ветвления логикиРеальные персональные данные, боевые ключи API, выгрузки из продакшена
stagingINFO плюс DEBUG точечноИнтеграции с внешними системами, миграции, старт и остановку, результаты нагрузочных прогоновСекреты тестовых стендов в открытом виде, тела платёжных запросов
productionINFOСобытия жизненного цикла, WARN при деградации, ERROR при отказах, audit-события безопасностиПароли, токены, тела запросов, параметры SQL, полные номера карт и документов

Матрица задаёт отправную точку, а не догму. Для высоконагруженных сервисов уровень остаётся тем же, а объём снижается через sampling и rate limiting на стороне коллектора.

Как менять уровень логирования без перезапуска приложения

Диагностика в production редко совпадает с плановым деплоем, поэтому уровень меняют на лету. В Spring Boot для этого есть Actuator:

curl -X POST http://localhost:8080/actuator/loggers/com.example -H 'Content-Type: application/json' -d '{"configuredLevel":"DEBUG"}'

Сброс обратно: то же самое с configuredLevel: "INFO" или DELETE на тот же адрес. В не-Spring проектах на logback работает scan="true" с внешним файлом: правите XML, через 30 секунд конфигурация перечитывается. В Go уровень держат в slog.LevelVar и меняют его из внутреннего HTTP-эндпоинта или по сигналу. Логируйте каждое переключение и ограничивайте эндпоинт смены уровня локальной сетью или отдельным портом, иначе получите способ залить диск по внешнему запросу.

Производительность и стоимость: что меняет уровень

Стоимость логирования складывается из трёх частей: накладные расходы на форматирование и запись, объём хранения и цена индексации. Переход с DEBUG на INFO обычно сокращает объём в разы, а не на проценты, потому что отладочные записи многословны и часто пишутся в циклах. Приёмы, которые снижают накладные расходы без потери детальности: ленивое форматирование вместо конкатенации строк, асинхронные хэндлеры с буферизацией, sampling для однотипных событий. Практические сниппеты собраны в статье про оптимизацию логирования в Python.

Безопасность логирования: как не слить пароли, токены и PII

Список того, что не должно попадать в логи: пароли и их хеши, токены и ключи API, заголовок Authorization целиком, номера банковских карт, коды CVV, номера документов, полные email и телефоны там, где они не нужны для разбора инцидента, содержимое .env и переменных окружения. Отдельная категория, тела запросов к платёжным и медицинским API.

Маскирование делают на уровне приложения, а не только в коллекторе. Причина простая: до попадания в Logstash или Fluentd лог лежит в файле на диске и доступен каждому, у кого есть доступ к хосту или к томуе контейнера.

Проверка на утечки по уже собранным логам:

grep -iE 'password|passwd|secret|token|api[_-]?key|authorization' /var/log/app/*.log | head -50
grep -iE '[0-9]{13,19}' /var/log/app/*.log | head -20

Маскирование секретов в конфигах Python, Java и Go

В Python проще всего добавить logging.Filter, который чистит сообщение до форматирования. Регулярное выражение ищет пары вида key=value и заменяет значение:

class MaskFilter(logging.Filter):
    PATTERN = re.compile(r'(?i)(password|token|secret|api_key)["\s:=]+([^\s"]+)')
    def filter(self, record):
        record.msg = self.PATTERN.sub(r'\1=***', str(record.msg))
        return True

В Java есть два уровня. Для консольного паттерна подходит conversion word %replace, для JSON-вывода через logstash-logback-encoder настраивают ReplacingCompositeConverter, который правит поле message целиком. В Go пишут собственный slog.Handler: он оборачивает базовый JSON-хендлер и заменяет значения атрибутов по списку ключей (password, token, secret, authorization). Маскирование по ключу надёжнее маскирования по регулярке в тексте, потому что структурированные логи дают доступ к имени поля, а не к строке целиком.

Полный набор приёмов для маскировки паролей и PII, а также настройка разграничения доступа к логам описаны в руководстве по защите данных в логах.

Защита от log injection и CRLF-атак

Log injection возникает, когда пользовательские данные попадают в лог без обработки. Злоумышленник передаёт в поле имени символ перевода строки, и в файле появляется поддельная запись, например строка о входе под root. Меры защиты: экранировать управляющие символы, использовать структурированный формат JSON вместо конкатенации, валидировать входные данные по допустимым символам. В Python подойдёт json.dumps для полезной нагрузки, в Java logstash-logback-encoder, в Go slog.JSONHandler.

Проверка: отправьте запрос, в котором параметр содержит %0A и текст «ERROR: payment failed», затем поищите эту строку в логе. Если она нашлась как отдельная запись с вашим уровнем, экранирования нет.

Готовые конфиги логирования для Python, Java и Go

Каждый конфиг ниже читает уровень из переменной окружения, пишет JSON и маскирует секреты. Различия только в синтаксисе.

Python: dictConfig с JSON и маскированием

import logging, logging.config, os, re

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "filters": {"mask": {"()": "app.log_utils.MaskFilter"}},
    "formatters": {"json": {"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
        "format": "%(asctime)s %(levelname)s %(name)s %(request_id)s %(message)s"}},
    "handlers": {"stdout": {"class": "logging.StreamHandler", "formatter": "json", "filters": ["mask"]}},
    "root": {"level": os.environ.get("LOG_LEVEL", "INFO"), "handlers": ["stdout"]},
}

logging.config.dictConfig(LOGGING)

Проверка: LOG_LEVEL=DEBUG python app.py и просмотр вывода через jq. Версии: Python 3.12 и выше, python-json-logger актуальной версии. Для Django и Flask схема та же, отличается только место инициализации; детали для этих фреймворков разобраны в материале про логирование Django и Flask. Базовые настройки модуля logging под промышленную эксплуатацию собраны в статье про настройку логирования в Python.

Java: logback-spring.xml с уровнями и маскированием

<configuration scan="true" scanPeriod="30 seconds">
  <springProperty scope="context" name="LOG_LEVEL" source="logging.level.root" defaultValue="INFO"/>
  <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
  </appender>
  <root level="${LOG_LEVEL}">
    <appender-ref ref="JSON"/>
  </root>
</configuration>

Для текстового вывода добавьте маскирование прямо в паттерн:

pattern="%d{yyyy-MM-dd HH:mm:ss} %-5level [%X{request_id}] %logger{36} - %replace(%msg){'(?i)(password|token|secret)=[^ ]+', '$1=***'}%n"

Проверка уровня на живом приложении: GET /actuator/loggers/com.example покажет configuredLevel и effectiveLevel, POST меняет значение. Версии: logback 1.5.x, Spring Boot 3.x. Для проектов без Spring тот же файл кладут в classpath как logback.xml, параметр scan="true" оставляют, а уровень задают через ${LOG_LEVEL} из переменной окружения.

Go: slog с JSONHandler и контекстом

level := slog.LevelInfo
switch os.Getenv("LOG_LEVEL") {
case "DEBUG":
    level = slog.LevelDebug
case "WARN":
    level = slog.LevelWarn
case "ERROR":
    level = slog.LevelError
}

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
slog.SetDefault(logger)

Для смены уровня без перезапуска используют slog.LevelVar:

var lvl slog.LevelVar
lvl.Set(slog.LevelInfo)
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &lvl})
// включение диагностики из внутреннего эндпоинта
lvl.Set(slog.LevelDebug)

Проверка: go run main.go | jq и фильтр по полю level. Пакет slog появился в Go 1.21, для новых проектов он закрывает большинство задач. Если проект уже использует zap или zerolog, логика та же: уровень из окружения, JSON на stdout, атрибут request_id в каждом сообщении.

Контекст и структура логов: как не утонуть в шуме

Структурированный лог это JSON с фиксированными полями: timestamp, level, service, request_id, user_id, message. По таким полям работают фильтры в Loki, Elasticsearch и других коллекторах, а человек читает их без парсинга регулярками. Текстовые логи со склейкой строк ломают и поиск, и маскирование.

Минимальный набор полей, который стоит завести с первого дня: service, env, request_id, user_id (или внутренний идентификатор вместо email), duration_ms, status. Поля с высокой кардинальностью вроде полного URL с query-параметрами в индекс не выносят, иначе он разрастётся.

Correlation ID и trace ID в микросервисах

Схема, которая работает: входной прокси присваивает идентификатор запроса и прокидывает его в заголовке. В Nginx это proxy_set_header X-Request-ID $request_id. Приложение читает заголовок, кладёт значение в контекст логирования (MDC в Java, context в Go, contextvars в Python) и добавляет во все сообщения. Дальше идентификатор передаётся в HTTP-заголовках при вызовах соседних сервисов, поэтому цепочка собирается одним поиском.

Проверка: curl -H 'X-Request-ID: test-123' http://localhost:8080/api/health, затем поиск строки test-123 в логах всех сервисов, участвовавших в обработке. Если идентификатор нашёлся только в одном сервисе, передача заголовка дальше не настроена. Для распределённой трассировки применяют W3C Trace Context с заголовком traceparent и OpenTelemetry, но request_id полезно оставить даже при наличии трейсинга: он живёт в логах, доступных без отдельной инфраструктуры.

Типичные ошибки логирования и как их исправить

  • Секреты в логе. Django REST Framework с телом запроса на DEBUG сохранит пароль из формы регистрации, Nginx с включённым логированием заголовков сохранит Authorization. Исправление: маскирование на уровне приложения плюс правка формата access-лога.
  • Логирование в цикле. Обработка 50 000 записей с сообщением на каждой даёт 50 000 строк на один запуск. Исправление: агрегировать результат и писать одну итоговую строку.
  • Log and throw. Исключение логируют в catch и пробрасывают выше, где логируют повторно. Исправление: логировать в одном месте, обычно на границе обработки запроса, остальным уровням оставить контекст.
  • ERROR для бизнес-ошибок. Валидация, «не найдено», «недостаточно средств» это ожидаемые результаты. Исправление: перевести на INFO или WARN и не заводить по ним алерты.
  • Отсутствие ротации. Файл растёт до заполнения раздела, и падает не логирование, а приложение. Исправление: logrotate для файлов, retention policy в Loki или Elasticsearch, лимиты на индекс.
  • Отсутствие контекста. Сообщение «ошибка обработки» без идентификатора запроса и параметров бесполезно. Исправление: добавить request_id, имя операции и код ошибки.

Отдельно стоит следить за диагностическими выводами фреймворков. Отладочные режимы сторонних библиотек пишут в тот же поток, что и ваши логи, и часто содержат подключения и параметры запросов. Проверяйте корневые логгеры, а не только свои модули: параметр root level из примера выше как раз задаёт общий порог.

Чек-лист ревью кода и конфигов логирования

  1. Уровень берётся из переменной окружения, хардкода DEBUG в продакшн-конфиге нет.
  2. Пароли, токены, ключи API и номера карт не попадают в сообщения ни на одном уровне.
  3. Формат структурированный (JSON), поля фиксированные.
  4. request_id присутствует в каждом сообщении, включая ошибки.
  5. Исключение логируется в одном месте, дублирования нет.
  6. Бизнес-ошибки не пишутся на ERROR.
  7. Ротация файлов и retention в коллекторе настроены.
  8. Отладочные выводы сторонних библиотек ограничены.
  9. Внутри циклов нет логирования на каждой итерации.
  10. Алерты заведены на ERROR и на ключевые WARN, а не на весь поток.

Автоматизация проверки: статический анализатор с набором правил на логирование (например, semgrep с собственными паттернами на password, token, Authorization) плюс grep по конфигам в CI. Команды для локального прогона:

grep -rn 'password\|token\|secret' src/ --include='*.py' --include='*.java' --include='*.go'
grep -rn 'DEBUG' config/ /etc/app/
grep -rn 'LOG_LEVEL' deploy/

Как проверить, что логирование настроено правильно

Проверка занимает несколько минут и закрывает большинство сюрпризов до деплоя.

  • Уровень в рантайме: journalctl -u app --since '10 min ago' | grep -c DEBUG. При production-конфиге счётчик должен быть равен нулю.
  • Маскирование: отправьте запрос с полем password и убедитесь, что в логе стоит ***.
  • Контекст: curl -H 'X-Request-ID: test-123' http://localhost:8080/api/health и поиск test-123 в выводе.
  • Контейнеры: docker logs --tail 100 app, для Kubernetes kubectl logs deploy/app --tail=100 или stern.
  • Ротация: ls -lh /var/log/app/ после принудительного запуска logrotate -f /etc/logrotate.d/app.
  • Объём: du -sh /var/log/app/ раз в сутки, чтобы увидеть динамику до заполнения диска.
  • Структура: вывод приложения через jq -c '. | select(.level=="ERROR")' покажет, парсится ли JSON.

Итог: алгоритм выбора уровня логирования за 5 шагов

  1. Определите стадию: dev, staging или production. От неё зависит уровень по умолчанию: DEBUG, INFO плюс точечный DEBUG, INFO.
  2. Составьте список событий для записи по каждому уровню, отталкиваясь от действия читателя, а не от важности события для автора кода.
  3. Настройте смену уровня без перезапуска: Actuator, scan="true", slog.LevelVar, и закройте эндпоинт смены уровня от внешней сети.
  4. Добавьте маскирование секретов и request_id, переведите вывод в JSON.
  5. Прогоните чек-лист и команды проверки из предыдущих разделов, настройте ротацию и алерты на ERROR.

Конфиги в статье даны как рабочие заготовки: подставьте свои имена модулей, пути и версии библиотек, проверьте поведение на staging и только потом переносите в production. Сохраните чек-лист рядом с инструкцией по деплою, чтобы возвращаться к нему при каждом изменении в логировании.

Поделиться: