Easy📖Теория4 min

logging

Уровни логирования, обработчики, форматирование и настройка через getLogger()

logging — система логирования Python

Зачем нужен logging

print() подходит для отладки, но в production-коде нужна полноценная система логирования. Модуль logging предоставляет уровни важности, множество обработчиков, форматирование и иерархию логгеров.

import logging

# Basic configuration — quick start
logging.basicConfig(level=logging.INFO)

logging.debug("Отладочная информация")      # Not shown (below INFO)
logging.info("Приложение запущено")          # Shown
logging.warning("Диск почти полон")          # Shown
logging.error("Не удалось подключиться")     # Shown
logging.critical("Фатальная ошибка!")        # Shown

Уровни логирования

Уровень Значение Когда использовать
DEBUG 10 Детальная отладочная информация
INFO 20 Подтверждение нормальной работы
WARNING 30 Что-то неожиданное, но работа продолжается
ERROR 40 Ошибка, часть функциональности недоступна
CRITICAL 50 Критическая ошибка, программа не может продолжить

getLogger() — именованные логгеры

import logging

# Create a named logger (convention: use __name__)
logger = logging.getLogger(__name__)

# Logger hierarchy: 'myapp.auth' is child of 'myapp'
auth_logger = logging.getLogger("myapp.auth")
db_logger = logging.getLogger("myapp.database")

def process_order(order_id: int) -> None:
    logger.info("Обработка заказа #%d", order_id)
    try:
        logger.debug("Шаг 1: валидация заказа #%d", order_id)
        logger.info("Заказ #%d успешно обработан", order_id)
    except Exception:
        logger.exception("Ошибка обработки заказа #%d", order_id)

# IMPORTANT: use lazy formatting (% style), NOT f-strings
# GOOD: logger.info("User %s logged in", username)
# BAD:  logger.info(f"User {username} logged in")
# Why: lazy formatting only formats if the message is actually logged

Handlers — обработчики

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

# Console output (INFO and above)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# File output (DEBUG and above)
file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)

# Rotating file (max 5MB, keep 3 backups)
rotating_handler = RotatingFileHandler(
    "app_rotating.log",
    maxBytes=5 * 1024 * 1024,
    backupCount=3,
    encoding="utf-8",
)

# Add handlers to logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)

# Now messages go to multiple destinations
logger.debug("Отладка — только в файл")
logger.info("Информация — консоль + файл")
logger.warning("Предупреждение — везде")

Formatters — форматирование

import logging

formatter = logging.Formatter(
    fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

# Apply formatter to handler
console = logging.StreamHandler()
console.setFormatter(formatter)

logger = logging.getLogger("myapp")
logger.addHandler(console)
logger.setLevel(logging.DEBUG)

logger.info("Приложение запущено")
# Output: 2026-03-30 14:30:00 | INFO     | myapp | Приложение запущено

# Available format attributes:
# %(asctime)s   — Time          %(levelname)s — Level
# %(name)s      — Logger name   %(message)s   — Message
# %(filename)s  — Source file   %(lineno)d    — Line number
# %(funcName)s  — Function      %(process)d   — Process ID

Собственный JSON-форматтер

import logging
import json
from datetime import datetime, timezone

class JSONFormatter(logging.Formatter):
    """Format log records as JSON."""

    def format(self, record: logging.LogRecord) -> str:
        log_data = {
            "timestamp": datetime.fromtimestamp(
                record.created, tz=timezone.utc,
            ).isoformat(),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "module": record.module,
            "line": record.lineno,
        }
        if record.exc_info:
            log_data["exception"] = self.formatException(record.exc_info)
        return json.dumps(log_data, ensure_ascii=False)

# Usage
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger = logging.getLogger("myapp")
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)

logger.info("Пользователь вошёл")
# {"timestamp": "2026-03-30T11:30:00+00:00", "level": "INFO", ...}

Конфигурация через dictConfig

import logging
import logging.config

LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "standard": {
            "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
            "datefmt": "%Y-%m-%d %H:%M:%S",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "INFO",
            "formatter": "standard",
            "stream": "ext://sys.stdout",
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "level": "DEBUG",
            "formatter": "standard",
            "filename": "app.log",
            "maxBytes": 10485760,
            "backupCount": 5,
            "encoding": "utf-8",
        },
    },
    "loggers": {
        "myapp": {
            "level": "DEBUG",
            "handlers": ["console", "file"],
            "propagate": False,
        },
    },
    "root": {
        "level": "WARNING",
        "handlers": ["console"],
    },
}

logging.config.dictConfig(LOGGING_CONFIG)

app_logger = logging.getLogger("myapp")
app_logger.info("Приложение запущено")

Контекстная информация

import logging

# LoggerAdapter — reusable extra context
class RequestLogger(logging.LoggerAdapter):
    """Logger that adds request context."""

    def process(self, msg, kwargs):
        request_id = self.extra.get("request_id", "N/A")
        return f"[req:{request_id}] {msg}", kwargs

base_logger = logging.getLogger("myapp.api")
request_logger = RequestLogger(base_logger, {"request_id": "abc-123"})
request_logger.info("Обработка запроса")
# [req:abc-123] Обработка запроса

Практический пример: настройка для приложения

import logging
import logging.config
import os

def setup_logging() -> None:
    """Configure application logging from environment."""
    level = os.getenv("LOG_LEVEL", "INFO")
    log_dir = os.getenv("LOG_DIR", "logs")
    json_format = os.getenv("LOG_JSON", "").lower() == "true"

    os.makedirs(log_dir, exist_ok=True)

    fmt = (
        '{"time":"%(asctime)s","level":"%(levelname)s","msg":"%(message)s"}'
        if json_format
        else "%(asctime)s [%(levelname)-8s] %(name)s: %(message)s"
    )

    logging.config.dictConfig({
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {"default": {"format": fmt, "datefmt": "%Y-%m-%d %H:%M:%S"}},
        "handlers": {
            "console": {"class": "logging.StreamHandler", "formatter": "default", "level": level},
            "file": {
                "class": "logging.handlers.RotatingFileHandler",
                "formatter": "default",
                "filename": f"{log_dir}/app.log",
                "maxBytes": 10_485_760,
                "backupCount": 5,
                "encoding": "utf-8",
            },
        },
        "root": {"level": level, "handlers": ["console", "file"]},
    })

# Initialize at startup
setup_logging()
logger = logging.getLogger(__name__)
logger.info("Модуль инициализирован")

Проверь себя

🧪

Какие сообщения будут выведены при `logging.basicConfig(level=logging.WARNING)`?

🧪

Почему рекомендуется использовать `logger.info('User %s', name)` вместо `logger.info(f'User {name}')`?

🧪

Как отправить лог-сообщения одновременно в консоль и в файл?

🧪

Что делает `logger.exception()` (или `logging.exception()`)?