Python logging: полный обзор инструментов для отладки и мониторинга
Логирование в Python: от базовых настроек до сложных примеров
Как настроить логирование минимальными усилиями?
Встроенная библиотека logging предоставляет гибкую систему записи сообщений. Самый быстрый способ - использовать basicConfig.
import logging
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
logger.info('Программа запущена')
logger.debug('Значение переменной x = 42')
Python logging (логирование в python)
Пояснения:
- basicConfig устанавливает уровень логирования (DEBUG) и формат вывода. Если вызвать до создания логгера, настройки применяются к корневому логгеру.
- getLogger(__name__) создаёт именованный логгер. Это позволяет отслеживать, из какого модуля пришло сообщение.
Типичные ошибки
- Вызов basicConfig после первого логирования - настройки не будут применены. Выход: вызывать basicConfig в самом начале программы.
- Использование корневого логгера без явного получения логгера (logging.info(...) вместо logger.info(...)). Это допустимо, но усложняет управление.
Как записывать логи в файл, а не в консоль?
Для вывода в файл используется аргумент filename в basicConfig.
import logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s %(message)s',
filename='app.log',
filemode='a') # 'a' - добавление, 'w' - перезапись
logger = logging.getLogger('file_logger')
logger.warning('Это предупреждение записано в файл')
Пояснения:
- filename задаёт путь к файлу. Если файл не существует, он создаётся.
- filemode определяет режим открытия: 'a' (по умолчанию) добавляет записи, 'w' перезаписывает.
Возможные проблемы
- Недостаточно прав на запись в директорию - ошибка PermissionError. Проверьте пути.
- Файл может расти бесконтрольно. Используйте ротацию (см. следующий раздел).
Как добавить временные метки и настроить формат сообщений?
Формат задаётся строкой с атрибутами, например %(asctime)s, %(levelname)s.
import logging
FORMAT = '[%(levelname)s] %(asctime)s - %(name)s: %(message)s'
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
logging.basicConfig(level=logging.WARNING, format=FORMAT, datefmt=DATE_FORMAT)
logger = logging.getLogger('format_logger')
logger.error('Ошибка подключения к базе данных')
Пояснения:
- datefmt позволяет указать свой формат даты/времени (по умолчанию - ISO 8601).
- Полный список атрибутов: %(name)s, %(levelno)s, %(pathname)s, %(funcName)s и другие.
Как организовать ротацию логов по размеру файла?
Вместо basicConfig с одним файлом используется RotatingFileHandler.
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger('rotate_logger')
logger.setLevel(logging.DEBUG)
handler = RotatingFileHandler('rotate.log', maxBytes=1000, backupCount=3)
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
for i in range(100):
logger.debug(f'Сообщение номер {i}')
Пояснения:
- maxBytes - максимальный размер файла (в байтах). При превышении создаётся новый файл.
- backupCount - количество резервных файлов. Старые удаляются автоматически.
Особенности
- Не используйте basicConfig вместе с добавлением своих хендлеров, иначе будут дублироваться записи.
- Если maxBytes равен 0, ротация отключается.
Как логировать исключения с полным traceback?
В простом случае используйте logger.exception() (автоматически добавляет stacktrace).
import logging
logging.basicConfig(level=logging.ERROR)
logger = logging.getLogger('exc_logger')
try:
x = 1 / 0
except ZeroDivisionError:
logger.exception('Произошла ошибка деления на ноль')
Результат выполнения (пример):
ERROR:exc_logger:Произошла ошибка деления на ноль Traceback (most recent call last): File "<stdin>", line 2, in <module> ZeroDivisionError: division by zero
Пояснения:
- logger.exception() автоматически вызывает exc_info=True. Если нужно логировать без исключения, то добавляют exc_info=True в любой вызов.
- Для ручного форматирования используйте logging.Formatter с %(exc_info)s.
Как настроить несколько обработчиков для разных уровней (например, ошибки в один файл, все сообщения - в другой)?
Создаются отдельные хендлеры, для каждого устанавливается уровень и формат, затем добавляются к логгеру.
import logging
logger = logging.getLogger('multi_logger')
logger.setLevel(logging.DEBUG)
# Хендлер для ошибок и выше
error_handler = logging.FileHandler('errors.log')
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
# Хендлер для всех сообщений (DEBUG и выше)
all_handler = logging.FileHandler('all.log')
all_handler.setLevel(logging.DEBUG)
all_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(error_handler)
logger.addHandler(all_handler)
logger.debug('Эта запись будет только в all.log')
logger.error('Эта запись попадёт в оба файла')
Пояснения:
- Каждый хендлер может иметь свой уровень - setLevel.
- Если не задать уровень на самом логгере, он наследуется от корневого (WARNING).
Проблема дублирования
Корневой логгер по умолчанию имеет обработчик, выводящий сообщения в консоль. Если вы добавляете свои хендлеры и не отключаете распространение, сообщения могут дублироваться. Используйте logger.propagate = False.
Как использовать конфигурацию из словаря (dictConfig) для больших проектов?
Метод logging.config.dictConfig() позволяет задать конфигурацию через словарь, включая логгеры, хендлеры, форматтеры.
import logging
import logging.config
LOGGING_CONFIG = {
'version': 1,
'formatters': {
'verbose': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
},
'simple': {
'format': '%(levelname)s: %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'formatter': 'simple'
},
'file': {
'class': 'logging.FileHandler',
'level': 'WARNING',
'filename': 'app.log',
'formatter': 'verbose'
}
},
'loggers': {
'myapp': {
'handlers': ['console', 'file'],
'level': 'DEBUG',
'propagate': False
}
}
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger('myapp')
logger.info('Информационное сообщение в консоль')
logger.warning('Предупреждение в оба обработчика')
Пояснения:
- Словарь описывает структуру логгеров и обработчиков. Ключ version обязателен.
- Позволяет менять настройки без изменения кода, например, из JSON-файла.
Расширенные примеры работы с логированием
Пример 1: Кастомный обработчик с отправкой логов по сети (SysLogHandler)
import logging
from logging.handlers import SysLogHandler
logger = logging.getLogger('syslog_logger')
logger.setLevel(logging.DEBUG)
handler = SysLogHandler(address='/dev/log')
handler.setFormatter(logging.Formatter('%(name)s: %(message)s'))
logger.addHandler(handler)
logger.critical('Критическая ошибка системы')
Результат (в системном журнале): сообщение появляется с facility user (по умолчанию). Для изменения facility передаётся facility аргумент.
Пример 2: Использование MemoryHandler для буферизации и отправки пакетов
import logging
from logging.handlers import MemoryHandler, RotatingFileHandler
log_file = 'buffered.log'
file_handler = RotatingFileHandler(log_file, maxBytes=1024, backupCount=1)
file_handler.setLevel(logging.ERROR)
memory_handler = MemoryHandler(capacity=10, target=file_handler, flushLevel=logging.ERROR)
logger = logging.getLogger('memory_logger')
logger.setLevel(logging.DEBUG)
logger.addHandler(memory_handler)
# Эти сообщения накапливаются в памяти
for i in range(5):
logger.info(f'Сообщение {i}')
# При ошибке все накопленные записи сбрасываются в файл
logger.error('Ошибка, вызывающая сброс')
Содержимое buffered.log (пример): INFO:memory_logger:Сообщение 0 INFO:memory_logger:Сообщение 1 INFO:memory_logger:Сообщение 2 INFO:memory_logger:Сообщение 3 INFO:memory_logger:Сообщение 4 ERROR:memory_logger:Ошибка, вызывающая сброс
Пояснение: MemoryHandler буферизирует сообщения до тех пор, пока не будет достигнут лимит capacity или не поступит запись уровня flushLevel (по умолчанию ERROR). Затем все записи переносятся в целевой обработчик.
Пример 3: Фильтры для выборочного логирования
import logging
class ImportantFilter(logging.Filter):
def filter(self, record):
return '[ВАЖНО]' in record.getMessage()
logger = logging.getLogger('filter_logger')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(message)s'))
handler.addFilter(ImportantFilter())
logger.addHandler(handler)
logger.info('Обычное сообщение') # не попадёт в вывод
logger.info('[ВАЖНО] Срочное уведомление') # попадёт
Результат:
[ВАЖНО] Срочное уведомление
Фильтры можно добавлять как к логгерам (через addFilter), так и к обработчикам.
Пример 4: Настройка логирования для многопоточных приложений
import logging
import threading
import time
logging.basicConfig(
level=logging.DEBUG,
format='[%(threadName)s] %(message)s'
)
logger = logging.getLogger('thread_logger')
def worker():
for i in range(3):
logger.debug(f'Шаг {i}')
time.sleep(0.1)
threads = []
for _ in range(2):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
for t in threads:
t.join()
Результат (порядок может отличаться):
[Thread-1] Шаг 0 [Thread-2] Шаг 0 [Thread-1] Шаг 1 [Thread-2] Шаг 1 [Thread-1] Шаг 2 [Thread-2] Шаг 2
Библиотека logging потокобезопасна, но при использовании одного файлового обработчика может возникать конкуренция. Для избежания потери записей в многопоточных сценариях используют QueueHandler и отдельный поток с QueueListener (доступно с Python 3.2+).
Пример 5: Использование QueueListener и QueueHandler для асинхронного логирования
import logging
from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
from queue import Queue
# Создаём очередь и хендлер
transfer_queue = Queue()
# Обработчик, который будет фактически выполнять запись
file_handler = RotatingFileHandler('async.log', maxBytes=1024, backupCount=1)
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
# Слушатель очереди
listener = QueueListener(transfer_queue, file_handler)
listener.start()
# Логгер использует QueueHandler
logger = logging.getLogger('async_logger')
logger.setLevel(logging.DEBUG)
queue_handler = QueueHandler(transfer_queue)
logger.addHandler(queue_handler)
# Теперь вызовы logger.debug() не блокируются на запись в файл
for i in range(10):
logger.debug(f'Асинхронное сообщение {i}')
# В конце программы остановить listener
listener.stop()
Пояснение: QueueListener работает в отдельном потоке, вычитывает записи из очереди и передаёт их целевому обработчику. Это повышает производительность при интенсивной записи.
Пример 6: Логирование с помощью dictConfig из JSON-файла
import json
import logging.config
config_str = '''
{
"version": 1,
"formatters": {
"json_formatter": {
"format": "{\"time\": \"%(asctime)s\", \"level\": \"%(levelname)s\", \"message\": \"%(message)s\"}"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "json_formatter"
}
},
"loggers": {
"json_logger": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": false
}
}
}
'''
config = json.loads(config_str)
logging.config.dictConfig(config)
logger = logging.getLogger('json_logger')
logger.info('Это сообщение в формате JSON')
{"time": "2025-03-15 14:22:01,234", "level": "INFO", "message": "Это сообщение в формате JSON"}
Такой подход удобен при интеграции с системами сбора логов (ELK, Splunk и т.д.).