# Journalisation B : enregistrer les journaux dans un fichier et consigner les actions importantes<no value>

## Qu'allons-nous apprendre ?

- Comment écrire les journaux **dans un fichier**, et pas seulement à l'écran.
- Les **modes d'écriture** : ajouter à la fin du fichier, ou repartir de zéro.
- Comment enregistrer une erreur **avec tous les détails** (Traceback).
- **Quoi consigner** : quelles actions sont importantes et à quel niveau.

## Pourquoi en avons-nous besoin ?

Vous vous souvenez du thème sur l'ordinateur, le système d'exploitation et le réseau ? Ce qui est à l'écran et en mémoire disparaît à la fin du programme. Un **fichier** est conservé sur le stockage. Un journal à l'écran n'aide que celui qui est assis devant **maintenant**. Un journal dans un fichier peut être ouvert **demain**, fouillé, et utilisé pour comprendre ce qui s'est passé pendant la nuit.

Sur les vrais serveurs, les journaux sont le principal outil d'investigation des pannes. « Donne-moi les journaux de 3 heures du matin » est une phrase courante dans une équipe de développement.

## Explication à partir des bases

### Journaliser dans un fichier

On ajoute le paramètre `filename` à `basicConfig`. À partir de là, les enregistrements sont écrits **dans le fichier** au lieu de l'écran. Le fichier est créé automatiquement s'il n'existe pas, **dans le dossier courant** (le Current Working Directory, comme tout chemin relatif).

### Les modes d'écriture (`filemode`)

| Mode | Signification | Quand |
|---|---|---|
| `"a"` (Append) — **par défaut** | Ajoute à la fin du fichier. Les journaux précédents sont conservés. | Presque toujours. On veut un historique. |
| `"w"` (Write) | **Supprime** le contenu précédent et repart de zéro à chaque exécution | En pratique, quand on ne veut que la dernière exécution |

### `logging.exception` — une erreur avec tous les détails

`logging.error("Payment failed")` n'enregistre que le message. Dans un bloc `except`, on peut utiliser **`logging.exception("...")`** : il enregistre au niveau ERROR **et joint aussi le Traceback complet**, si bien que le fichier montre exactement à quelle ligne et dans quelle fonction l'erreur s'est produite. Comme si vous aviez vu le plantage vous-même, sans que le programme plante.

### Qu'est-ce qui vaut la peine d'être consigné ?

| À consigner | Pourquoi |
|---|---|
| Le démarrage et l'arrêt du programme/serveur | Pour savoir quand il a tourné et quand il s'est arrêté |
| Les actions qui modifient des données : création, mise à jour, suppression | « Qui a supprimé la commande ? » |
| Les connexions et déconnexions, les tentatives échouées | La sécurité |
| Chaque erreur attrapée (`except`) | Pour ne pas faire disparaître les problèmes |
| Les appels à des services externes et leur résultat | « Le paiement a-t-il été envoyé ? » |

| À **ne pas** consigner | Pourquoi |
|---|---|
| Les mots de passe, les numéros de carte bancaire, les jetons | La sécurité et la vie privée |
| Chaque tour d'une grande boucle en INFO | Du « bruit » qui cache l'essentiel. Si nécessaire, en DEBUG |

## Syntaxe

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

## Premier exemple de code

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

**À l'écran :**

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

**Dans le fichier `app.log`** (après **deux** exécutions) :

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

## Analyse du code ligne par ligne

| Ligne | Explication |
|---|---|
| `filename="app.log"` | Les journaux sont écrits dans ce fichier, **et non** à l'écran. |
| (pas de `filemode`) | La valeur par défaut est `"a"` : chaque exécution **ajoute** à la fin du fichier. C'est pourquoi après deux exécutions il y a 6 lignes. |
| `encoding="utf-8"` | Permet d'écrire n'importe quel caractère, y compris des lettres non anglaises dans les messages (dans notre code les messages sont en anglais, mais les noms d'utilisateur peuvent être dans n'importe quelle langue). |
| `print("Done...")` | `print` arrive toujours à l'écran. C'est une sortie **pour l'utilisateur**, sans rapport avec le journal. |

Pour lire le fichier : ouvrez-le dans VS Code, ou depuis le terminal avec `cat app.log`. Dans PowerShell, on peut aussi le suivre en temps réel : `Get-Content app.log -Wait`. Sur Mac/Linux : `tail -f app.log`.

## Un autre exemple

**Un journal complet pour un programme qui traite des paiements**, avec `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)
```

**À l'écran :**

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

**Dans `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
```

**Analyse :**

- `filemode="w"` — ici, nous avons choisi d'écraser à chaque exécution, pour qu'il n'y ait en pratique que le dernier journal.
- `%.2f` — un nombre décimal avec 2 chiffres après la virgule (`120.00`). Adapté aux montants d'argent.
- `%r` — affiche la valeur **avec des guillemets** (`'abc'`). Ainsi, on voit clairement ce qui a été reçu, y compris les espaces en trop ou une chaîne vide.
- `logging.exception(...)` — un enregistrement ERROR, **avec le Traceback complet en dessous**. Les deux erreurs sont des `ValueError`, mais pour des raisons différentes. Le fichier montre exactement quelle ligne a lancé chacune : `float()` (ligne 13) ou notre `raise` (ligne 15).
- **Le programme n'a pas planté** : il passe au paiement suivant, et toutes les informations pour enquêter sont conservées.
- `print` résume pour l'utilisateur en une ligne. Le journal raconte toute l'histoire au développeur.

## Erreur courante

