# Логирование B: сохранение логов в файл и запись важных действий<no value>

## Что мы изучим?

- Как писать логи **в файл**, а не только на экран.
- **Режимы записи**: дописывать в конец файла или начинать заново.
- Как записать ошибку **со всеми деталями** (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 |

## Синтаксис

```python
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`:

```python
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")
```

**На экране:**

```text
Done. Check app.log
```

**В файле `app.log`** (после **двух** запусков):

```text
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`:

```python
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)
```

**На экране:**

```text
Batch done. Total: 200.5 | Failed: 2
```

**В `payments.log`:**

```text
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`, когда нужно расследовать.**

```python
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`), по-прежнему за вами.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

### Упражнение 1: Лог в файл

Настройте лог, который сохраняется в файл `shop.log`, на уровне INFO, со временем, уровнем и сообщением, и который не удаляется между запусками. Запишите в него:

- INFO, когда создан заказ номер 17;
- WARNING, когда на складе осталось только 2 единицы.

{{< solution >}}
```python
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`
{{< /solution >}}

### Упражнение 2: append или write

1. В чём разница между `filemode="a"` и `filemode="w"`?
2. Сервер упал ночью и был автоматически перезапущен. Лог был настроен с `"w"`. В чём проблема?

{{< solution >}}
1. **`"a"`** (append) — дописывает в конец существующего файла. **`"w"`** (write) — удаляет файл и начинает заново при каждом запуске.
2. С `"w"` перезапуск **удалил лог** — именно ту информацию, которая нужна, чтобы понять, почему сервер упал. Для логов почти всегда используют `"a"`.
{{< /solution >}}

### Упражнение 3: logging.exception

Напишите цикл, который проводит список платежей. Каждая сумма — текст; сумма, которая не является числом, записывается в лог **со всеми деталями ошибки**, и цикл продолжается. В конце печатается общая сумма.

```python
payments = ["120", "abc", "80"]
```

**Ожидаемый вывод:**

```text
Total charged: 200
```

{{< solution >}}
```python
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)
```

```text
Total charged: 200
```

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

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

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

1. Номер заказа, который не прошёл.
2. Пароль, который ввёл пользователь, когда вход не удался.
3. Имя пользователя, который пытался войти.
4. Полный номер кредитной карты.
5. Сколько времени заняло медленное действие.

{{< solution >}}
1. **Записывать** — без этого сбой не расследовать.
2. **Не записывать** — пароли никогда не должны появляться в логе. Любой, кто может прочитать лог, увидел бы их.
3. **Записывать** — важно для безопасности: кто пытался войти и когда.
4. **Не записывать** — чувствительная информация. В крайнем случае последние четыре цифры.
5. **Записывать** — это помогает находить проблемы производительности.

Правило: достаточно деталей, чтобы **расследовать**, — но никогда **чувствительной** информации.
{{< /solution >}}

### Упражнение 5: Журнал входов

У вас есть список попыток входа. Каждая попытка — имя пользователя и пароль (два параллельных списка). Правильный пароль для всех пользователей в упражнении — `"1234"`. Запишите в лог `security.log`:

- INFO для каждого успешного входа;
- WARNING для каждого неверного пароля (не записывая сам пароль!).

В конце напечатайте, сколько входов удалось и сколько нет.

```python
users = ["dana", "avi", "dana", "noa"]
passwords = ["1234", "0000", "1111", "1234"]
```

**Ожидаемый вывод:**

```text
Successful: 2, failed: 2
```

{{< solution >}}
```python
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))
```

```text
Successful: 2, failed: 2
```

Обратите внимание: WARNING записывает **имя пользователя** — а не пароль.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- `basicConfig(filename="app.log")` пишет логи в файл на накопителе, и они сохраняются после завершения программы.
- `filemode="a"` (по умолчанию) дописывает. `"w"` перезаписывает. На серверах — всегда дописывание.
- `logging.exception("...")` внутри `except` записывает ERROR **с полным Traceback**, не роняя программу.
- Записывайте запуск/завершение, изменения данных, события безопасности и ошибки. Не записывайте пароли и чувствительную информацию.
- Файл создаётся в текущей папке. `tail -f` / `Get-Content -Wait` позволяют следить за ним в реальном времени.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

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

- [Logging HOWTO — Logging to a file](https://docs.python.org/3/howto/logging.html#logging-to-a-file)
- [logging.basicConfig()](https://docs.python.org/3/library/logging.html#logging.basicConfig)
- [logging.exception()](https://docs.python.org/3/library/logging.html#logging.exception)

<!-- docs:end -->
