Logger, Handler, Formatter, Filter — основные компоненты
Понимание основных компонентов logging — Logger, Handler, Formatter, Filter — ключ к эффективному использованию в production. Каждый компонент отвечает за свою часть обработки логов.
Проблема: Нужно создать logger в модуле для логирования.
Решение: getLogger(__name__) создаёт logger с именем модуля.
import logging
# В файле app/api/users.py
logger = logging.getLogger(__name__)
# Имя logger: 'app.api.users'
# Использование
logger.debug("Debug message")
logger.info("Info message")
logger.warning("Warning message")
logger.error("Error message")
logger.critical("Critical message")Проблема: Нужно понять, как logger'ы связаны друг с другом.
Решение: Logger'ы организованы в дерево по именам.

# Создание logger'ов
logging.getLogger('app') # Родительский
logging.getLogger('app.api') # Дочерний app
logging.getLogger('app.api.users') # Дочерний app.api
# Иерархия определяется по '.'
# app.api.users → app.api → app → rootПравила иерархии:
root — корневой logger (получается через getLogger() без аргументов)app.api.users — дочерний app.api, который дочерний apppropagate=True)import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Все сообщения от DEBUG и выше будут обработаны
# Но могут быть отфильтрованы handler'амиВажно: Уровень logger — это фильтр "на входе". Если сообщение не проходит этот фильтр, оно не попадает ни в один handler.
Проблема: Какой уровень будет использоваться, если у logger'а не установлен свой уровень?
Решение: getEffectiveLevel() возвращает уровень ближайшего родителя с установленным уровнем.
import logging
# Root logger имеет уровень WARNING по умолчанию
logging.getLogger().setLevel(logging.WARNING)
# app имеет свой уровень
logging.getLogger('app').setLevel(logging.INFO)
# app.api не имеет установленного уровня
api_logger = logging.getLogger('app.api')
print(api_logger.level) # 0 (NOTSET)
print(api_logger.getEffectiveLevel()) # 20 (INFO) — от 'app'
# app.api.users наследует от app.api → app
users_logger = logging.getLogger('app.api.users')
print(users_logger.getEffectiveLevel()) # 20 (INFO) — от 'app'Проблема: Форматирование сообщения дорогое, не хочется выполнять если уровень отключён.
Решение: isEnabledFor() проверяет перед форматированием.
# ❌ Плохо: строка формируется всегда
logger.debug(f"Processing {expensive_operation()}")
# ✅ Хорошо: проверка перед форматированием
if logger.isEnabledFor(logging.DEBUG):
logger.debug(f"Processing {expensive_operation()}")
# ✅ Хорошо: % форматирование (ленивое)
logger.debug("Processing %s", expensive_operation())
# expensive_operation() вызывается только если DEBUG включёнPerformance impact:
# Без isEnabledFor: 10000 вызовов = 0.5s (строки формируются)
# С isEnabledFor: 10000 вызовов = 0.001s (строки не формируются)Проблема: Нужно отправить логи в файл, консоль, email.
Решение: Handler определяет точку назначения.
Проблема: Нужно выводить логи в консоль.
Решение: StreamHandler для stdout/stderr.
import logging
import sys
# Консоль (stdout)
console_handler = logging.StreamHandler(sys.stdout)
# Консоль (stderr) — для ошибок
error_handler = logging.StreamHandler(sys.stderr)
error_handler.setLevel(logging.ERROR)
# Добавление к logger
logger.addHandler(console_handler)
logger.addHandler(error_handler)Проблема: Нужно сохранять логи в файл.
Решение: FileHandler пишет в файл.
import logging
# Простой файл
file_handler = logging.FileHandler('app.log')
# С кодировкой
file_handler = logging.FileHandler('app.log', encoding='utf-8')
# В абсолютный путь
file_handler = logging.FileHandler('/var/log/myapp/app.log')
# Добавление к logger
logger.addHandler(file_handler)Проблема: Файл лога растёт бесконечно.
Решение: RotatingFileHandler ротирует файл при достижении размера.
from logging.handlers import RotatingFileHandler
# Ротация при 10MB, хранение 5 backup файлов
handler = RotatingFileHandler(
'app.log',
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=5
)
# Файлы:
# app.log ← текущий
# app.log.1 ← последний backup
# app.log.2
# ...
# app.log.5 ← самый старыйПроблема: Нужно ротировать логи по расписанию (каждый день).
Решение: TimedRotatingFileHandler ротирует по времени.
from logging.handlers import TimedRotatingFileHandler
# Ротация каждый день в полночь, хранение 30 дней
handler = TimedRotatingFileHandler(
'app.log',
when='midnight',
interval=1,
backupCount=30
)
# when варианты:
# 'S' — секунды
# 'M' — минуты
# 'H' — часы
# 'D' — дни
# 'W0'-'W6' — дни недели (0=понедельник)
# 'midnight' — полночьПроблема: Нужно отправлять критические ошибки по email.
Решение: SMTPHandler отправляет логи по email.
from logging.handlers import SMTPHandler
# Email при ERROR и выше
mail_handler = SMTPHandler(
mailhost=('smtp.example.com', 587),
fromaddr='app@example.com',
toaddrs=['admin@example.com', 'oncall@example.com'],
subject='Application Error',
credentials=('username', 'password'),
secure=() # TLS
)
mail_handler.setLevel(logging.ERROR)Проблема: Нужно выводить логи одновременно в консоль, файл и email.
Решение: Добавить несколько handler'ов к logger.
import logging
from logging.handlers import RotatingFileHandler, SMTPHandler
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Консоль — INFO и выше
console = logging.StreamHandler()
console.setLevel(logging.INFO)
# Файл — DEBUG и выше
file_handler = RotatingFileHandler('app.log', maxBytes=10*1024*1024, backupCount=5)
file_handler.setLevel(logging.DEBUG)
# Email — ERROR и выше
mail_handler = SMTPHandler(
mailhost=('smtp.example.com', 587),
fromaddr='app@example.com',
toaddrs=['admin@example.com'],
subject='Application Error'
)
mail_handler.setLevel(logging.ERROR)
# Добавление handler'ов
logger.addHandler(console)
logger.addHandler(file_handler)
logger.addHandler(mail_handler)Flow:
logger.error("Error") → проходит logger (DEBUG+)
→ проходит console (INFO+) → вывод в консоль
→ проходит file_handler (DEBUG+) → запись в файл
→ проходит mail_handler (ERROR+) → отправка email
Проблема: При многократном импорте модуля handlers добавляются повторно.
Решение: Проверять перед добавлением.
import logging
logger = logging.getLogger(__name__)
# ❌ Плохо: handler добавляется при каждом импорте
logger.addHandler(logging.StreamHandler())
# ✅ Хорошо: проверка перед добавлением
if not logger.handlers:
logger.addHandler(logging.StreamHandler())
# ✅ Или: проверка типа
if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
logger.addHandler(logging.StreamHandler())Проблема: Нужно форматировать сообщения с timestamp, уровнем, именем logger.
Решение: Formatter определяет формат вывода.
import logging
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)Пример вывода:
2026-03-21 10:00:00,123 - app.api.users - INFO - User logged in
| Переменная | Значение | Пример |
|---|---|---|
%(asctime)s | Timestamp | 2026-03-21 10:00:00,123 |
%(name)s | Имя logger | app.api.users |
%(levelname)s | Уровень | INFO, ERROR |
%(message)s | Сообщение | User logged in |
%(filename)s | Имя файла | users.py |
%(lineno)d | Номер строки | 42 |
%(funcName)s | Функция | create_user |
%(process)d | PID процесса | 12345 |
%(thread)d | ID потока | 98765 |
%(module)s | Модуль | users |
# ISO 8601 формат
formatter = logging.Formatter(
'%(asctime)s - %(message)s',
datefmt='%Y-%m-%dT%H:%M:%S%z'
)
# Вывод: 2026-03-21T10:00:00+0000 - User logged in
# Простой формат
formatter = logging.Formatter(
'%(asctime)s - %(message)s',
datefmt='%H:%M:%S'
)
# Вывод: 10:00:00 - User logged in# Formatter с контекстными полями
formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s - user_id=%(user_id)s request_id=%(request_id)s'
)
# Использование
logger.info(
"User action completed",
extra={
"user_id": 123,
"request_id": "abc-xyz"
}
)
# Вывод: 2026-03-21 10:00:00,123 - INFO - User action completed - user_id=123 request_id=abc-xyzВажно: Если extra поле не передано, а formatter его ожидает — KeyError.
Проблема: В консоли нужен краткий формат, в файле — подробный.
Решение: Разные formatter для разных handler'ов.
# Консоль — кратко
console_formatter = logging.Formatter(
'%(levelname)s: %(message)s'
)
console_handler.setFormatter(console_formatter)
# Файл — подробно
file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(filename)s:%(lineno)d'
)
file_handler.setFormatter(file_formatter)
# Email — с traceback
email_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s\n%(message)s\n\n%(exc_text)s'
)
email_handler.setFormatter(email_formatter)Проблема: Нужно фильтровать сообщения по кастомным условиям.
Решение: Filter решает, пропустить ли LogRecord.
# Handler фильтрует по уровню
handler.setLevel(logging.WARNING)
# Пропускает только WARNING, ERROR, CRITICALimport logging
class SensitiveDataFilter(logging.Filter):
def filter(self, record):
# Пропускаем логи с чувствительными данными
message = record.getMessage()
if 'password' in message.lower():
return False
if 'secret' in message.lower():
return False
return True
logger.addFilter(SensitiveDataFilter())# Пропускать только логи от определённых модулей
class ModuleFilter(logging.Filter):
def __init__(self, allowed_modules):
super().__init__()
self.allowed_modules = allowed_modules
def filter(self, record):
return any(
record.name.startswith(mod)
for mod in self.allowed_modules
)
logger.addFilter(ModuleFilter(['app.api', 'app.db']))Проблема: Нужно фильтровать сообщения для конкретного handler.
Решение: Добавить filter к handler.
# Только ERROR для email
mail_handler.addFilter(lambda record: record.levelno >= logging.ERROR)
# Исключить health check логи из файла
class HealthCheckFilter(logging.Filter):
def filter(self, record):
return 'health check' not in record.getMessage().lower()
file_handler.addFilter(HealthCheckFilter())import logging
from contextvars import ContextVar
# ContextVar для хранения request_id
request_id_var: ContextVar[str] = ContextVar('request_id', default='')
class RequestContextFilter(logging.Filter):
"""Добавляет request_id из ContextVar ко всем логам."""
def filter(self, record):
record.request_id = request_id_var.get()
return True
# Применение
logger = logging.getLogger('app')
logger.addFilter(RequestContextFilter())
# В middleware
request_id_var.set('abc-123')
logger.info("Processing request") # Автоматически включает request_idПроблема: Нужно добавлять контекст (user_id, request_id) ко всем сообщениям без явного extra={}.
Решение: LoggerAdapter автоматически добавляет контекст.
import logging
logger = logging.getLogger(__name__)
# Создание адаптера с контекстом
adapter = logging.LoggerAdapter(
logger,
extra={
"user_id": 123,
"request_id": "abc-xyz"
}
)
# Использование как обычный logger
adapter.info("User logged in")
# Вывод: ... - User logged in - user_id=123 request_id=abc-xyz
adapter.debug("Processing data")
# Контекст добавляется автоматическиПроблема: Нужно добавить request_id ко всем логам запроса.
Решение: Создать адаптер при начале запроса.
# middleware.py
import logging
import uuid
class RequestLoggingMiddleware:
def __init__(self, app):
self.app = app
def __call__(self, request):
# Генерация request_id
request_id = str(uuid.uuid4())
request.request_id = request_id
# Создание logger с контекстом
logger = logging.getLogger('app.api')
adapter = logging.LoggerAdapter(
logger,
extra={
"request_id": request_id,
"method": request.method,
"path": request.path
}
)
adapter.info("Request started")
try:
response = self.app(request)
adapter.info("Request completed", extra={"status": response.status})
return response
except Exception as e:
adapter.exception("Request failed")
raiseПроблема: Нужно динамически вычислять контекст.
Решение: Переопределить process().
import logging
import threading
class DynamicContextAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
# Добавляем динамический контекст
kwargs.setdefault('extra', {})
kwargs['extra'].update(self.extra)
kwargs['extra']['thread_name'] = threading.current_thread().name
return msg, kwargs
# Использование
adapter = DynamicContextAdapter(
logger,
extra={"user_id": 123}
)Проблема: Нужно настроить logger для production приложения.
Решение: Комбинация всех компонентов.
import logging
from logging.handlers import RotatingFileHandler, SMTPHandler
import sys
def setup_logger(name: str) -> logging.Logger:
"""Настройка logger для production."""
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# Избегаем дублирования handler'ов
if logger.handlers:
return logger
# Formatter'ы
console_formatter = logging.Formatter(
'%(levelname)s: %(message)s'
)
file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(filename)s:%(lineno)d'
)
email_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s\n%(message)s\n\n%(exc_text)s'
)
# Console handler (INFO+)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(console_formatter)
# File handler (DEBUG+, ротация)
file_handler = RotatingFileHandler(
'app.log',
maxBytes=10 * 1024 * 1024,
backupCount=5
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(file_formatter)
# Email handler (ERROR+, критические ошибки)
email_handler = SMTPHandler(
mailhost=('smtp.example.com', 587),
fromaddr='app@example.com',
toaddrs=['admin@example.com'],
subject='Application Error'
)
email_handler.setLevel(logging.ERROR)
email_handler.setFormatter(email_formatter)
# Фильтр для исключения health check из email
class HealthCheckFilter(logging.Filter):
def filter(self, record):
return 'health check' not in record.getMessage().lower()
email_handler.addFilter(HealthCheckFilter())
# Добавление handler'ов
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.addHandler(email_handler)
return logger
# Использование
logger = setup_logger(__name__)
logger.info("Application started")# ❌ Плохо: handler добавляется при каждом импорте
logger = logging.getLogger(__name__)
logger.addHandler(logging.StreamHandler())
# ✅ Хорошо: проверка перед добавлением
logger = logging.getLogger(__name__)
if not logger.handlers:
logger.addHandler(logging.StreamHandler())
# ✅ Или: use getHandler
if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
logger.addHandler(logging.StreamHandler())# ❌ Плохо
logger.info(f"User login: {username}, password: {password}")
# ✅ Хорошо
logger.info(f"User login attempt", extra={"username": username})
# password не логируется# ❌ Плохо
try:
risky_operation()
except Exception as e:
logger.error(f"Error: {e}")
# ✅ Хорошо
try:
risky_operation()
except Exception:
logger.error("Operation failed", exc_info=True)
# или
logger.exception("Operation failed") # Только внутри except# ✅ Хорошо
logger = logging.getLogger(__name__)
# ❌ Плохо
logger = logging.getLogger('my_logger') # Одинаковое имя везде# ✅ Хорошо: предотвращает дублирование
logger = logging.getLogger('app')
logger.addHandler(handler)
logger.propagate = False
# ❌ Плохо: логи дублируются в root logger
logger = logging.getLogger('app')
logger.addHandler(handler)
logger.propagate = True # По умолчанию| Компонент | Ответственность | Пример |
|---|---|---|
| Logger | Точка входа, фильтрация по уровню | getLogger(__name__) |
| Handler | Куда отправлять логи | StreamHandler, FileHandler |
| Formatter | Формат вывода | %(asctime)s - %(message)s |
| Filter | Кастомная фильтрация | SensitiveDataFilter |
| LoggerAdapter | Автоматический контекст | LoggerAdapter(logger, extra={}) |
Сообщение → Logger.setLevel() → Handler.setLevel() → Filter → Вывод
app.api.users → app.api → app → root
getLogger(__name__) для создания logger'овif not logger.handlers перед добавлениемextra={}exc_info=True для ошибокpropagate=False для application logger'овВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.