Bibliothèque · Résumé et avis

Observability Engineering

De Charity Majors, Liz Fong-Jones et George Miranda. Déboguer une panne que personne n'avait prévue, et ce qui vaut d'être retenu du livre.

FR EN
Couverture de Observability Engineering, Majors, Fong-Jones et Miranda, 1re édition

Observability Engineering

Observability Engineering: Achieving Production Excellence

6.4 /10

« Une thèse juste, des auteurs vendeurs, du code à ne pas copier et des calculs à refaire : on lit les idées, pas les listings. »

  • AuteurCharity Majors, Liz Fong-Jones, George Miranda
  • VOO'Reilly, 2022 · 1re édition, 321 pages
  • LangueEn anglais uniquement
  • Fiche~34 min de lecture
Notation du livre sur 5 dimensionsIdées8/10Applicable6/10Lisibilité7/10Actualité6/10Exemples5/10

Déboguer une panne que personne n'avait prévue, en gardant un événement riche par requête et en l'interrogeant après coup.

Pourquoi ce livre

Un utilisateur écrit que l'export de ses données tourne dans le vide. Vos graphiques sont verts et aucune alerte ne sonne. Vous ne savez pas où chercher, parce que personne n'avait imaginé cette panne. C'est le problème que traite ce livre.

Charity Majors, Liz Fong-Jones et George Miranda y défendent l'observabilité : la capacité d'expliquer n'importe quel état de votre système, même inédit. On interroge pour cela des données déjà enregistrées, sans écrire ni livrer de nouveau code (p. 6). Elle s'oppose au monitoring, qui surveille des seuils fixés à l'avance. Tout le livre découle de cette opposition. Il traite d'abord de quoi enregistrer (idées 1 à 3), puis de la façon de le recueillir et de l'interroger (idées 4 et 5). Viennent ensuite qui s'en sert et par où commencer (idées 6 et 7), puis comment alerter sans réveiller pour rien (idées 8 et 9). Il finit sur le volume et un cas PHP (idées 10 et 11).

Les trois auteurs sont aussi des vendeurs, et ils le disent : « Pour être juste, nous, les auteurs de ce livre, sommes aussi des vendeurs dans le domaine de l'observabilité » (p. 8). Leur outil, Honeycomb, revient 63 fois dans le texte : ce n'est pas un prospectus. Le biais se concentre pourtant en trois endroits que je signale : l'idée 7 (ch. 10), les alertes de budget d'erreur (idée 9) (ch. 13) et la démonstration de l'idée 5 (ch. 8). Mon « tri » est une première lecture rapide de chaque chapitre, avant la lecture complète des chapitres retenus.

Je l'ai ouvert pour combler le chapitre 6 du Grand livre, qui traite de l'observabilité. Je laisse de côté les chapitres d'organisation : équipes, culture, modèle de maturité. Je garde ce qui change la façon de déboguer.

Les idées qui restent

1Observabilité contre monitoring : le test des états jamais vus

Prenons une panne d'un genre nouveau. Le p99 d'un service, le temps sous lequel répondent 99 % des requêtes, est excellent. Des clients se plaignent pourtant de timeouts. Le livre en fait un test (pp. 5-6). Peut-on trouver des timeouts cachés quand les p99, p99.9 et p99.99 sont rapides ? Peut-on isoler les requêtes du 142e utilisateur le plus lent ? Le rang est arbitraire : un p99 ne désigne jamais un utilisateur précis (p. 5). Si la réponse est non, on fait du monitoring, pas de l'observabilité.

Le monitoring repose sur la métrique, un nombre unique avec des étiquettes facultatives : « Une métrique est un nombre unique, avec des étiquettes ajoutées en option pour regrouper et chercher ces nombres » (p. 9). Une machine compare ce nombre à un seuil (par exemple : le processeur dépasse 80 %) et décide d'alerter ou non (p. 20). Cela marche pour ce qu'on a prévu : « Le monitoring sert aux connus-inconnus, mais l'observabilité sert aux inconnus-inconnus » (p. 13). Un connu-inconnu est une panne qu'on sait possible sans savoir quand elle viendra, la base qui sature par exemple. Un inconnu-inconnu est une panne qu'on n'a pas imaginée.

La définition des auteurs tient en une condition : « Si vous comprenez n'importe quel état bizarre ou inédit sans avoir à livrer de nouveau code, vous avez de l'observabilité » (p. 6). Livrer du code pour ajouter un log suppose qu'on savait déjà quoi chercher. Les auteurs admettent que la barre est haute (p. 6). Le mot vient de la théorie du contrôle (Rudolf Kálmán, 1960, p. 4).

Pourquoi les métriques n'y suffisent pas ? Question de cardinalité, le nombre de valeurs différentes d'un champ (pp. 13-14). Un identifiant d'utilisateur en a autant que d'utilisateurs, un code de statut HTTP en a quelques-unes. Les champs à haute cardinalité (identifiant d'utilisateur, de panier, de requête, nom d'hôte, version déployée) sont presque toujours les plus utiles pour déboguer : un identifiant unique est l'aiguille qu'on cherche dans la botte de foin (p. 14). Or « malheureusement, les outils à métriques ne savent traiter que des dimensions à basse cardinalité, à toute échelle raisonnable » (p. 14). Une dimension est un champ de la donnée, comme l'identifiant d'utilisateur ou le nom d'hôte. La raison, hors livre : chaque combinaison de valeurs d'étiquettes crée une série de mesures que l'outil stocke à part. Une étiquette « utilisateur » en crée une par utilisateur.

Le passage de l'un à l'autre est à sens unique : « On peut toujours dégrader une valeur à haute cardinalité en quelque chose de moins précis » (p. 14), on ne fait jamais l'inverse (idée 2). La dimensionnalité, elle, est le nombre de dimensions, donc de champs, de la donnée (p. 14).

Côté tableaux de bord (des pages de graphiques), le livre est net : « Les tableaux de bord sont pourtant un mauvais choix pour découvrir de nouveaux problèmes en débogage » (p. 20). Le cas typique est le saute-outil (pp. 23-24). Un pic d'erreurs apparaît sur un graphique. On ouvre d'autres graphiques, on cherche dans les logs, puis on copie l'identifiant de requête dans l'outil de traces. Cet outil suit une requête de service en service (idée 3). Si la requête n'était pas tracée, on recommence. Le contexte se perd à chaque saut.

