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 и т.д.).

Логирование в Python - comments

En
Python logging (python)