Логирование и обработка ошибок
Учим «Цыплёнка-помощника» вести дневник своих действий и не падать замертво, когда что-то идёт не так, а спокойно извиняться перед пользователем.
Логирование — это привычка бота записывать в специальный журнал, что он делал и где споткнулся: кто, когда, какую команду нажал и какая ошибка вылетела. Без такого журнала ты как врач без анализов — лечишь вслепую.
Привет! В прошлых уроках наш «Цыплёнок-помощник» уже умеет отвечать на команды, показывать кнопки, вести диалог через анкету и даже ходить за погодой в большой интернет. И вот ты впервые выкатил его «в люди»: скинул ссылку друзьям в чат игрового клана, гордо смотришь, как он отвечает… а через десять минут пишет одноклассник: «слушай, твой бот завис, ничего не пишет». Ты открываешь терминал, а там бот молча упал. И главный вопрос — почему? — повисает в воздухе. Этому уроку и посвящено: научить бота рассказывать, что с ним происходит, и не умирать от первой же ошибки.
Зачем боту дневник
Представь, что ты ведёшь дневник тренировок. Сегодня пробежал 3 км, завтра 5, послезавтра колено заболело. Когда колено заболит сильно, ты открываешь дневник и видишь: «ага, я слишком резко увеличил нагрузку вот в этот день». Без записей ты бы просто гадал.
С ботом то же самое. Пока он крутится на твоём компьютере и ты сидишь рядом — ты видишь всё прямо в терминале. Но настоящий бот работает без тебя: на сервере, ночью, пока ты спишь или сидишь на уроке. Когда утром кто-то напишет «бот сломался», единственный способ понять, что случилось — это заглянуть в его журнал (лог). Если журнала нет, ты узнаешь только факт «сломался», но не причину.
Многие новички решают эту задачу через print(): натыкают по коду print("сюда дошли") и смотрят в консоль. Это работает, пока ты рядом. Но print() не умеет ставить время, не различает «обычное событие» и «жуткую ошибку», и его невозможно аккуратно выключить или записать в файл. Поэтому в Python есть специальный инструмент — модуль logging.
logging — это умный print со штампиком
Главное отличие logging от print() — у каждого сообщения есть уровень важности, как у новостей: бывает «просто к сведению», а бывает «срочно, всё горит». Уровней пять, от самого спокойного к самому тревожному:
DEBUG | Мелкие подробности для отладки: «зашёл в функцию», «переменная равна 5». Обычно скрыты. |
INFO | Нормальные события: «бот запустился», «пользователь нажал /start». |
WARNING | Что-то странное, но бот выжил: «API ответил медленно», «пустое сообщение». |
ERROR | Произошла ошибка, действие не выполнилось: «не смог отправить сообщение». |
CRITICAL | Совсем плохо, бот может упасть целиком. |
Ты говоришь логгеру: «показывай мне всё начиная с уровня INFO» — и тогда DEBUG-болтовня прячется, а важное остаётся. Поменял настройку на DEBUG — и видишь вообще каждый шаг. Удобно: один переключатель регулирует «громкость» бота.
Настраиваем logging в боте
Давай сначала разберёмся с самим модулем на маленьком чистом примере, который можно запустить прямо в браузере — без всякого Telegram. Тут нет ни bot, ни dp, только стандартная библиотека Python.
import logging
# basicConfig настраивает журнал один раз на всю программу
logging.basicConfig(
level=logging.INFO, # показывать INFO и всё, что тревожнее
format="%(asctime)s %(levelname)s %(message)s",
)
logging.debug("Это мелкая деталь — её не покажут, уровень ниже INFO")
logging.info("Бот запустился")
logging.warning("Пользователь прислал пустое сообщение")
logging.error("Не получилось сохранить данные")Вывод:
2026-06-19 10:00:00,000 INFO Бот запустился 2026-06-19 10:00:00,001 WARNING Пользователь прислал пустое сообщение 2026-06-19 10:00:00,002 ERROR Не получилось сохранить данные
Видишь? Строчку с debug журнал проглотил, потому что мы попросили показывать от INFO и выше. А у каждой оставшейся строки спереди штампик: дата, время, уровень. Это и есть то, чего print() дать не может.
Разберём basicConfig по косточкам:
level=logging.INFO— порог «громкости». Всё, что тишеINFO(то естьDEBUG), не печатается.format="..."— шаблон строки.%(asctime)sподставит время,%(levelname)s— уровень,%(message)s— сам текст. Эти три кусочка как поля в почтовом штампе.
Подключаем журнал к «Цыплёнку»
Теперь то же самое, но в нашем файле bot.py. Код бота на aiogram в браузере не запускается (там сервер и async), поэтому даю его как обычный текст, а ниже описываю, что ты увидишь в терминале.
import logging
import os
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.types import Message
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
bot = Bot(token=os.environ["BOT_TOKEN"])
dp = Dispatcher()
@dp.message(CommandStart())
async def start_handler(message: Message):
logger.info("Пользователь %s нажал /start", message.from_user.id)
await message.answer("Привет! Я Цыплёнок-помощник 🐤")
async def main():
logger.info("Бот запускается...")
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())Результат: в чате бот ответит «Привет! Я Цыплёнок-помощник 🐤», а у тебя в терминале появятся строчки вроде 2026-06-19 10:00:00 INFO __main__: Бот запускается... и 2026-06-19 10:00:05 INFO __main__: Пользователь 123456789 нажал /start. Теперь ты видишь, кто и когда дёргает бота.
Пара важных деталей:
logger = logging.getLogger(__name__)— создаём «свой» логгер с именем текущего файла. Так в большом проекте будет видно, из какого модуля прилетела запись (%(name)sв формате как раз это и печатает).logger.info("Пользователь %s нажал /start", message.from_user.id)— обрати внимание: мы не склеиваем строку через+или f-строку, а ставим%sи передаём значение отдельным аргументом. Это привычка профи: если уровеньINFOвыключен, logging даже не будет тратить силы на сборку строки.
Пишем журнал не только в консоль, но и в файл
Пока бот крутится в твоём терминале, логи бегут прямо на экран. Но как только ты закроешь окно — вся история пропадёт. А ведь самое интересное часто случается именно тогда, когда ты не смотришь. Поэтому журнал стоит дублировать в файл: тогда наутро ты просто откроешь его и прочитаешь всё, что бот накопил за ночь.
Самый простой способ — добавить в basicConfig параметр filename. Покажу на чистом примере (его можно запустить, но он создаст файл, поэтому смотри на сам код — суть в настройке):
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[
logging.FileHandler("bot.log", encoding="utf-8"), # пишем в файл
logging.StreamHandler(), # и одновременно в консоль
],
)
logging.info("Эта строка попадёт и на экран, и в файл bot.log")
print("Готово: загляни в файл bot.log рядом с программой")Вывод:
2026-06-19 10:00:00,000 INFO Эта строка попадёт и на экран, и в файл bot.log Готово: загляни в файл bot.log рядом с программой
Тут вместо одного места вывода мы передали список handlers — «приёмников» записей. FileHandler("bot.log") складывает всё в файл (обязательно с encoding="utf-8", иначе русские буквы и эмодзи могут превратиться в кракозябры), а StreamHandler() по-прежнему печатает на экран. Один logger.info(...) — и запись летит сразу в оба места. Когда выкатишь «Цыплёнка» на сервер, именно файл bot.log станет твоим главным свидетелем.
Учимся читать traceback — это не страшно
Когда что-то ломается, Python печатает traceback — это не ругань, а маршрут ошибки: «я был тут, потом зашёл туда, и вот на этой строчке всё рухнуло». Читать его нужно снизу вверх: самая нижняя строка говорит, что случилось, а строки выше — где по пути это произошло. Например:
Traceback (most recent call last):
File "bot.py", line 18, in start_handler
result = 10 / 0
ZeroDivisionError: division by zeroРезультат: нижняя строка ZeroDivisionError: division by zero прямо говорит — «деление на ноль», а строка выше показывает файл bot.py, строку 18 и функцию start_handler. То есть ты сразу знаешь и причину, и адрес. Именно такой traceback и приложит logger.exception(...) к твоей записи в журнале — поэтому он так ценен.
Глобальный обработчик ошибок
Журнал — это половина дела. Вторая половина — сделать так, чтобы бот не падал от ошибки. Сейчас, если внутри любого хэндлера вылетит исключение (например, ты обратился к data["name"], а такого ключа нет), aiogram честно напечатает в консоль длинный страшный traceback, а пользователь… просто не получит ответа. Он будет сидеть и думать, что бот его игнорирует.
Аналогия: бот — это официант. Если на кухне разбилась тарелка, плохой официант просто исчезает и не возвращается к столику. Хороший — подходит и говорит: «Извините, небольшая заминка, сейчас всё исправим». Глобальный обработчик ошибок учит «Цыплёнка» быть хорошим официантом.
В aiogram 3.x для этого есть специальный декоратор @dp.errors(). Хэндлер с этим декоратором ловит любую ошибку, которая вылетела в обычных хэндлерах, — как сетка-страховка под канатоходцем.
import logging
from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.types import Message, ErrorEvent
logger = logging.getLogger(__name__)
dp = Dispatcher()
@dp.message(CommandStart())
async def start_handler(message: Message):
# специально ломаем: делим на ноль, чтобы поймать ошибку
result = 10 / 0
await message.answer(f"Результат: {result}")
@dp.errors()
async def errors_handler(event: ErrorEvent):
# event.exception — само исключение, event.update — что пришло от Telegram
logger.exception("Ошибка при обработке обновления: %s", event.exception)
# пробуем вежливо ответить пользователю, если знаем, куда писать
if event.update.message:
await event.update.message.answer(
"Ой, я споткнулся 🐤 Уже разбираюсь, попробуй ещё раз чуть позже."
)
return True # говорим aiogram: ошибку обработали, можно жить дальшеРезультат: пользователь нажимает /start, внутри случается деление на ноль, но вместо тишины он видит «Ой, я споткнулся 🐤 Уже разбираюсь, попробуй ещё раз чуть позже.», а в терминале появляется запись уровня ERROR с полным traceback'ом — ты потом спокойно его прочитаешь и починишь.
Разберём ключевые места:
event: ErrorEvent— это специальный объект aiogram, внутри которого лежитevent.exception(само исключение) иevent.update(то обновление, на котором всё сломалось).logger.exception(...)— особый метод: он пишет сообщение уровняERRORи автоматически добавляет traceback. Использовать его можно только внутри блока обработки ошибки. Это твой лучший друг при отладке.if event.update.message:— проверяем, что ошибка случилась именно на сообщении (а не, скажем, на нажатии inline-кнопки), чтобы знать, куда отправить извинение.return True— сигнал диспетчеру «я разрулил, дальше можно работать». Без него aiogram может посчитать обновление необработанным.
Ловим конкретную ошибку прямо в хэндлере
Глобальный обработчик — это сетка-страховка на крайний случай. Но если ты заранее знаешь, что какое-то место рискованное (например, поход за погодой по сети, как в уроке про хэндлеры сообщений мы учились отвечать на текст), лучше обернуть именно его в try/except и обработать аккуратно на месте.
@dp.message(Command("weather"))
async def weather_handler(message: Message):
try:
temp = await get_weather("Москва") # вдруг сеть отвалится
except Exception:
logger.exception("Не смог получить погоду")
await message.answer("Не дотянулся до прогноза 🌧 Попробуй через минуту.")
return
await message.answer(f"Сейчас в Москве {temp}°C")Результат: если сервис погоды доступен — бот пришлёт «Сейчас в Москве 5°C»; если сеть подвела — пользователь получит «Не дотянулся до прогноза 🌧 Попробуй через минуту.», а ты — запись об ошибке в журнале. Заметь: тут мы решаем проблему точечно и даём пользователю осмысленный совет, а не общую отписку.
Правило простое: известный риск ловим на месте через try/except с понятным сообщением, а глобальный @dp.errors() держим как страховку от того, что мы не предусмотрели.
Частые ошибки и подводные камни
Вот на чём чаще всего спотыкаются новички, когда добавляют логирование и обработку ошибок.
- Забыли вызвать
basicConfig— и логов нет. Если просто написатьlogging.info("привет")без настройки, Python по умолчанию показывает толькоWARNINGи выше, поэтому твоиinfo-записи будто исчезают. Решение: один раз в началеbot.pyвызватьlogging.basicConfig(level=logging.INFO). - Ловят ошибку и молча её прячут. Самое опасное — написать
except Exception: pass. Так ошибка действительно исчезает с глаз, но вместе с ней исчезает и причина: бот тихо не работает, а ты не знаешь почему. Всегда хотя бы логируй:except Exception: logger.exception("..."). - Пишут токен в лог. Соблазнительно при отладке вывести
logger.info("токен: %s", BOT_TOKEN). Не делай этого никогда: лог-файл легко утекает (скриншот, репозиторий, чужой сервер), а токен — это пароль от твоего бота. Кто его получит, тот заберёт бота себе. - Используют
logger.errorвместоlogger.exceptionвнутриexcept. Оба напишут сообщение уровняERROR, но толькоexceptionприложит traceback. А без traceback ты увидишь «что-то сломалось», но не где именно — и снова гадаешь. - Возвращают извинение, но не делают
returnпосле него. Вtry/exceptпосле отправки сообщения об ошибке нужно прервать функцию (return), иначе код пойдёт дальше с «битыми» данными и упадёт второй раз, уже по-настоящему.
Мини-практика: «Цыплёнок» ведёт честный дневник
Теперь твоя очередь. Возьми свой bot.py и доработай его по шагам:
- В самом начале файла настрой
logging.basicConfigс уровнемINFOи форматом, где есть время, уровень и имя логгера. Создай свой логгер черезlogging.getLogger(__name__). - В хэндлере
/startдобавь записьlogger.info(...), которая печатаетidи имя пользователя (message.from_user.first_name) — передавай их через%s, а не f-строкой. - Добавь команду
/divide, которая просит два числа в одном сообщении (например «10 0»), делит первое на второе и шлёт результат. Оберни деление вtry/except: при делении на ноль ответь пользователю «На ноль делить нельзя 🙅», а в журнал запишиlogger.exception(...). - Добавь глобальный
@dp.errors()-хэндлер, который логирует любую непойманную ошибку и отвечает пользователю общим извинением.
Проверь себя так: отправь боту «/divide 10 0» — он должен вежливо ответить про ноль, а не упасть, и при этом оставить запись в терминале. Потом отправь «/divide привет мир» — числа не распарсятся, вылетит другая ошибка, и тут уже сработает твоя глобальная сетка-страховка. Если оба случая обработаны и бот жив — ты справился!
Итоги
Сегодня «Цыплёнок-помощник» повзрослел: он перестал быть капризным малышом, который замолкает при первой неприятности, и стал ответственным помощником, который ведёт дневник и умеет извиняться. Что мы освоили:
- Модуль
logging— умныйprint()со штампиком времени и уровнем важности; настраивается один раз черезbasicConfig. - Пять уровней от
DEBUGдоCRITICAL— регулятор «громкости» бота. - Глобальный
@dp.errors()— сетка-страховка, которая ловит любую непойманную ошибку, логирует её и даёт пользователю понятный ответ вместо тишины. - Точечный
try/exceptсlogger.exception— для мест, где ты заранее ждёшь беды (сеть, парсинг чисел, чужие ключи в данных).
Теперь, когда бот умеет рассказывать о себе и держать удар, самое время подумать о его защите от назойливых пользователей. В следующем уроке мы займёмся middleware и throttling'ом — научим «Цыплёнка» вежливо притормаживать тех, кто долбит команды по сто раз в секунду, чтобы бот не падал от спама. До встречи! 🐤