**Erreur 1 : « le journal n'est pas écrit / je ne trouve pas le fichier ».** Le fichier est créé dans le **Current Working Directory**. Si vous avez lancé depuis un autre dossier, le fichier est **là-bas**. (Souvenez-vous du thème sur l'ordinateur et le système d'exploitation !)

**Erreur 2 : `logging.error` dans un `except` quand on veut enquêter.**

```python
except ValueError:
    logging.error("Payment failed")
```

Cela dit que ça a échoué, mais **ni pourquoi ni où**. Dans un `except`, `logging.exception` donne le tableau complet sans effort. (Si l'erreur est attendue et ordinaire, comme une mauvaise saisie d'un utilisateur, et que vous n'avez pas besoin de Traceback, vous pouvez utiliser `logging.warning` avec le message de l'erreur : `logging.warning("bad input: %s", error)`.)

**Erreur 3 : `filemode="w"` sur un vrai serveur.** Chaque redémarrage du serveur **efface l'historique**. Juste après un plantage, quand on a le plus besoin du journal, il est effacé. Sur les serveurs : uniquement `"a"`.

**Erreur 4 : croire que `logging.exception` gère l'erreur.** Il ne fait que l'**enregistrer**. La décision de quoi faire (continuer, réessayer, la transmettre avec `raise`) reste la vôtre.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Exercices

Les exercices n'utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d'abord seul, et n'ouvrez la solution qu'ensuite.

### Exercice 1: Un journal dans un fichier

Mettez en place un journal enregistré dans le fichier `shop.log`, au niveau INFO, avec l'heure, le niveau et le message, et qui n'est pas effacé d'une exécution à l'autre. Consignez-y :

- INFO quand la commande numéro 17 est créée ;
- WARNING quand il ne reste que 2 unités en stock.

{{< 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` — le journal est écrit dans un fichier au lieu de l'écran.
- `filemode="a"` — **ajout** (append) : chaque exécution ajoute à la fin du fichier, et l'historique est conservé.
- Le contenu du fichier ressemblera à ceci :

`2026-11-01 10:00:01 INFO     order 17 created`
`2026-11-01 10:00:01 WARNING  low stock: 2 left`
{{< /solution >}}

### Exercice 2: Ajout face à écriture

1. Quelle est la différence entre `filemode="a"` et `filemode="w"` ?
2. Un serveur a planté pendant la nuit et a été redémarré automatiquement. Le journal était configuré avec `"w"`. Quel est le problème ?

{{< solution >}}
1. **`"a"`** (append) — ajoute à la fin du fichier existant. **`"w"`** (write) — supprime le fichier et repart de zéro à chaque exécution.
2. Avec `"w"`, le redémarrage **a effacé le journal** — exactement l'information nécessaire pour comprendre pourquoi le serveur a planté. Pour les journaux, on utilise presque toujours `"a"`.
{{< /solution >}}

### Exercice 3: logging.exception

Écrivez une boucle qui encaisse une liste de paiements. Chaque montant est un texte ; un montant qui n'est pas un nombre est consigné dans le journal avec **tous les détails de l'erreur**, et la boucle continue. À la fin, le montant total est affiché.

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

**Résultat attendu :**

```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` est appelé **dans** le `except`. Il enregistre au niveau ERROR, et ajoute le **Traceback complet** au journal — ainsi on voit exactement où et pourquoi le montant a échoué.
{{< /solution >}}

### Exercice 4: Quoi consigner et quoi ne pas consigner

Pour chaque élément — le consigner dans le journal ou non ? Expliquez.

1. Le numéro de la commande qui a échoué.
2. Le mot de passe tapé par l'utilisateur, quand la connexion a échoué.
3. Le nom d'utilisateur qui a essayé de se connecter.
4. Le numéro complet de la carte bancaire.
5. Le temps qu'a pris une action lente.

{{< solution >}}
1. **À consigner** — sans lui, on ne peut pas enquêter sur la panne.
2. **À ne pas consigner** — les mots de passe ne doivent jamais apparaître dans le journal. Quiconque peut lire le journal les verrait.
3. **À consigner** — important pour la sécurité : qui a essayé de se connecter, et quand.
4. **À ne pas consigner** — une information sensible. Au plus les quatre derniers chiffres.
5. **À consigner** — cela aide à trouver des problèmes de performance.

La règle : assez de détails pour **enquêter** — mais jamais d'informations **sensibles**.
{{< /solution >}}

### Exercice 5: Un journal de connexions

Vous avez une liste de tentatives de connexion. Chaque tentative est un nom d'utilisateur et un mot de passe (deux listes parallèles). Le mot de passe correct pour tous les utilisateurs de l'exercice est `"1234"`. Consignez dans le journal `security.log` :

- INFO pour chaque connexion réussie ;
- WARNING pour chaque mauvais mot de passe (sans consigner le mot de passe !).

À la fin, affichez combien de connexions ont réussi et combien ont échoué.

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

**Résultat attendu :**

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

Remarquez : le WARNING consigne **le nom d'utilisateur** — et non le mot de passe.
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- `basicConfig(filename="app.log")` écrit les journaux dans un fichier sur le stockage, et ils sont conservés après la fin du programme.
- `filemode="a"` (par défaut) ajoute. `"w"` écrase. Sur les serveurs, toujours en ajout.
- `logging.exception("...")` dans un `except` enregistre une ERROR **avec le Traceback complet**, sans que le programme plante.
- Consignez le démarrage/l'arrêt, les modifications de données, la sécurité et les erreurs. Ne consignez pas les mots de passe ni les informations sensibles.
- Le fichier est créé dans le Current Working Directory. `tail -f` / `Get-Content -Wait` le suivent en temps réel.

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

## Documentation officielle

- [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 -->