Reste la conséquence humaine : le meilleur débogueur de l'équipe est-il toujours celui qui est là depuis le plus longtemps ? Avec de bons outils, ce serait plutôt l'ingénieur le plus curieux (pp. 25-26).

Ma lecture

Un développeur web connaît déjà le côté monitoring : logs grepés, graphiques, alertes à seuil. Le neuf est de garder le contexte brut de chaque requête, identifiant d'utilisateur compris, et de le questionner après coup.

Mes réserves

Les auteurs jugent le monitoring traditionnel catastrophiquement inefficace (p. 9) sans donnée comparative dans ces chapitres. Leurs onze hypothèses cachées du monitoring (p. 10), comme le monolithe ou la base unique, sont des procès d'intention : une équipe à métriques et à SLO (idée 8) gère très bien du distribué. Ils rejettent aussi la définition en « trois piliers » (métriques, logs, traces), bonne selon eux pour des vendeurs (p. 7), et lui préfèrent trois autres piliers. Ce sont la haute cardinalité, la haute dimensionnalité et l'explorabilité, la capacité de poser n'importe quelle question aux données (pp. 16 et 18).

2Un événement large par requête, au lieu de métriques éparses

Si l'on veut interroger après coup avec des champs à haute cardinalité, il faut un format qui les garde ensemble. C'est l'événement : « un événement est un enregistrement de tout ce qui s'est passé pendant qu'une requête particulière interagissait avec votre service » (p. 52). La requête est l'unité de travail : télécharger un fichier ou répondre à une requête HTTP (p. 56).

Le mécanisme est simple. À l'entrée de la requête, on crée une map vide. Pendant la vie de la requête, on y ajoute tout détail intéressant : identifiants, valeurs de variables, en-têtes, paramètres, durée des appels distants. À la sortie ou à l'erreur, la map entière part comme un seul enregistrement clé-valeur (p. 52), vers un backend de données. C'est le magasin de l'outil d'observabilité, qui doit accepter des événements arbitrairement larges (p. 57). On interroge ensuite ce magasin en filtrant et en regroupant par n'importe quel champ (p. 52). Avec une instrumentation mûre (le code qui remplit l'événement, idée 4), « il est courant que les événements contiennent 300 à 400 dimensions chacun » (p. 58).

Mon image en PHP

Le geste tient en quelques lignes. C'est une esquisse que je n'ai pas exécutée. Ici error_log écrit la ligne dans un fichier. Un agent de collecte la lit et l'envoie au backend, sans quoi ce n'est qu'un log structuré. On l'interroge ensuite comme en SQL, avec un WHERE user_id = 142 et un GROUP BY build.

$start = microtime(true);
$event = ['route' => $_SERVER['REQUEST_URI'], 'build' => BUILD_ID];
register_shutdown_function(function () use (&$event, $start) {
    $event['duration_ms'] = (int) ((microtime(true) - $start) * 1000);
    $event['status'] = http_response_code();
    error_log(json_encode($event)); // une ligne JSON par requête
});
// n'importe où dans le code : $event['user_id'] = $user->id;

Pourquoi pas des métriques ? Une métrique est une mesure pré-agrégée, par exemple la moyenne du temps de chargement sur les cinq dernières secondes. Une fois la moyenne calculée, on ne retrouve plus le détail : c'est le plus fin qu'on puisse regarder (pp. 53-54). Chaque nouvelle question exige donc une nouvelle métrique, définie avant le bug (p. 14). Une requête participe à des centaines de métriques déconnectées, sans fil qui dise lesquelles viennent de la même requête. À l'inverse, prenons 1 000 événements portant chacun leur temps de chargement. On retrouve la même moyenne, et aussi celle par seconde ou par utilisateur (p. 54).

Les logs non structurés échouent autrement. Ce sont des lignes de texte pour humains. Une même unité de travail s'éclate en plusieurs lignes, avec un identifiant commun seulement parfois. Une connexion produit cinq lignes de log entre 6:01:00 et 6:01:19. Le livre les fusionne en un événement unique de onze champs, avec duration_ms=123 et un identifiant de trace (pp. 55-57). Les logs structurés (même contenu en clé-valeur) ne suffisent pas non plus : « Beaucoup de logs ne sont que des portions d'événements, qu'ils soient structurés ou non » (p. 56). La thèse n'est donc pas « logs contre événements » : les logs structurés sont recommandés (p. 55), à condition d'avoir une ligne par unité de travail, avec tout le contexte.

Dernier point, le schéma : les auteurs refusent d'en imposer un, car prédéfinir un schéma oblige à prévoir les dimensions à capturer (p. 58). C'est justement ce que l'observabilité cherche à éviter.

Le coût caché

Mon constat. Le livre ne chiffre pas le coût de stockage d'événements à 300 ou 400 champs par requête. Il renvoie à l'échantillonnage (idée 10). Il ne dit rien non plus des données personnelles dans ces chapitres, malgré ses propres exemples de champs app.api_key et identifiant d'utilisateur (p. 15). Il y revient plus loin (ch. 18), sur les chaînes de collecte de télémétrie (les « pipelines », idée 11). Ne présentez pas l'événement large comme gratuit.

3Une trace : le même identifiant, recopié de service en service

Un événement décrit une requête dans un service. Quand la requête traverse trois services, il faut les recoller, sans quoi on ne sait pas lequel est lent. La réponse est la trace : « Les traces distribuées ne sont qu'une série d'événements reliés entre eux » (p. 61). Sans cette liaison, une base lente ralentit les services qui l'appellent. Trois ou quatre couches plus haut, des dizaines de services paraissent lents sans qu'on retrouve la source (p. 62). Avec un identifiant commun, on remonte la chaîne jusqu'au maillon vraiment lent.

Voici le vocabulaire. La trace se dessine en cascade (waterfall). « Chaque morceau de cette cascade s'appelle un span de trace, ou span en bref » (p. 63). Un span est donc un événement de l'idée 2, celui d'une étape de la requête. Un span est soit la racine, soit imbriqué dans un autre : si A appelle B qui appelle C, A est le parent de B et B le parent de C (p. 63). Chaque span porte cinq données obligatoires (pp. 63-64) :

  • trace_id : l'identifiant de toute la requête, créé par le span racine et propagé à chaque étape (p. 64).
  • span_id : l'identifiant unique du span.
  • parent_id : le span appelant, absent du span racine : c'est ainsi qu'on reconnaît la racine (p. 64).
  • un horodatage de début.
  • une durée.

Reste le passage d'un service à l'autre, la propagation. Le service appelant écrit ces identifiants dans les en-têtes de sa requête sortante, et le service appelé les lit. C'est la façon la plus courante de partager l'information (p. 67). L'exemple du livre utilise le format B3, un jeu d'en-têtes de traçage. X-B3-TraceId porte l'identifiant de la trace. Dans X-B3-ParentSpanId, l'appelant écrit son propre span_id, que l'appelé lit comme l'identifiant de son parent (p. 67). L'exemple compte au minimum trois spans : la requête d'origine, l'appel à un service d'authentification et l'appel à un service de noms (p. 66).

La trace sert aussi hors RPC (appel de fonction à distance), car n'importe quel ensemble d'événements reliés peut être cousu en traces (p. 71). Trois usages. On peut isoler dans leurs propres spans les morceaux de code coûteux (les « blocs chauds »), comme la désérialisation JSON, même dans un monolithe. On peut créer un span par morceau d'un lot de travail. On peut coudre des logs multi-lignes en cascade, pis-aller utile pour débuter mais déconseillé à long terme (pp. 70-71).

Côté PHP

Hors livre, d'après le standard W3C. Si vous propagez déjà un X-Request-ID d'un service à l'autre, vous avez fait la moitié du chemin. La trace ajoute le chaînage parent-enfant et la durée de chaque maillon. D'après la spécification W3C Trace Context, l'en-tête standard actuel s'appelle traceparent et regroupe version, identifiant de trace, identifiant du span parent et drapeaux. Le livre cite « W3C ou B3 » (p. 67) sans jamais écrire traceparent.

Le code d'exemple (ch. 6) est du pseudo-Go qui ne compile pas, et les auteurs le jugent eux-mêmes un peu artificiel (p. 69). Exemple de défaut : time.Now().Sub(startTime) renvoie une time.Duration, en nanosecondes, que le code range dans un champ nommé duration_ms. Le code sert à voir les cinq champs et la propagation, pas à être exécuté.

4OpenTelemetry : instrumenter une fois, envoyer où l'on veut

Chaque éditeur d'outil fournit sa propre bibliothèque d'instrumentation. Si vous en changez, il faut tout réinstrumenter, et deux outils en parallèle doublent le coût de mesure (p. 74). Le remède est OpenTelemetry (OTel), un standard ouvert né en 2019 de la fusion d'OpenTracing (2016) et d'OpenCensus (2017) sous la CNCF (pp. 74-75). Il est devenu l'unique standard open source d'instrumentation d'applications (p. 75). La promesse : « vous pouvez instrumenter le code de votre application une seule fois et envoyer vos données de télémétrie vers le système de votre choix » (p. 75). Onze langages sont listés, PHP compris, mais tout le code du livre est en Go (p. 75).

Quatre composants suffisent pour suivre (le glossaire du livre en compte sept, pp. 75-76). L'API est la spécification : vous instrumentez sans vous soucier de l'implémentation. Le SDK est l'implémentation, qui garde l'état et groupe les données avant envoi. L'exporteur est le module du SDK qui traduit les objets en mémoire vers le format de la destination, OTLP par défaut (le protocole d'OpenTelemetry). Le Collector est un programme autonome qui reçoit les données, les traite, puis les envoie vers une ou plusieurs destinations. Hors livre, d'après la documentation d'OTel : une fois l'application branchée sur le Collector, changer de destination se fait dans sa configuration.

