Что такое уровни логирования в Python и зачем они нужны
В модуле logging из стандартной библиотеки Python есть пять уровней важности: DEBUG, INFO, WARNING, ERROR и CRITICAL. У каждого уровня своё числовое значение, а логгер пропускает только записи с уровнем не ниже заданного порога. Поэтому один параметр level в конфигурации решает, попадёт событие в консоль, файл или централизованное хранилище.
print такого управления не даёт. Он пишет всё в stdout, не различает важность сообщений, не добавляет время, имя модуля и номер строки, не умеет писать в файл с автоматической ротацией и не отправляет записи в syslog или внешние системы. Когда сервис под нагрузкой, поток одинаковых строк без меток времени и уровней разбирать долго.
Модуль logging входит в стандартную библиотеку, дополнительных пакетов для базовой работы не требуется. Короткий ответ по сути вопроса: DEBUG=10, INFO=20, WARNING=30, ERROR=40, CRITICAL=50, и корневой логгер по умолчанию имеет уровень WARNING, то есть сообщения DEBUG и INFO без явной настройки никуда не выводятся.
Уровни дают три практических эффекта: фильтрацию сообщений по важности, управление объёмом логов и предсказуемые правила для всей команды. Кто пишет в лог на уровне ERROR, тот берёт на себя обязательство разбирать эти записи. Кто пишет всё подряд на DEBUG в продакшене, тот платит за это диском и шумом в поиске. Подробный разбор критериев отбора событий есть в материале что логировать, а что нет: практические критерии.
Пять уровней логирования: назначение и порядок фильтрации
Уровни выстроены по возрастанию важности, и порог работает как отсечка снизу. Ниже таблица с числовыми значениями и типичными сценариями.
| Уровень | Число | Когда использовать | Пример события |
|---|---|---|---|
| DEBUG | 10 | Разработка, отладка, разбор инцидента | Значения переменных, SQL-запросы, вход и выход из функции |
| INFO | 20 | Штатная работа сервиса | Старт и остановка, успешный вход, ответ на запрос |
| WARNING | 30 | Ситуация не сломала работу, но требует внимания | Fallback-конфиг, приближение к лимиту, устаревший вызов API |
| ERROR | 40 | Сбой операции, приложение продолжает работать | Таймаут внешнего API, ошибка запроса к БД |
| CRITICAL | 50 | Работа невозможна, нужно вмешательство | Потеря соединения с БД, исчерпание памяти, падение воркера |
DEBUG: детальная отладка
DEBUG несёт подробности, нужные при разработке и разборе сложных дефектов: содержимое структур данных, параметры вызовов, текст SQL. В продакшене этот уровень обычно выключен, потому что объём таких логов растёт быстрее, чем нагрузка, а в сообщениях легко оказаться рядом с токенами и персональными данными.
logging.debug("Переменная x = %s", x)
logging.debug("Выборка вернула %d строк за %.3f с", len(rows), elapsed)
Обратите внимание на стиль вызова: подстановка значений через аргументы, а не через f-строку. Форматирование выполняется только тогда, когда сообщение реально проходит фильтр уровня.
INFO: подтверждение нормальной работы
INFO фиксирует штатные события, по которым потом восстанавливают картину работы сервиса: старт и остановка, успешная аутентификация, завершение обработки запроса, применение миграции. Для большинства продакшен-сервисов это основной уровень.
logging.info("Сервис запущен на порту %d", port)
logging.info("Обработан запрос %s за %d мс", request_id, duration_ms)
WARNING: потенциальные проблемы
WARNING описывает ситуации, в которых код отработал, но не так, как задумано: не нашёлся файл конфигурации и подставились значения по умолчанию, пул соединений заполнен на 90 процентов, внешняя библиотека помечена как устаревшая. Такие записи читают при разборе проблем, но ночных дежурств не поднимают.
logging.warning("Файл конфигурации не найден, используются значения по умолчанию")
logging.warning("Пул соединений занят на %d%%", usage_percent)
ERROR: ошибки, не останавливающие программу
ERROR ставят там, где операция провалилась, но приложение продолжает обслуживать запросы: таймаут внешнего API, недоступность реплики БД, ошибка валидации ответа. Для исключений указывайте exc_info=True, иначе в логе останется только текст без трассировки, и разбор займёт больше времени.
try:
response = client.get(url, timeout=2)
except TimeoutError:
logging.error("Таймаут при запросе к API %s", url, exc_info=True)
CRITICAL: критические сбои
CRITICAL означает, что продолжать работу нельзя: потеряно соединение с основной базой, закончилась память, не поднялся обязательный воркер. Такие сообщения обычно сопровождаются аварийным завершением процесса, а хендлер для них отправляет уведомление в систему оповещения.
logging.critical("Не удалось подключиться к базе данных, завершение работы", exc_info=True)
sys.exit(1)
Как работает фильтрация: порядок и числовые значения уровней
Уровень есть и у логгера, и у каждого хендлера. Сообщение проходит дальше только при выполнении двух условий: его уровень не ниже уровня логгера и не ниже уровня конкретного хендлера. Логгер с уровнем INFO и хендлер с уровнем ERROR не пропустят ни INFO, ни WARNING в этот хендлер.
Порог логгера, который применяется на практике, называется эффективным уровнем: если у логгера level не задан, берётся уровень родителя, вплоть до корневого. Отсюда частая ситуация, когда логгер молчит, потому что унаследовал WARNING от root, хотя код исправно вызывает logging.info.
import logging
logger = logging.getLogger("billing")
logger.setLevel(logging.INFO) # порог самого логгера
print(logger.getEffectiveLevel()) # 20
root = logging.getLogger()
print(root.getEffectiveLevel()) # 30, пока никто не поменял
Собственные уровни добавляют через logging.addLevelName, например NOTICE между INFO и WARNING. Это оправдано в редких случаях, когда встроенных пяти действительно не хватает, а внешние системы сбора логов умеют работать с нестандартными значениями.
logging.addLevelName(25, "NOTICE") logging.log(25, "Плановая задача выполнена с замечаниями")
Настройка логирования: форматтеры, хендлеры и фильтры
Базовая настройка укладывается в один вызов basicConfig. Его достаточно для скриптов и небольших сервисов: задаёте уровень, формат и файл назначения.
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(process)d %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
filename="/var/log/app/service.log",
encoding="utf-8",
)
Форматтеры: настройка вывода
Форматтер собирает строку лога из атрибутов записи. Минимально полезный набор: время, уровень, имя логгера, сообщение. Для разбора проблем добавляют идентификатор процесса и потока, а для распределённых систем - идентификатор запроса, переданный через extra.
fmt = logging.Formatter(
"%(asctime)s %(levelname)s %(name)s [%(process)d] %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S%z",
)
logger.info("Заказ создан", extra={"request_id": "r-8f31"})
Если планируете собирать логи в Elasticsearch или Loki, строковый формат придётся парсить. Готовый набор полей и конфиги коллекторов разобраны в статье структурированные логи: переход от plain text к JSON.
Хендлеры: разделение потоков вывода
Разделение stdout и stderr удобно в контейнерах: сборщик забирает оба потока, но вы можете независимо настроить их уровень и развести шум отладки и ошибки. Уровень DEBUG ниже ERROR, поэтому в поток stdout добавляют фильтр, который отсекает ошибки, иначе они продублируются.
import logging
import sys
class BelowErrorFilter(logging.Filter):
def filter(self, record):
return record.levelno < logging.ERROR
logger = logging.getLogger("api")
logger.setLevel(logging.DEBUG)
logger.propagate = False
fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
out = logging.StreamHandler(sys.stdout)
out.setLevel(logging.DEBUG)
out.addFilter(BelowErrorFilter())
out.setFormatter(fmt)
err = logging.StreamHandler(sys.stderr)
err.setLevel(logging.ERROR)
err.setFormatter(fmt)
logger.addHandler(out)
logger.addHandler(err)
Фильтры: тонкая настройка
Фильтр получает запись и возвращает True либо False. Так отсекают сообщения от конкретного логгера, отбрасывают технический шум сторонних библиотек и маскируют чувствительные значения. Маскировать надёжнее в фильтре, чем в каждом вызове, потому что правило применяется ко всем сообщениям логгера.
import logging
class DropSecretsFilter(logging.Filter):
BLOCKED = ("password", "token", "secret", "authorization", "api_key")
def filter(self, record):
text = record.getMessage().lower()
if any(word in text for word in self.BLOCKED):
record.msg = "[сообщение скрыто фильтром]"
record.args = ()
return True
handler = logging.StreamHandler()
handler.addFilter(DropSecretsFilter())
Ротация файлов логов: управление размером и хранением
Файл логов растёт до тех пор, пока не закончится место. Автоматическую ротацию решают два класса из logging.handlers: RotatingFileHandler меняет файл по размеру, TimedRotatingFileHandler по расписанию.
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
by_size = RotatingFileHandler(
"/var/log/app/api.log",
maxBytes=10 * 1024 * 1024, # 10 МБ на файл
backupCount=5, # храним 5 архивов
encoding="utf-8",
)
by_size.setLevel(logging.INFO)
by_time = TimedRotatingFileHandler(
"/var/log/app/api-daily.log",
when="midnight",
interval=1,
backupCount=14, # две недели истории
utc=True,
encoding="utf-8",
)
by_time.setLevel(logging.INFO)
При ротации по времени utc=True избавляет от путаницы при переходе на летнее время и упрощает сопоставление логов из разных регионов. Для контейнеров ротация внутри приложения часто не нужна: файловую систему контейнера пересоздают, а логи забирает драйвер логирования и внешний сборщик.
Декларативная конфигурация через logging.config.dictConfig
dictConfig описывает форматтеры, хендлеры, фильтры и логгеры в одном словаре. Конфигурацию можно держать в отдельном модуле, грузить из JSON и повторять в разных сервисах почти без правок, а менять поведение логирования без переписывания кода вызовов.
import logging.config
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {"format": "%(asctime)s %(levelname)s %(name)s %(message)s"},
},
"filters": {
"drop_secrets": {"()": "app.logging_filters.DropSecretsFilter"},
"below_error": {"()": "app.logging_filters.BelowErrorFilter"},
},
"handlers": {
"stdout": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "standard",
"filters": ["below_error", "drop_secrets"],
"stream": "ext://sys.stdout",
},
"stderr": {
"class": "logging.StreamHandler",
"level": "ERROR",
"formatter": "standard",
"stream": "ext://sys.stderr",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
"formatter": "standard",
"filename": "/var/log/app/api.log",
"maxBytes": 10485760,
"backupCount": 5,
"encoding": "utf-8",
},
},
"loggers": {
"api": {
"handlers": ["stdout", "stderr", "file"],
"level": "DEBUG",
"propagate": False,
},
},
"root": {"handlers": ["stdout"], "level": "WARNING"},
}
logging.config.dictConfig(LOGGING)
Флаг disable_existing_loggers: False оставьте включённым по умолчанию: при значении True логгеры, созданные до вызова dictConfig, замолчат. Классы фильтров и собственные форматтеры подключаются через ключ "()" с полным путём импорта. Расширенная конфигурация с переменными окружения, Docker и Kubernetes разобрана в гайде настройка логирования в Python: от основ до промышленного развёртывания.
Типичные сценарии использования уровней в продакшене
Разработка и отладка. Уровень DEBUG, вывод в консоль, максимально подробный форматтер с именем файла и номером строки. Включение через переменную окружения LOG_LEVEL, чтобы не править код перед деплоем.
level = os.getenv("LOG_LEVEL", "DEBUG").upper()
logging.basicConfig(level=level, format="%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(message)s")
Продакшен-сервис. Уровень INFO, запись в файл с ротацией и одновременно в stdout для контейнера. DEBUG выключен, WARNING и выше попадают в отдельный поток, чтобы ошибки было видно в мониторинге без чтения всего файла.
logger.setLevel(logging.INFO)
file_handler = RotatingFileHandler("/var/log/app/api.log", maxBytes=10485760, backupCount=5)
file_handler.setLevel(logging.INFO)
logger.addHandler(file_handler)
Мониторинг ошибок. Уровень ERROR с отправкой в Sentry, Elasticsearch или Loki. Здесь особенно важно передавать трассировку и контекст: идентификатор запроса, пользователя (без персональных данных), версию сборки.
Аудит безопасности. Уровень INFO для событий входа, смены прав и работы с секретами, с обязательным маскированием. Для высоконагруженных сервисов учитывайте стоимость логирования: на уровне DEBUG объём записей может вырасти на порядок. Приёмы, которые снижают накладные расходы, собраны в статье как ускорить логирование в Python без потери детальности отладки.
Интеграция с внешними системами сбора логов
Отправлять логи в централизованное хранилище можно штатными средствами: SysLogHandler пишет в syslog, HTTPHandler отправляет записи по сети, а в Kubernetes достаточно правильно настроить stdout и stderr, потому что логи контейнера забирает среда исполнения.
from logging.handlers import SysLogHandler
syslog_handler = SysLogHandler(address=("log-collector.internal", 514))
syslog_handler.setLevel(logging.WARNING)
syslog_handler.setFormatter(logging.Formatter("%(name)s %(levelname)s %(message)s"))
logger.addHandler(syslog_handler)
Для централизованного сбора добавьте в каждое сообщение два поля: имя сервиса и корреляционный идентификатор запроса, который приходит из заголовка или генерируется на входе. Тогда по одному request_id собирается цепочка событий между сервисами. Схема маршрутизации логов и метрик, включая настройку коллекторов и визуализацию, описана в руководстве маршрутизация логов и метрик в DevOps. JSON в качестве формата упрощает фильтры по level и service, потому что поля извлекаются напрямую, без регулярных выражений.
Безопасность логов: чек-лист против утечек
Логи часто доступны шире, чем база данных: их читают разработчики, дежурные, внешние сборщики и архивные хранилища. Проверьте конфигурацию по списку.
- Пароли, токены, ключи API, номера карт и персональные данные не попадают в сообщения ни на одном уровне.
- Фильтр маскирует подозрительные записи: подстрока password, token, secret, authorization заменяется на заглушку.
- Права на файлы логов ограничены: владелец - сервисный пользователь, режим 640, каталог без доступа для посторонних.
- Ротация и срок хранения заданы так, чтобы старые логи удалялись автоматически.
- Перед отправкой во внешнюю систему проверьте, что в поток не уходят тела запросов и заголовки Authorization целиком.
- В продакшене уровень DEBUG выключен; для разбора инцидента его включают точечно и на короткое время.
- Аудит логов проводится регулярно: ищите в них секреты и лишние персональные данные, а не только ошибки.
Пример фильтра с маскированием приведён выше, в разделе о фильтрах. Полный набор правил, что можно и что нельзя писать в лог, есть в отдельном чек-листе по критериям логирования, ссылка на него дана в начале статьи.
Частые ошибки при настройке логирования и как их избежать
- Дублирование сообщений. Логгер с propagate=True и собственный хендлер плюс хендлер на корневом логгере дают две одинаковые строки. Решение: оставить propagate=False для сервисных логгеров или не добавлять хендлеры на root.
- Игнорирование уровня хендлера. Логгер с уровнем DEBUG и хендлер с уровнем WARNING отбросят отладку, и будет казаться, что код не выполняется. Решение: проверять getEffectiveLevel и level каждого хендлера.
- print вместо logging. Вывод не попадает в файл, не имеет времени и уровня, не собирается централизованно. Решение: перевести вывод на логгер модуля через logging.getLogger(__name__).
- Чувствительные данные в сообщениях. Токены и тела запросов оседают в файлах и архивах. Решение: фильтр с маскированием и запрет на логирование объектов целиком.
- Отсутствие ротации. Один файл растёт до заполнения диска. Решение: RotatingFileHandler с maxBytes и backupCount или TimedRotatingFileHandler с backupCount.
- Форматтер, ломающий парсинг. Многострочные сообщения и запятые в поле времени разбивают строку на части. Решение: JSON-формат или фиксированный порядок полей с ISO-8601 временем.
Заключение
Уровни DEBUG, INFO, WARNING, ERROR и CRITICAL задают правила игры: DEBUG для отладки, INFO для штатной работы, WARNING для внимания, ERROR для сбоев операции, CRITICAL для остановки сервиса. Порог работает как отсечка снизу, а уровень хендлера проверяется отдельно от уровня логгера.
Дальше практика: замените print на логгер модуля, задайте уровень INFO в продакшене, добавьте RotatingFileHandler с maxBytes и backupCount, вынесите конфигурацию в dictConfig и подключите фильтр, который скрывает секреты. После деплоя проверьте результат: вызовите по одному сообщению на каждом уровне и убедитесь, что в stdout попали только записи ниже ERROR, в stderr только ошибки и критические сбои, а в файл всё от INFO и выше.