Логирование и отладка
print — не логирование
print подходит для учебных скриптов. В реальном сервисе нужны:
- уровни важности — в продакшене не нужны отладочные сообщения, а ошибки нужны всегда;
- время, модуль, строка в каждой записи;
- вывод в разные места — консоль, файл, системы сбора логов (ELK, Loki, Sentry);
- настройка без правки кода.
Всё это даёт стандартный модуль logging.
Быстрый старт
import logging, sys
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
datefmt="%H:%M:%S",
stream=sys.stdout,
force=True,
)
log = logging.getLogger("orders")
log.debug("Детали для отладки: корзина=%s", ["чай", "кофе"])
log.info("Заказ %d создан", 42)
log.warning("Склад почти пуст: осталось %d шт.", 3)
log.error("Не удалось списать оплату")
log.critical("База данных недоступна")
Уровни
| Уровень | Число | Когда |
|---|---|---|
DEBUG |
10 | детали для разработчика |
INFO |
20 | штатные события: запуск, обработан запрос |
WARNING |
30 | что-то странное, но работаем (уровень по умолчанию) |
ERROR |
40 | операция не удалась |
CRITICAL |
50 | приложение не может продолжать работу |
Сообщения ниже установленного уровня отбрасываются.
Логгеры, обработчики, форматтеры
- Logger — точка входа, имеет имя. Имена образуют иерархию по точкам:
app.db— потомокapp. - Handler — куда писать. У одного логгера может быть несколько обработчиков со своими уровнями.
- Formatter — как выглядит строка.
- Запись всплывает к родительским логгерам (
propagate) — поэтому обычно обработчики настраивают только у корневого логгера.
import logging, sys
app = logging.getLogger("app")
app.setLevel(logging.DEBUG)
app.propagate = False # чтобы не дублировать в root в этом примере
console = logging.StreamHandler(sys.stdout)
console.setLevel(logging.INFO) # в консоль — от INFO
console.setFormatter(logging.Formatter("[%(levelname)s] %(name)s: %(message)s"))
app.addHandler(console)
errors_only = logging.StreamHandler(sys.stdout)
errors_only.setLevel(logging.ERROR) # отдельный обработчик для ошибок
errors_only.setFormatter(logging.Formatter(" !!! АЛЕРТ: %(message)s (%(funcName)s:%(lineno)d)"))
app.addHandler(errors_only)
db = logging.getLogger("app.db") # дочерний логгер
db.debug("не попадёт: консоль начинает с INFO")
db.info("подключение установлено")
db.error("таймаут запроса")
Правило для модулей
В каждом модуле: logger = logging.getLogger(__name__). Имя совпадёт с путём модуля (shop.payments), и логами можно управлять по пакетам. Настраивать обработчики (basicConfig, dictConfig) — только в точке входа приложения, не в библиотеке.
Логирование исключений
import logging, sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(levelname)s %(message)s", force=True)
log = logging.getLogger("payments")
def charge(amount):
return 100 / amount
try:
charge(0)
except ZeroDivisionError:
log.exception("Ошибка оплаты, amount=%s", 0) # ERROR + полный traceback
log.exception(...) = log.error(..., exc_info=True). Вызывайте его внутри except.
Конфигурация словарём
В реальных проектах (Django LOGGING, FastAPI/uvicorn) конфигурацию описывают словарём:
import logging.config
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {"format": "%(asctime)s %(levelname)s %(name)s: %(message)s"},
},
"handlers": {
"console": {"class": "logging.StreamHandler", "formatter": "default"},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "app.log", "maxBytes": 10_000_000, "backupCount": 5,
"formatter": "default",
},
},
"loggers": {
"shop": {"level": "DEBUG"},
"urllib3": {"level": "WARNING"}, # приглушить болтливую библиотеку
},
"root": {"level": "INFO", "handlers": ["console", "file"]},
}
logging.config.dictConfig(LOGGING)
Структурные логи
Системы сбора логов лучше понимают JSON. Дополнительные поля передают через extra:
import json, logging, sys
class JsonFormatter(logging.Formatter):
def format(self, record):
data = {"level": record.levelname, "logger": record.name, "msg": record.getMessage()}
data.update(getattr(record, "ctx", {}))
return json.dumps(data, ensure_ascii=False)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
log = logging.getLogger("api")
log.handlers[:] = [handler]
log.propagate = False
log.setLevel(logging.INFO)
log.info("Запрос обработан", extra={"ctx": {"path": "/orders", "status": 200, "ms": 37}})
В продакшене для этого часто берут библиотеку structlog или python-json-logger.
Отладка
1. Прочитайте traceback
Последняя строка — тип и сообщение. Выше — путь вызовов. Python 3.11+ ещё и подчёркивает ^^^^ точное выражение, которое упало.
2. pdb и breakpoint()
Вставьте breakpoint() в код — выполнение остановится, и откроется интерактивный отладчик:
def total(prices, discount):
subtotal = sum(prices)
breakpoint() # остановка здесь
return subtotal * (1 - discount)
| Команда | Действие |
|---|---|
p expr / pp expr |
напечатать значение (красиво) |
n (next) |
следующая строка, не заходя в функции |
s (step) |
зайти внутрь вызова |
c (continue) |
продолжить до следующей точки останова |
l / ll |
показать код вокруг / всю функцию |
w (where) |
стек вызовов |
u / d |
подняться / спуститься по стеку |
b 42 |
точка останова на строке 42 |
q |
выйти |
python -m pdb script.py— запустить скрипт под отладчиком.PYTHONBREAKPOINT=0— отключить всеbreakpoint().pytest --pdb— открыть отладчик в месте упавшего теста.- В VS Code и PyCharm то же самое делается кликом по полю слева от строки.
Визуализатор в этом курсе
Кнопка 👣 Пошагово у примеров — тот же отладчик, только наглядный: шаги, переменные и объекты в памяти.
3. Полезные модули
import traceback, warnings
def risky():
return {}["нет"]
try:
risky()
except KeyError:
text = traceback.format_exc() # traceback как строка — например, отправить в лог
print(text.strip().splitlines()[-1])
warnings.simplefilter("always")
def old_api():
warnings.warn("old_api устарел, используйте new_api", DeprecationWarning, stacklevel=2)
return 1
with warnings.catch_warnings(record=True) as caught:
old_api()
print(caught[0].category.__name__, caught[0].message)
faulthandler.enable()— печатает стек даже при падении интерпретатора (segfault) или по таймауту зависания.python -X dev— режим разработки: больше предупреждений и проверок.
Вопрос на собеседовании
«Как вы отлаживаете проблему в продакшене?» — Сначала логи и метрики (что, когда, с какими данными; трассировка в Sentry), воспроизведение локально или тестом, затем отладчик или дополнительное логирование. Важно: структурные логи с идентификатором запроса, правильные уровни и logger.exception в обработчиках ошибок.
Итог
logging.getLogger(__name__)в каждом модуле, настройка — один раз в точке входа.- Уровни: DEBUG < INFO < WARNING < ERROR < CRITICAL.
- Logger → Handler → Formatter; записи всплывают к родителям.
- Ленивое форматирование
%s,logger.exceptionвexcept, JSON-логи для продакшена. - Отладка: traceback снизу вверх,
breakpoint()и команды pdb,pytest --pdb.
Практика решено 0 из 5
Создайте логгер log = logging.getLogger("shop") и функцию buy(item, qty):
- при qty <= 0 пишет в лог WARNING с сообщением "Некорректное количество: <qty>" и возвращает False;
- иначе пишет INFO "Покупка: <item> x<qty>" и возвращает True.
Используйте ленивое форматирование log.info("...%s", value), а не f-строки.
Какие тесты проверяют код (1)
import logging
records = []
class H(logging.Handler):
def emit(self, record):
records.append((record.levelname, record.getMessage()))
log.addHandler(H()); log.setLevel(logging.DEBUG)
assert buy("чай", 2) is True and buy("кофе", 0) is False
assert records == [("INFO", "Покупка: чай x2"), ("WARNING", "Некорректное количество: 0")], records
import logging
log = logging.getLogger("shop")
def buy(item, qty):
if qty <= 0:
log.warning("Некорректное количество: %s", qty)
return False
log.info("Покупка: %s x%s", item, qty)
return True
Какой уровень логирования подходит?
| DEBUG | INFO | WARNING | ERROR | CRITICAL | |
|---|---|---|---|---|---|
| Значения переменных внутри алгоритма при поиске бага | |||||
| Сервис запущен на порту 8000 | |||||
| Диск заполнен на 90%, но работа продолжается | |||||
| Не удалось отправить письмо клиенту (исключение) | |||||
| База данных недоступна, приложение не может работать |
Почему пишут log.debug("Данные: %s", data), а не log.debug(f"Данные: {data}")?
Что делает log.exception("Ошибка оплаты") внутри блока except?
Что делает встроенная функция breakpoint()?
Войдите, чтобы отмечать пройденные уроки.