On procède en deux temps. D'abord l'instrumentation automatique : OTel génère des spans pour les appels HTTP, gRPC et base de données, entrants et sortants. On obtient au moins le squelette de qui appelle qui entre les microservices et leurs dépendances (p. 77). Ensuite l'instrumentation personnalisée, là où se trouve la valeur : « le vrai pouvoir de l'observabilité vient des attributs personnalisés qui ajoutent du contexte pour vous aider à déboguer la façon dont votre logique métier voulue fonctionne réellement » (p. 73). Un attribut est un champ de l'événement de l'idée 2 : identifiant de client, de shard, erreurs. On le pose sur le span en cours, ou sur un span ouvert autour d'une étape interne coûteuse (pp. 78-79).

Ce qu'on code vraiment

Hors livre, esquisse Go non compilée. Dans le handler d'un export de données, on ouvre un span, on y pose des attributs, puis on le ferme. C'est l'équivalent OTel du $event['user_id'] de l'idée 2.

ctx, span := tracer.Start(ctx, "export")
defer span.End()
span.SetAttributes(attribute.String("user.id", userID), attribute.Int("rows", n))

Les métriques, elles, s'enregistrent comme attributs du span : « En général, la plupart des métriques, comme les mesures et les compteurs, devraient être enregistrées comme attributs du span de trace auquel elles se rapportent » (p. 79). Ainsi rangée, la métrique reste une valeur de l'événement, regroupable plus tard comme n'importe quel champ. Deux exceptions : un compte exact (sans échantillonnage, idée 10), ou une valeur propre au processus relevée périodiquement, comme le nombre de goroutines (pp. 79-80). Pour l'envoi, le choix par défaut le plus sensé est l'exportateur OTLP gRPC, vers votre fournisseur ou vers un Collector (p. 81).

Mon contrôle du code Go

