Логирование B: сохранение логов в файл и запись важных действий
- Настраивать логирование в файл и понимать, почему лог «переживает» запуски
- Различать режим append (дописывание) и режим write (перезапись)
- Записывать исключение вместе с полным Traceback с помощью
logging.exception - Решать, какие действия важно записывать и на каком уровне
Что мы изучим?
- Как писать логи в файл, а не только на экран.
- Режимы записи: дописывать в конец файла или начинать заново.
- Как записать ошибку со всеми деталями (Traceback).
- Что записывать: какие действия важны и на каком уровне.
Зачем это нужно?
Помните тему о компьютере, операционной системе и сети? То, что на экране и в памяти, исчезает, когда программа завершается. Файл хранится на накопителе (Storage). Лог на экране помогает только тому, кто сидит перед ним сейчас. Лог в файле можно открыть завтра, искать в нём и понять, что происходило ночью.
На настоящих серверах логи — главный инструмент расследования сбоев. «Дай мне логи с трёх часов ночи» — обычная фраза в команде разработки.
Объяснение с основ
Логирование в файл
В basicConfig добавляют параметр filename. С этого момента записи пишутся в файл, а не на экран. Файл создаётся автоматически, если его нет, в текущей папке (Current Working Directory, как любой относительный путь).
Режимы записи (filemode)
| Режим | Значение | Когда |
|---|---|---|
"a" (Append) — по умолчанию | Дописывает в конец файла. Предыдущие логи сохраняются. | Почти всегда. Нужна история. |
"w" (Write) | Удаляет прежнее содержимое и начинает заново при каждом запуске | На практике — когда нужен только последний запуск |
logging.exception — ошибка со всеми деталями
logging.error("Payment failed") записывает только сообщение. Внутри блока except можно использовать logging.exception("..."): он делает запись на уровне ERROR и прикладывает полный Traceback, так что в файле видно, на какой строке и в какой функции произошла ошибка. Как будто вы сами видели падение — но программа не упала.
Что стоит записывать?
| Стоит записывать | Почему |
|---|---|
| Запуск и завершение программы/сервера | Чтобы знать, когда она работала и когда остановилась |
| Действия, меняющие данные: создание, обновление, удаление | «Кто удалил заказ?» |
| Вход и выход, неудачные попытки | Безопасность |
Каждую пойманную ошибку (except) | Чтобы проблемы не исчезали |
| Обращения к внешним сервисам и их результат | «Был ли отправлен платёж?» |
| Не записывать | Почему |
|---|---|
| Пароли, номера кредитных карт, токены | Безопасность и приватность |
| Каждый круг большого цикла на уровне INFO | «Шум», который скрывает важное. Если уж записывать — то на DEBUG |
Синтаксис
import logging
logging.basicConfig(
filename="app.log", # write to this file (relative to the working directory)
filemode="a", # "a" = append (default), "w" = overwrite each run
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
encoding="utf-8", # safe for any characters in messages
)
try:
risky()
except SomeError:
logging.exception("What we were trying to do") # ERROR + full tracebackПервый пример кода
week-2/file_logger.py:
import logging
logging.basicConfig(
filename="app.log",
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
encoding="utf-8",
)
logging.info("Program started")
logging.warning("Low disk space")
logging.info("Program finished")
print("Done. Check app.log")На экране:
Done. Check app.logВ файле app.log (после двух запусков):
2026-10-06 10:02:41,203 INFO Program started
2026-10-06 10:02:41,203 WARNING Low disk space
2026-10-06 10:02:41,204 INFO Program finished
2026-10-06 10:03:15,881 INFO Program started
2026-10-06 10:03:15,881 WARNING Low disk space
2026-10-06 10:03:15,882 INFO Program finishedРазбор кода построчно
| Строка | Объяснение |
|---|---|
filename="app.log" | Логи пишутся в этот файл, а не на экран. |
(нет filemode) | По умолчанию "a": каждый запуск дописывает в конец файла. Поэтому после двух запусков в нём 6 строк. |
encoding="utf-8" | Позволяет записывать любые символы, включая неанглийские буквы в сообщениях (в нашем коде сообщения на английском, но имена пользователей могут быть на любом языке). |
print("Done...") | print по-прежнему выводит на экран. Это вывод для пользователя, и к логу он отношения не имеет. |
Чтобы прочитать файл: откройте его в VS Code или из терминала через cat app.log. В PowerShell за ним можно следить в реальном времени: Get-Content app.log -Wait. В Mac/Linux: tail -f app.log.
Ещё один пример
Полный лог программы, которая обрабатывает платежи, с logging.exception. week-2/payments.py:
import logging
logging.basicConfig(
filename="payments.log",
filemode="w",
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
encoding="utf-8",
)
def charge(customer, amount_text):
amount = float(amount_text)
if amount <= 0:
raise ValueError("amount must be positive, got " + str(amount))
logging.info("Charged %s: %.2f NIS", customer, amount)
return amount
payments = [("dana", "120"), ("avi", "abc"), ("noa", "-5"), ("yossi", "80.5")]
logging.info("Batch started: %s payments", len(payments))
total = 0
failed = 0
for customer, amount_text in payments:
try:
total = total + charge(customer, amount_text)
except ValueError:
failed = failed + 1
logging.exception("Payment failed for %s (input: %r)", customer, amount_text)
logging.info("Batch finished: total %.2f NIS, %s failed", total, failed)
print("Batch done. Total:", total, "| Failed:", failed)На экране:
Batch done. Total: 200.5 | Failed: 2В payments.log:
2026-10-06 10:20:01,010 INFO Batch started: 4 payments
2026-10-06 10:20:01,010 INFO Charged dana: 120.00 NIS
2026-10-06 10:20:01,011 ERROR Payment failed for avi (input: 'abc')
Traceback (most recent call last):
File "payments.py", line 28, in <module>
total = total + charge(customer, amount_text)
File "payments.py", line 13, in charge
amount = float(amount_text)
ValueError: could not convert string to float: 'abc'
2026-10-06 10:20:01,012 ERROR Payment failed for noa (input: '-5')
Traceback (most recent call last):
File "payments.py", line 28, in <module>
total = total + charge(customer, amount_text)
File "payments.py", line 15, in charge
raise ValueError("amount must be positive, got " + str(amount))
ValueError: amount must be positive, got -5.0
2026-10-06 10:20:01,012 INFO Charged yossi: 80.50 NIS
2026-10-06 10:20:01,012 INFO Batch finished: total 200.50 NIS, 2 failedРазбор:
filemode="w"— здесь мы решили перезаписывать файл при каждом запуске, так что фактически в нём только последний лог.%.2f— десятичное число с 2 цифрами после точки (120.00). Подходит для денежных сумм.%r— показывает значение в кавычках ('abc'). Так чётко видно, что пришло, включая лишние пробелы или пустую строку.logging.exception(...)— запись ERROR с полным Traceback под ней. Обе ошибки —ValueError, но по разным причинам. В файле точно видно, какая строка выбросила каждую:float()(строка 13) или нашraise(строка 15).- Программа не упала: она переходит к следующему платежу, а вся информация для расследования сохранена.
printподводит итог для пользователя одной строкой. Лог рассказывает разработчику всю историю.
Типичная ошибка
Ошибка 1: «лог не пишется / я не могу найти файл». Файл создаётся в текущей папке (Current Working Directory). Если вы запускали из другой папки, файл там. (Вспомните тему о компьютере и операционной системе!)
Ошибка 2: logging.error внутри except, когда нужно расследовать.
except ValueError:
logging.error("Payment failed")Сказано, что не удалось, но не сказано почему и где. Внутри except logging.exception без усилий даёт полную картину. (Если ошибка ожидаемая и обычная, например неверный ввод пользователя, и Traceback не нужен, можно использовать logging.warning с сообщением ошибки: logging.warning("bad input: %s", error).)
Ошибка 3: filemode="w" на настоящем сервере. Каждый перезапуск сервера стирает историю. Как раз после падения, когда лог нужнее всего, он удалён. На серверах — только "a".
Ошибка 4: думать, что logging.exception обрабатывает ошибку. Он только записывает её. Решение, что делать (продолжить, попробовать снова, передать дальше через raise), по-прежнему за вами.
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: Лог в файл
Настройте лог, который сохраняется в файл shop.log, на уровне INFO, со временем, уровнем и сообщением, и который не удаляется между запусками. Запишите в него:
- INFO, когда создан заказ номер 17;
- WARNING, когда на складе осталось только 2 единицы.
Показать решение
import logging
logging.basicConfig(
filename="shop.log",
filemode="a",
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
encoding="utf-8",
)
logging.info("order 17 created")
logging.warning("low stock: 2 left")filename— лог пишется в файл, а не на экран.filemode="a"— append (дописывание): каждый запуск дописывает в конец файла, и история сохраняется.- Содержимое файла будет выглядеть так:
2026-11-01 10:00:01 INFO order 17 created
2026-11-01 10:00:01 WARNING low stock: 2 left
Упражнение 2: append или write
- В чём разница между
filemode="a"иfilemode="w"? - Сервер упал ночью и был автоматически перезапущен. Лог был настроен с
"w". В чём проблема?
Показать решение
"a"(append) — дописывает в конец существующего файла."w"(write) — удаляет файл и начинает заново при каждом запуске.- С
"w"перезапуск удалил лог — именно ту информацию, которая нужна, чтобы понять, почему сервер упал. Для логов почти всегда используют"a".
Упражнение 3: logging.exception
Напишите цикл, который проводит список платежей. Каждая сумма — текст; сумма, которая не является числом, записывается в лог со всеми деталями ошибки, и цикл продолжается. В конце печатается общая сумма.
payments = ["120", "abc", "80"]Ожидаемый вывод:
Total charged: 200Показать решение
import logging
logging.basicConfig(filename="payments.log", filemode="a", level=logging.INFO, encoding="utf-8")
payments = ["120", "abc", "80"]
total = 0
for amount_text in payments:
try:
total = total + int(amount_text)
logging.info("charged %s", amount_text)
except ValueError:
logging.exception("invalid amount: %s", amount_text)
print("Total charged:", total)Total charged: 200logging.exception вызывается внутри except. Он делает запись на уровне ERROR и добавляет в лог полный Traceback — так точно видно, где и почему сумма не прошла.
Упражнение 4: Что записывать, а что нет
Для каждого пункта — записывать его в лог или нет? Объясните.
- Номер заказа, который не прошёл.
- Пароль, который ввёл пользователь, когда вход не удался.
- Имя пользователя, который пытался войти.
- Полный номер кредитной карты.
- Сколько времени заняло медленное действие.
Показать решение
- Записывать — без этого сбой не расследовать.
- Не записывать — пароли никогда не должны появляться в логе. Любой, кто может прочитать лог, увидел бы их.
- Записывать — важно для безопасности: кто пытался войти и когда.
- Не записывать — чувствительная информация. В крайнем случае последние четыре цифры.
- Записывать — это помогает находить проблемы производительности.
Правило: достаточно деталей, чтобы расследовать, — но никогда чувствительной информации.
Упражнение 5: Журнал входов
У вас есть список попыток входа. Каждая попытка — имя пользователя и пароль (два параллельных списка). Правильный пароль для всех пользователей в упражнении — "1234". Запишите в лог security.log:
- INFO для каждого успешного входа;
- WARNING для каждого неверного пароля (не записывая сам пароль!).
В конце напечатайте, сколько входов удалось и сколько нет.
users = ["dana", "avi", "dana", "noa"]
passwords = ["1234", "0000", "1111", "1234"]Ожидаемый вывод:
Successful: 2, failed: 2Показать решение
import logging
CORRECT_PASSWORD = "1234"
logging.basicConfig(
filename="security.log",
filemode="a",
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
encoding="utf-8",
)
users = ["dana", "avi", "dana", "noa"]
passwords = ["1234", "0000", "1111", "1234"]
successful = 0
failed = 0
for i in range(len(users)):
if passwords[i] == CORRECT_PASSWORD:
successful = successful + 1
logging.info("login ok: %s", users[i])
else:
failed = failed + 1
logging.warning("wrong password for %s", users[i])
logging.info("summary: %s successful, %s failed", successful, failed)
print("Successful: " + str(successful) + ", failed: " + str(failed))Successful: 2, failed: 2Обратите внимание: WARNING записывает имя пользователя — а не пароль.
Итоги
basicConfig(filename="app.log")пишет логи в файл на накопителе, и они сохраняются после завершения программы.filemode="a"(по умолчанию) дописывает."w"перезаписывает. На серверах — всегда дописывание.logging.exception("...")внутриexceptзаписывает ERROR с полным Traceback, не роняя программу.- Записывайте запуск/завершение, изменения данных, события безопасности и ошибки. Не записывайте пароли и чувствительную информацию.
- Файл создаётся в текущей папке.
tail -f/Get-Content -Waitпозволяют следить за ним в реальном времени.