Логирование 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

  1. В чём разница между filemode="a" и filemode="w"?
  2. Сервер упал ночью и был автоматически перезапущен. Лог был настроен с "w". В чём проблема?
Показать решение
  1. "a" (append) — дописывает в конец существующего файла. "w" (write) — удаляет файл и начинает заново при каждом запуске.
  2. С "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: 200

logging.exception вызывается внутри except. Он делает запись на уровне ERROR и добавляет в лог полный Traceback — так точно видно, где и почему сумма не прошла.

Упражнение 4: Что записывать, а что нет

Для каждого пункта — записывать его в лог или нет? Объясните.

  1. Номер заказа, который не прошёл.
  2. Пароль, который ввёл пользователь, когда вход не удался.
  3. Имя пользователя, который пытался войти.
  4. Полный номер кредитной карты.
  5. Сколько времени заняло медленное действие.
Показать решение
  1. Записывать — без этого сбой не расследовать.
  2. Не записывать — пароли никогда не должны появляться в логе. Любой, кто может прочитать лог, увидел бы их.
  3. Записывать — важно для безопасности: кто пытался войти и когда.
  4. Не записывать — чувствительная информация. В крайнем случае последние четыре цифры.
  5. Записывать — это помогает находить проблемы производительности.

Правило: достаточно деталей, чтобы расследовать, — но никогда чувствительной информации.

Упражнение 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 позволяют следить за ним в реальном времени.

Официальная документация