Hors livre pour l'API actuelle. Le code d'exemple (ch. 7) date de l'API de 2021 et a vieilli. metric.Must(meter).NewInt64Measure(...) (p. 80) et otlpgrpc.NewClient(...) (p. 81) figurent bien dans le livre. Or le SDK de métriques de Go n'est stable que depuis 2023 (version 1.19, d'après les notes de version du projet). Les exporteurs OTLP vivent désormais dans des modules par signal, comme otlptracegrpc. Deux extraits semblent faux même pour 2021 : Start renvoie deux valeurs et le livre n'en affecte qu'une (p. 78), NewTracerProvider n'en renvoie qu'une et le livre en lit deux (p. 81). Gardez les concepts, recodez les appels d'après la documentation d'OTel.

5La boucle d'analyse : comparer la zone anormale au reste

Une alerte tombe, aucune explication connue. Que fait-on ? La méthode classique est de partir de ce qu'on sait : on consulte un runbook (une procédure écrite pour une panne connue), on parcourt des dashboards et on devine une cause. Or « ce temps passé à créer des runbooks et des tableaux de bord est largement perdu, parce que les systèmes modernes tombent rarement en panne deux fois exactement de la même façon » (p. 84).

Les auteurs proposent plutôt de déboguer à partir des principes premiers : ne rien présumer, partir de ce qui est prouvé, former une hypothèse, la valider ou l'invalider par l'observation.

La méthode s'appelle la boucle d'analyse centrale, en quatre étapes (p. 87) :

  1. partir de la vue d'ensemble de ce qui a déclenché l'enquête, l'alerte ou la plainte.
  2. vérifier que ce qu'on croit savoir est vrai : un changement notable de performance a-t-il lieu ? Il se voit comme un décrochage sur une courbe.
  3. chercher les dimensions qui expliquent le changement : regarder des exemples de la zone anormale (la partie du graphique où la courbe décroche), regrouper par dimension, filtrer sur une valeur.
  4. se demander si l'on en sait assez, sinon filtrer la vue pour isoler la zone et retourner à l'étape 3.

Le calcul peut s'automatiser en comparant la zone anormale à la ligne de base, le reste du système. Pour chaque dimension, on compare sa distribution dedans et dehors, puis on trie par écart (p. 88). Exemple chiffré (p. 88) :

DimensionDans la zone anormaleDans la ligne de baseÉcart (points, mon calcul)
request.endpoint vaut batch100 %20 %80
handler_route vaut /1/markers/100 %10 %90
request.header.user_agent renseigné97 %100 %-3

Les deux premières lignes sautent aux yeux, la troisième ne dit rien. Voilà le tri qu'une machine fait en brute-force, sans rien savoir de l'application. L'outil de Honeycomb, BubbleUp, le fait sur une heatmap (un graphique en cases colorées : le temps en abscisse, la durée en ordonnée, la couleur compte les requêtes) : on entoure la zone, et il classe les dimensions. Dans un incident réel de l'éditeur, il remonte 63 dimensions intéressantes. Parmi elles, global.availability_zone = us-east-1a apparaît dans 98 % des événements de la zone contre 17 % de la base (pp. 89-90). La cause réelle était un problème réseau dans cette zone chez le fournisseur de cloud, confirmé par des clients touchés (p. 90). Aucun code n'était en cause, ce que seule une comparaison large pouvait montrer.

La boucle ne se fait qu'avec les briques de base de l'observabilité (p. 90), donc les événements larges de l'idée 2. Avec des logs, il faut avoir ajouté des identifiants de requête et beaucoup de post-traitement (pp. 90-91).

Les auteurs écartent aussi l'AIOps, terme que Gartner a lancé en 2017 pour désigner l'IA appliquée aux opérations (pp. 91-92). L'argument : l'IA doit tracer la frontière entre « anomalie » et « ligne de base ». Dans un environnement qui change tout le temps, elle la serre trop et signale des variations normales (faux positifs). Ou elle la desserre trop et prend des anomalies pour du normal. Chaque déploiement est d'ailleurs lui-même une anomalie. Les auteurs concluent : « n'importe quel ordinateur peut calculer et détecter un pic, mais seul un humain peut donner un sens à ce pic » (p. 92).

Mon analogie SQL

Le geste ressemble à un GROUP BY en SQL, répété sur toutes les colonnes et comparé à la population entière. La démonstration repose sur BubbleUp et sur un incident choisi par l'éditeur. Les auteurs avouent aussi un piège : avoir des événements larges ne suffit pas, on peut les consommer comme des logs ou des dashboards et rester dans le débogage « à partir de ce qu'on connaît » (p. 85).

6Qui regarde quoi : le monitoring pour les systèmes, l'observabilité pour le code

La méthode pose une question pratique : jette-t-on les métriques ? Non, avec une règle de partage : « L'observabilité est la plus adaptée pour comprendre les problèmes au niveau de l'application, et le monitoring pour comprendre les problèmes au niveau du système » (p. 95). Le logiciel est « le code que vous développez activement et qui fait tourner un service de production apportant de la valeur à vos clients » (p. 97). Le système regroupe tout le reste : infrastructure et environnement d'exécution (p. 98).

Le tableau 9-1 (p. 98) compare les deux sur neuf facteurs. J'en retiens trois :

  • la fréquence de changement : le système change peu (mises à jour de paquets mensuelles), le logiciel tous les jours, avec les commits.
  • la prévisibilité : le système est prévisible, le logiciel l'est peu, parce que de nouvelles fonctions arrivent sans cesse.
  • le point de vue : l'équipe qui exploite se demande si le système est sain, alors que le client veut que chaque requête obtienne ses ressources à temps.

La dernière ligne du tableau donne la méthode : le système se comprend par le monitoring, le logiciel par l'observabilité. Un cas pour un développeur PHP ou Go : le processeur du serveur est à 90 %, c'est du système, une métrique et une alerte suffisent. Un appel curl vers l'API de paiement prend 8 secondes pour un seul client, c'est du logiciel : votre code et vos requêtes, qu'aucun graphique de processeur ne montre. Il faut l'événement de cette requête, avec l'identifiant du client (mon exemple, hors livre).

Les métriques agrégées conviennent donc à l'infrastructure (p. 99). La frontière suit la responsabilité opérationnelle, pas l'hébergement. Un composant acheté comme service (Kafka, Memcached) n'est pas votre infrastructure : son fournisseur l'exploite. Celui que votre équipe installe, configure et dépanne l'est (p. 97).

Plus on externalise, moins on a de monitoring d'infrastructure à faire. Sur un PaaS (plateforme qui gère les serveurs à votre place), peu d'alertes d'infrastructure suffisent si le fournisseur est robuste (p. 100). Deux garde-fous restent : surveiller les métriques de haut niveau (processeur, mémoire, disque) comme alarmes précoces, et voir métriques système et données applicatives côte à côte (p. 101).

Dans la vraie vie

Hors livre. La séparation ops (processeur, mémoire, disque) contre dev (logs et traces applicatifs) existe déjà dans la plupart des équipes. Le livre en fait une règle fondée sur la fréquence de changement. Seuls Honeycomb et Lightstep sont cités pour afficher les deux familles côte à côte (p. 101).

7Démarrer par le plus gros point de douleur, et instrumenter avec la fonctionnalité

Comment adopter ces idées sans projet de six mois ? Le livre donne six conseils (ch. 10). Le premier est de rejoindre une communauté de pairs, par exemple le groupe Slack de Honeycomb (p. 108). Le plus contre-intuitif est le deuxième : « Contre toute intuition, pour l'observabilité, commencer petit est l'une des plus grosses erreurs que nous voyons souvent » (p. 109). Sinon l'équipe subit tout le travail sans en tirer aucun bénéfice (p. 109). Le conseil : « Ne choisissez pas un problème facile pour commencer » (p. 109). Prenez le service instable qui réveille l'astreinte, la congestion de base inexpliquée ou la charge d'un utilisateur inconnu, puis diffusez le succès (p. 109). Pour l'export qui tourne dans le vide du début de cette fiche, on commence donc par lui (mon exemple). On pose le span de l'idée 4 avec le client et le nombre de lignes, au lieu d'instrumenter la page d'accueil qui va bien.

Le troisième, acheter plutôt que construire, est discuté dans l'encadré. Les trois derniers se résument ainsi :

  • itérer par l'astreinte : l'astreinte (on-call) est la garde à tour de rôle. Quand un ingénieur est appelé, sa première action est d'instrumenter la zone en cause avec le nouvel outil, puis de s'en servir. Après la deuxième ou troisième fois, les gens comprennent que c'est plus simple (p. 112).
  • réutiliser l'existant : dupliquer le flux de logs vers le nouvel outil, ajouter un identifiant unique aux logs structurés, ou recréer les meilleurs dashboards comme requêtes (pp. 112-113).
  • prévoir le dernier effort, le plus dur : l'itération par l'astreinte mène à environ la moitié ou les deux tiers du travail. Les parties rarement touchées de la pile restent sans douleur, donc sans moteur : il faut un plan d'achèvement (p. 114).

Le livre prolonge l'idée en trois points (ch. 11). Une question à poser sur chaque changement : « Comment saurai-je si ce changement fonctionne comme prévu ? » (p. 121). Une limite : « L'observabilité n'est pas faite pour déboguer la logique de votre code » (p. 119). Elle montre dans quelle région du système regarder, puis un débogueur prend le relais, comme le microscope après le télescope (p. 120). Un geste : router pendant 30 minutes à 1 heure les alertes de production vers l'ingénieur qui vient de fusionner son code. Cela resserre la boucle entre la livraison et ses conséquences (p. 122).

Où le vendeur parle

Mon commentaire. C'est ici que le biais d'éditeur se voit le mieux. Le livre conseille d'acheter plutôt que de construire (ch. 10). Les trois candidats open source, Prometheus (métriques, idée 11), ELK (logs : Elasticsearch, Logstash, Kibana) et Jaeger (traces), offrent chacun un morceau, mais « aucun ne propose une solution complète capable de répondre aux exigences fonctionnelles de l'observabilité décrites dans ce livre » (p. 110). Plus absolu : « Aujourd'hui, aucun outil open source n'offre les capacités d'observabilité dans une solution clé en main » (p. 111). Le conseil de tester les outils sur des problèmes durs (p. 111) reste valable quel que soit le vendeur.

8Alerter sur le symptôme utilisateur, pas sur la cause possible

Prenons une alerte qui sonne dès que le processeur dépasse 80 %. Elle déclenche surtout de faux positifs, donc de la fatigue d'alerte. Après chaque incident on en ajoute d'autres. C'est une normalisation de la déviance, un terme tiré de l'enquête sur l'accident de la navette Challenger : on s'habitue au bruit jusqu'à ignorer le vrai signal (p. 128). Le verdict : « Les alertes sur des conditions qui ne sont pas directement liées à l'expérience client deviendront vite un simple bruit de fond » (p. 130). Les pannes remédiées automatiquement ne devraient pas déclencher d'alarme : on les examine en heures ouvrées (p. 130).

Les auteurs donnent deux critères pour une bonne alerte. D'abord, « ce doit être un indicateur fiable que l'expérience utilisateur de votre service est dégradée » (p. 131). Ensuite, elle doit être soluble : il existe une façon systématique de déboguer et d'agir (p. 131). Sinon, il faut la supprimer (p. 138).

L'outil pour y arriver est le SLO (service level objective), un objectif interne de disponibilité, plus strict que l'engagement envoyé au client. Il se mesure avec un SLI (service level indicator), la règle qui classe une requête comme bonne ou mauvaise. Un SLI par événement vaut mieux qu'un SLI par fenêtre de temps : chaque requête est jugée seule, au lieu d'une minute entière classée bonne ou mauvaise (p. 133). Exemple : pour les requêtes sur /home, une réponse réussie en moins de 100 ms est bonne. Plus lente, elle compte comme une erreur, même avec un code de succès (p. 133). Chaque événement mauvais consomme le budget d'erreur, la part de requêtes mauvaises que l'objectif tolère.

L'alerte SLO dit qu'il y a de la douleur, pas pourquoi. C'est là que l'observabilité reprend la main : quand tout peut tomber en panne à tout moment, il faut des données pour tout (p. 134).

Le cas d'école est celui d'un service de Honeycomb, Shepherd, qui reçoit les données des clients (pp. 135-137). Fin 2019, un brownout (une panne partielle, où seule une fraction des requêtes échoue) de 1,5 % dure 20 minutes. Selon la légende de la fig. 12-2, les événements mauvais surviennent à intervalles assez réguliers jusqu'à la correction. Une alerte SLO réveille l'ingénieur d'astreinte à 1 h 29. Voyant le service en marche, il conclut à un à-coup. Une quatrième alerte arrive à 9 h 55 : l'incident est déclaré, puis résolu à 10 h 32. La cause est une fuite mémoire après un déploiement : toutes les machines tombent ensemble, faute de mémoire, puis redémarrent.

Les sondes, ces tests périodiques qui demandent au service s'il répond, n'ont rien vu. Elles exigent plusieurs échecs consécutifs, et sur 50 sondes une seule échoue puis se rétablit (p. 136). À la fin, le budget affiche -566 % pour une cible de 99,995 % (fig. 12-2, p. 137). Quelques incidents ont montré que le SLO alertait bien avant les alertes classiques. L'équipe a supprimé les alertes sur le pourcentage d'erreurs des cinq dernières minutes, sur le nombre d'erreurs et sur le comportement système. Elle fait des alertes SLO sa première ligne de défense, car les SLO ont prouvé leur capacité à détecter les brownouts (p. 137).

Le récit est flou sur un point. L'équipe ne faisait pas confiance à ses SLO, et leurs alertes allaient dans un canal de faible priorité (p. 135). Pourtant l'une d'elles a réveillé l'astreinte, sans qu'on sache lesquelles réveillaient. La leçon reste claire : l'ingénieur de 1 h 29 a ignoré une bonne alerte, et une culture qui ne suit pas annule l'outil.

Mes calculs sur Shepherd

J'ai refait deux calculs de ce cas. Le premier colle. La cible de 99,995 % laisse un budget de 0,005 %. Un budget à -566 % veut dire 6,66 fois ce budget consommé, soit une conformité d'environ 99,9667 %. La légende donne 99,97 %, l'arrondi exact. Le second ne colle pas : le livre écrit que le brownout de 1,5 % pendant 20 minutes « a brûlé la plus grande partie du budget de 30 jours » (p. 135). Comptons. Vingt minutes font 0,046 % de 43 200 minutes. À 1,5 %, la panne touche 0,046 % × 1,5 % = 0,0007 % des requêtes, si le trafic est uniforme. Cela pèse 0,0007 / 0,005 = 14 % du budget. Il faudrait environ 16 heures à 1,5 % pour atteindre -566 %. Les alertes s'étalent de 1 h 29 à 9 h 55 : l'incident dure plus de 20 minutes, sans total donné.

9Les burn alerts : prévoir l'épuisement du budget plutôt qu'attendre le zéro

Un SLO à 99,9 % tolère 8 h 45 min 57 s d'indisponibilité par an, soit 43 min 50 s par mois (pp. 139-140). Le mois est ici un douzième d'année. En mode événementiel, le budget est le nombre de requêtes de la période multiplié par (1 moins la cible). Le budget restant est ce budget moins les requêtes en échec (p. 140). Plus la cible est haute, moins on a de temps pour réagir :

Cible du SLOBudget sur 30 jours (minutes, mon calcul)Budget brûlé par une minute mauvaise (mon calcul)
99 %4320,23 %
99,9 %43,22,3 %
99,95 %21,64,6 %
99,99 %4,3223 %

La prévision (voir plus bas) fonctionne bien jusqu'à 99,95 %. Cela fait 21 min 54 s de panne totale tolérée par mois moyen. Au-delà, elle sert plutôt à rapporter qu'à prévenir (p. 140), et le livre renvoie au Site Reliability Workbook (p. 141).

La fenêtre de mesure est glissante, c'est-à-dire les 30 derniers jours à chaque instant : « Pour la plupart des SLO, une fenêtre de 30 jours est la période la plus pragmatique » (p. 141). Une fenêtre fixe, calendaire, remet le budget à zéro d'un coup le 1er du mois et manque de données juste après (p. 142). La glissante oublie les fautes peu à peu, comme la mémoire humaine : « le meilleur SLO est celui qui tient compte des émotions humaines, de la mémoire et du biais de récence » (p. 142).

Deux façons d'alerter avant le zéro. Un seuil non nul (par exemple : budget restant sous 30 %) est grossier : « il ne fait que déplacer les poteaux de but en fixant un autre seuil de vide » (p. 143). Ces 30 % peuvent durer dix jours ou une heure : le seuil ne dit pas à quelle vitesse on y va (mon explication).

L'alerte prédictive projette l'avenir avec deux fenêtres (pp. 143-144). La fenêtre de prévision (lookahead) dit jusqu'où l'on regarde. La fenêtre de référence (baseline) dit quelle part du passé récent sert à prévoir. Ne la confondez pas avec la ligne de base de l'idée 5. Tout ne justifie pas de réveiller quelqu'un. Une dérive vers 99,88 % en un mois attend le jour ouvré. Un SLO en route vers 98 % en une heure appelle l'astreinte (p. 144).

Règle empirique des auteurs : « une fenêtre de référence donnée peut prédire linéairement vers l'avant d'un facteur quatre au plus sans avoir à compenser la saisonnalité » (p. 145). La saisonnalité désigne les cycles du jour, de la semaine ou de l'année : creux de nuit, pics de soldes (p. 145). D'où l'heuristique : « une alarme de 24 heures repose sur les 6 dernières heures de données, une alarme de 4 heures repose sur la dernière heure de données » (p. 152). Trop court, l'alerte devient « flappy » (instable). Trop long, on voit la panne trop tard (pp. 145, 151).

Voici l'exemple du livre pour une cible de 99 % sur 30 jours (pp. 147-148). Il compte en unités : la brique de base du calcul, ici un point de mesure par minute, bon ou mauvais (p. 147). Un mois typique compte 43 800 unités, le budget d'erreur est 1 %, soit 438 unités en échec. Sur les dernières 24 heures, le service a vu 1 440 unités dont 50 en échec. Sur les 6 dernières heures, il en a vu 50 dont 25 en échec. Ce 50 est une incohérence : 6 heures font 360 unités, comme dans le premier exemple (p. 147). Le second ressemble à une retouche du premier. Je garde ses chiffres pour suivre le calcul.

Il s'agit de prévoir les 24 heures suivantes, soit quatre fois la fenêtre de 6 heures. L'extrapolation linéaire prolonge le nombre d'échecs : 25 × 4 = 100, très en dessous de 438. L'extrapolation proportionnelle, plus réaliste, prolonge le taux d'échec : 25 unités sur 50, soit 50 %. Sur 24 heures, 50 % de 1 440 unités font 720 échecs, bien au-dessus de 438. L'alerte sonne, et le budget serait épuisé en environ une demi-journée.

Le livre ajoute un exemple « sensible au contexte » (pp. 148-151), qui garde le compte de toute la fenêtre de 30 jours. On prévoit sur quatre jours à partir d'un jour de référence, donc on ne regarde en arrière que 26 jours : 26 + 4 = 30 (p. 149). Ces 26 jours comptent 285 échecs, le dernier jour 130. Il reste 438 - 285 = 153 échecs permis. La projection répète quatre fois le dernier jour : 285 + 4 × 130 = 805, contre 438 autorisés. L'épuisement tomberait en 153 / 130, environ 1,2 jour, ce que dit aussi la figure 13-7 (moins de deux jours, p. 151).

Ce calcul sensible au contexte coûte cher : chez Honeycomb, de petits jeux de données SLO ont soudain coûté plus de 5 000 $ par jour en AWS Lambda (p. 148).

Un dernier argument vaut pour les SLO par événement plutôt que par minute. Prenons un SLO à quatre neuf dont chaque minute entière est classée bonne ou mauvaise. Une seule minute mauvaise suffit à « brûler 25 % de votre budget d'erreur mensuel de 4,32 minutes » (p. 155). Mon calcul donne plutôt 23 % (1 / 4,32). Plus généralement, « un brownout de 1 % sur un système fiable à 99,99 % revient à mesurer une panne totale de 100 % sur un système fiable à 99 % » (p. 155). Dans les deux cas, le taux d'échec vaut 100 fois le taux toléré : 1 % pour 0,01 %, et 100 % pour 1 %. Le livre en tire la conséquence : plus de sept heures pour remédier avant d'épuiser le budget mensuel (p. 155). Les brownouts partiels sont plus fréquents que les pannes totales, et un SLO événementiel ne retire du budget que la part réellement mauvaise.

Mes recomptages

J'ai refait tous les calculs du chapitre, deux fois chacun. J'y trouve cinq écarts d'arithmétique, en comptant le 50 au lieu de 360, le 25 % et le 14 % de l'idée 8. Aucun ne casse la conclusion, mais ne reprenez pas ces nombres pour régler un seuil.

  • Le livre écrit « 43 800 unités / 30 jours, soit 1 440 unités » (pp. 147-148). Or 43 800 / 30 = 1 460, et 1 440 est le nombre de minutes d'un jour. Le calcul sensible au contexte utilise bien 1 460.
  • Le second exemple projette « 105 échecs au total » (p. 148). Le 105 semble venir du premier exemple (100 + 5). Avec les chiffres du second, 100 + 25 font 125, toujours très en dessous de 438.
  • Le « plus proche de 30 erreurs » (p. 148) reste flou. Le calcul direct donne 40 : une erreur en 6 heures, fois 4 pour 24 heures, fois 10 pour un dixième du trafic.

Les catégories « court terme » et « sensible au contexte » sont celles de Honeycomb, pas un standard. Le Workbook de Google, auquel le livre renvoie (p. 141), alerte plutôt sur le burn rate. C'est la vitesse de consommation du budget, mesurée sur une fenêtre longue et une courte.

10Échantillonner sans fausser les comptes

À grande échelle, garder tous les événements coûte plus qu'il ne rapporte : « La réalité de la plupart des applications est que beaucoup de leurs événements sont quasi identiques et réussis » (p. 208). L'échantillonnage consiste à n'en garder qu'une partie. Contrairement à une métrique pré-agrégée, l'événement gardé conserve toute sa cardinalité (p. 208). Quatre familles de stratégies (pp. 209-211) :

  • probabilité constante : on garde 1 requête sur 10, par exemple. Inefficace si l'on tient plus aux erreurs qu'aux succès, ou si certains clients envoient des ordres de grandeur de trafic en plus (p. 209).
  • selon le volume récent : on garde plus d'événements quand le trafic récent est faible, moins quand une rafale menace de submerger l'outil d'analyse (p. 210).
  • selon le contenu : on choisit un taux par clé, une combinaison de champs comme le code HTTP, le type de requête ou le client payant. Cela marche si l'ensemble des clés possibles est petit et stable. Il faut vérifier l'hypothèse (les erreurs peuvent redevenir majoritaires, p. 211).
  • clé plus historique : le taux dépend du volume vu pour une combinaison de champs dans les 30 dernières secondes. Plus une combinaison est fréquente, moins elle intéresse (p. 211).

Le point central est de ne pas fausser les comptes. Si l'on garde 1 requête sur 1 000, chaque événement conservé en représente 1 000 : sur 100 000 requêtes, on reçoit environ 100 événements (p. 209). La règle : « pour calculer un compte, votre système doit additionner le nombre d'événements représentés, pas seulement le nombre d'événements collectés » (p. 210). Avec un taux constant, la médiane et le p99 n'ont pas besoin d'être ajustés, car l'échantillonnage ne les déforme pas (p. 209).

Avec un taux variable, il faut pondérer. Prenons trois paires (valeur, taux) : {1, 5}, {3, 2}, {7, 9}. La valeur est par exemple une latence, et un taux de 5 veut dire qu'un événement gardé en représente cinq. La médiane naïve des trois valeurs est 3. En développant (cinq fois 1, deux fois 3, neuf fois 7), on obtient 16 valeurs. Leur médiane est 7 (p. 210, recomptée par moi).

D'où une pratique : enregistrer le taux dans l'événement lui-même. Sinon le taux peut changer plus tard, et le serveur ne saurait plus ce que représente chaque événement (p. 213). Compter les erreurs devient alors la somme des sampleRate des événements en erreur (p. 214).

Pour les traces, deux moments de décision (pp. 211-212). L'échantillonnage en tête décide à la tête de la trace, donc au début, et propage la décision aux enfants dans un en-tête HTTP. Il convient aux propriétés connues dès l'entrée, comme le point d'accès ou le client. L'échantillonnage en queue décide à la fin, pour des propriétés connues seulement alors (le statut, la latence) : c'est la seule façon de décider correctement sur ces valeurs (p. 212). Il faut alors tamponner tous les spans avant de trancher, ce qui est coûteux et irréalisable en pratique depuis le code instrumenté lui-même : on le fait côté collecteur (p. 212).

Autre piège : si chaque service échantillonne indépendamment, on garde rarement tous les spans d'une même trace. L'échantillonnage cohérent corrige cela. Un nombre aléatoire (Sampling-ID) est tiré une fois à la racine, puis propagé, et chaque service le compare à son propre taux. La garantie a une condition : « l'échantillonnage cohérent garantit que, lorsque le taux d'échantillonnage est constant, les traces sont soit gardées, soit écartées en entier » (p. 215). Si les taux diffèrent, la trace peut perdre des maillons. Prenons Redis, échantillonné à 1 sur 1 000 sous un parent gardé à 1 sur 10. L'enfant gardé a toujours son parent, mais le parent gardé perd ses enfants écartés (p. 215).

Hors livre, d'après la spécification d'OpenTelemetry, les échantillonneurs courants dérivent la décision de l'identifiant de trace. Ils utilisent un hachage ou des bits aléatoires, au lieu de propager un nombre à part.

Pour viser un nombre d'événements par seconde, le service compte les requêtes de la minute précédente et en déduit son taux. Avec 5 événements par seconde voulus, soit 300 par minute, 30 000 requêtes par minute donnent un taux de 1 sur 100 (mon calcul).

Les bugs du Go d'exemple

Mon relevé, code non compilé. Trois défauts.

  • Le r masqué (pp. 216, 217 et 222). Le code déclare var r float64, puis écrit if r, err := floatFromHexBytes(...); err != nil { r = rand.Float64() }. Le := crée un nouveau r, local au if. Le r extérieur reste à 0, et c'est lui qu'on compare au seuil.
  • Le seuil entier (p. 216). Ici sampleRate est un *int (flag.Int, 1000 par défaut). 1.0 / *sampleRate est donc une division d'entiers, qui vaut 0. Le test r < 0 ne garderait jamais rien, et comparer un float64 à un int ne compile pas en Go.
  • Le taux tronqué (p. 217). Là, sampleRate est un float64, le seuil est juste. Mais newSampleRate = *requestsInPastMinute / (60 * *targetEventsPerSec) divise deux entiers. Avec 500 requêtes par minute (mon exemple), 500 / 300 donne 1 au lieu de 1,67 : on garde trop d'événements.

Avec le r à 0, la page 217 garde donc tout. Retenez les concepts : le taux enregistré dans l'événement, l'identifiant de décision propagé, le compteur par clé remis à zéro à chaque intervalle. Ne copiez pas le code. OpenTelemetry implémente de plus en plus ces stratégies (p. 223).

11Un processus par requête casse le modèle de Prometheus : la perle PHP

Le seul cas PHP du livre se cache dans un chapitre invité sur la télémétrie chez Slack (pp. 225-240). Prometheus est un outil de métriques qui fonctionne en pull : il interroge à intervalles réguliers un point d'accès que chaque programme expose avec ses compteurs. Cela suppose un programme qui vit longtemps et garde ses compteurs en mémoire.

Le backend de Slack tourne en PHP puis en Hack, le dialecte de PHP de Meta, avec un processus par requête : chaque requête démarre un processus qui disparaît ensuite : « Comme PHP/Hack utilise un modèle d'un processus par requête, le modèle pull de Prometheus ne marcherait pas, car il n'a pas le contexte de processus » (p. 235). Le serveur Prometheus viendrait interroger un processus déjà disparu, et aucun compteur ne survit d'une requête à l'autre.

La solution de Slack : « Slack utilise une bibliothèque Prometheus maison pour émettre des métriques par requête vers un démon local écrit en Go » (p. 235). Un démon est un programme qui tourne en arrière-plan. Celui-ci agrège les métriques sur une fenêtre de temps et expose le point d'accès que Prometheus vient lire (fig. 18-3).

Le même chapitre décrit un pipeline de télémétrie : un chemin entre l'application et l'outil d'analyse, avec récepteur, tampon, processeur et exportateur, qui route, filtre et enrichit les données (p. 226). Développer en interne la base de ce pipeline n'a pas de sens économique, vu les nombreuses alternatives open source modulaires (p. 239). Les exploiter coûte quand même. Ce chapitre invité fait ainsi confiance à l'open source pour transporter les données, là où l'idée 7 le jugeait insuffisant pour les analyser.

Mon rapprochement avec statsd

Ce démon ressemble à un agrégateur local comme statsd, que le livre ne cite pas. La question vaut pour tout développeur PHP : le modèle de collecte suppose-t-il un processus durable ? Le client Prometheus courant en PHP, promphp/prometheus_client_php, garde ses compteurs dans Redis ou APCu (d'après son README). Le livre ne parle d'aucun framework PHP (vérifié par recherche sur tout le texte).

Mon avis, honnêtement

Deux questions comptaient pour moi. L'idée centrale, expliquer une panne qu'on n'avait pas prévue, est-elle juste ? Oui, et c'est ce qui fait l'intérêt du livre. Peut-on l'appliquer à une application PHP ou Go sans équipe dédiée ? En partie : trois chapitres (9, 10 et 12) y arrivent, le reste suppose plus de moyens. Je l'ai lu comme on lit un vendeur sincère : on prend l'idée, on vérifie les chiffres, on laisse le catalogue.

Sa force : une thèse claire, tenue d'un bout à l'autre, et deux cas narrés qui marquent. Le brownout invisible aux sondes réveille quelqu'un à 1 h 29, et la zone us-east-1a saute aux yeux dès la première comparaison. La méthode du livre donne un geste qu'on refait seul, sans outil d'éditeur : comparer la zone anormale au reste, dimension par dimension (p. 88).

Deux réserves. Les auteurs sont vendeurs et l'avouent, et le multi-fenêtre de Google n'est pas décrit : testez les outils vous-même et lisez le Workbook pour des cibles strictes. Ensuite, le code Go est pédagogique et daté, et les calculs de SLO contiennent cinq écarts d'arithmétique (idée 9) : réécrivez le code d'après la documentation actuelle et refaites chaque calcul avant de fixer un seuil.

Mon conseil : lisez les chapitres 1, 5 et 8 pour l'idée. Ajoutez le chapitre 9 pour le partage entre monitoring et observabilité, et le chapitre 12 pour l'alerte sur symptôme. Lisez le chapitre 17 si vous traitez du volume. Parcourez le chapitre 13 en refaisant les calculs, lisez le chapitre 10 en sachant qui parle, et sautez les chapitres d'organisation. Seul ou à deux, vous traduirez le reste vous-même : le livre ne parle jamais d'équipe réduite.

Odilon

Toujours valable en 2026 ?

Les idées durent : événement large, cardinalité, boucle d'analyse, séparation systèmes et logiciel, alerte sur le symptôme, taux enregistré dans l'événement. Plusieurs points vieillissent ou sont à surveiller :

À qui s'adresse-t-il ?

Lisez-le si

  • vous écrivez des services web en PHP ou en Go qui parlent à d'autres services et vous déboguez avec des logs et des graphiques.
  • vous voulez comprendre pourquoi une alerte sur le processeur réveille pour rien et ce que veut dire « alerter sur le symptôme utilisateur ».
  • vous devez choisir un fournisseur d'observabilité ou instrumenter avec OpenTelemetry, et vous voulez connaître les arguments du camp qui vend.
  • vous avez au moins quelques années de métier, un service en production et déjà vu un incident que vos graphiques n'expliquaient pas.

Passez si

  • vous cherchez du code à copier : tout est en Go, daté, et en partie non compilable.
  • vous êtes seul ou presque et attendez un guide pour petite équipe : le livre n'en dit rien.
  • vous attendez une comparaison neutre d'outils : les auteurs vendent leur produit.

Pour aller plus loin

Fiche fondée sur : chapitres 1, 2, 5 à 13 et 17 lus en entier. Chapitres 14, 18 et 21 survolés. Chapitres 3, 4, 15, 16, 19, 20 et 22 triés mais non lus en entier. Les encadrés « hors livre », les calculs notés « mon calcul » et les vérifications d'absence sont de moi, pas du livre.

Commentaires (0)

Voir toute la bibliothèque

D'autres fiches arrivent : un livre à la fois, la substantifique moelle seulement.