Aller au contenu

TechniquePratique15 min de lecture (+2 technique)

Entities, Profiles, Injector : trois briques suivies le temps d'un incident

Trois briques d'OpenTelemetry souvent mal expliquées, suivies pendant un même incident : une façon de dire quel objet émet les données (Entities), un quatrième signal qui descend jusqu'au code (Profiles) et un mécanisme qui installe les sondes sans toucher aux applications (Injector). Pour chacune : le problème, le fonctionnement, la maturité.

Pour : ingénieurs et SRE · architectes · managers d’équipePrérequis : Savoir ce que sont une trace, un span et le Collector. L'article OpenTelemetry de bout en bout les présente.

Mode de lecture

Trois noms reviennent dans les annonces d’OpenTelemetry : Entities, Profiles et Injector. Je les vois souvent cités ensemble et rarement expliqués simplement. Plutôt que de les présenter un par un dans l’abstrait, je les suis pendant un même incident, un mardi après-midi. Pour chacun, on verra le problème qu’il règle, comment il fonctionne et où il en est.

On entend souvent qu’Entities serait un nouveau nom pour des attributs qui existent déjà, que Profiles serait le profilage de code qu’on pratique depuis des années et que l’Injector serait un agent de plus à déployer. Aucune de ces trois idées n’est juste.

Entities est un modèle de données : une façon de dire, sans ambiguïté, de quel objet viennent les signaux (un service, un pod, une machine) et ce qui fait son identité. Profiles est un nouveau signal, le quatrième après les traces, les métriques et les logs, qu’on peut relier aux trois autres. L’Injector est un mécanisme de déploiement : une petite bibliothèque qui branche les agents existants dans les applications au moment où elles démarrent, sans qu’on y touche.

À mon avis, la plupart des malentendus viennent de là : on compare des choses qui ne se situent pas au même étage.

Le scénario est illustratif. Une boutique en ligne repose sur plusieurs services, des programmes qui rendent chacun une fonction précise. Le service checkout encaisse les commandes.

À 14 h 02, la version 4.2.0 de checkout part en production. Elle tourne dans un nouveau pod (dans Kubernetes, l’enveloppe qui fait tourner un ou plusieurs conteneurs). L’outil qui stocke les données croit voir un nouveau service : l’historique est coupé en deux. C’est le terrain d’Entities.

À 14 h 10, le temps de réponse grimpe. La trace montre qu’une étape de checkout est lente, sans dire quel morceau de code la ralentit. C’est le terrain de Profiles.

À 14 h 25, la piste mène à pricing, le service qui calcule les prix. C’est un programme Java installé sur une machine virtuelle, en dehors de Kubernetes. Personne ne l’a jamais instrumenté. C’est le terrain de l’Injector.

14 h 02. Le déploiement a changé la version et le pod. Est-ce encore le même checkout ?

Pensez à une carte d’identité. Votre nom et votre date de naissance vous identifient : ils ne changent pas. Votre adresse ou votre coupe de cheveux vous décrivent : elles peuvent changer sans que vous deveniez quelqu’un d’autre. Une administration qui confondrait les deux vous créerait un nouveau dossier à chaque déménagement.

C’est ce qui arrive aujourd’hui à checkout.

Le problème : la fiche ne dit pas ce qui fait l’identité

Section intitulée « Le problème : la fiche ne dit pas ce qui fait l’identité »

Chaque signal OpenTelemetry arrive avec une fiche qui décrit sa source. On l’appelle la Resource : c’est une liste d’attributs, des paires nom et valeur comme service.name = checkout. L’article OpenTelemetry de bout en bout la présente en détail.

Au moment du déploiement, la fiche de checkout change sur trois lignes : la version passe de 4.1.9 à 4.2.0 et le pod a un nouvel identifiant et un nouveau nom. Le nom du service et la machine, eux, restent les mêmes.

La Resource fonctionne, mais elle mélange plusieurs objets (un service, un pod, une machine) dans une seule liste. Rien n’y dit quels attributs font l’identité et lesquels ne font que décrire. Chaque outil de stockage devine. S’il se fie à la version ou au pod, il voit deux services différents et un simple déploiement suffit à casser la continuité des courbes.

Pour les équipes techniques : la Resource de checkout avant et après le déploiement
Attribut14 h 01, version 4.1.914 h 02, version 4.2.0
service.namecheckoutcheckout
service.version4.1.94.2.0 (modifié)
k8s.pod.uid0c1e…9b77… (modifié)
k8s.pod.namecheckout-5d9f-x2kcheckout-7c4d-m8q (modifié)
host.idi-08ab…i-08ab…

Le modèle : un type, ce qui identifie, ce qui décrit

Section intitulée « Le modèle : un type, ce qui identifie, ce qui décrit »

La spécification définit une entité comme un objet d’intérêt auquel se rattachent les signaux produits, qu’il s’agisse de traces, de métriques, de logs ou de profils. Une entité a trois parties. Un type d’abord, qui ne change pas pendant sa vie : service, pod, machine. Puis des attributs qui l’identifient et ne changent pas non plus, comme le nom et la date de naissance de la carte d’identité. Enfin des attributs qui la décrivent et peuvent évoluer, comme l’adresse.

Pour un service, c’est le nom qui identifie et la version qui décrit. Pour un pod, c’est son identifiant unique qui identifie, son nom qui décrit. À 14 h 02, le nom de checkout n’a pas bougé : c’est donc le même service, dans une nouvelle version. Le pod, lui, est bien une nouvelle entité. Les deux constats sont justes en même temps et l’outil n’a plus à deviner.

Trois choses changent en pratique. L’identité reste stable : une version ou un label change, l’objet reste le même et son historique aussi. La corrélation devient explicite : traces, métriques, logs et profils désignent le même objet, sans règle de correspondance bricolée côté stockage. L’inventaire devient vivant : des événements d’entité peuvent signaler un objet même quand il n’émet aucun signal, au moment où il apparaît, change ou disparaît.

Pour les équipes techniques : la même Resource découpée en entités
TypeIdentifie (ne change pas)Décrit (peut changer)
serviceservice.nameservice.version
k8s.podk8s.pod.uidk8s.pod.name, labels
hosthost.idhost.name, host.type
processusidentifiant du processus et heure de démarragenom de l’exécutable

Les trois premières lignes reprennent les exemples de la spécification et des conventions sémantiques. La ligne du processus est illustrative : la spécification cite ce couple comme exemple d’identité minimale, sans en faire une définition.

Le modèle de données des entités, les événements d’entité et le modèle de la Resource qui les assemble portent tous le statut Development dans la spécification. C’est le stade le plus précoce, avant toute garantie de stabilité.

Côté protocole, OTLP (le format d’échange d’OpenTelemetry, commun à tous les signaux) permet depuis sa version 1.6.0 d’ajouter à la Resource des références d’entités, elles aussi en Development. Elles disent quels attributs forment l’identité et lesquels décrivent. Les attributs eux-mêmes restent dans la Resource : un outil qui ignore les entités continue de lire la fiche comme avant.

Les relations entre entités, comme « ce pod tourne sur cette machine », sont le chantier en cours. Les événements d’entité savent déjà en transporter, mais la spécification annonce que leur modélisation sera affinée.

Le premier pas que je recommande ne demande aucun outil : pour chacune de vos Resources, notez ce qui identifie et ce qui décrit. L’exercice est utile dès maintenant, quel que soit le calendrier de la spécification.

Pour les équipes techniques : le champ dans le protocole

Les références d’entités sont portées par le champ entity_refs de la Resource, défini dans resource.proto depuis OTLP 1.6.0, au statut Development.

14 h 10. La trace montre une étape lente. Elle ne montre pas le code qui la ralentit.

Une trace ressemble au suivi d’un colis : elle dit à quel relais il a attendu, pas ce qui se passait à l’intérieur de l’entrepôt. Pour le savoir, il faudrait photographier l’entrepôt toutes les quelques millisecondes et compter sur combien de photos chaque poste de travail est occupé. C’est exactement ce que fait un profil.

Après les traces, les métriques et les logs, le profilage devient un signal OpenTelemetry à part entière. Un profil dit où un programme passe son temps, fonction par fonction. Il se construit à partir d’échantillons de la pile d’appels, c’est-à-dire de la liste des fonctions en cours d’exécution à un instant donné, chacune appelée par la précédente.

La trace découpe la requête en étapes, qu’on appelle des spans. Elle répond à la question « où, entre les étapes ? » :

POST /checkout ████████████████████████████████████████
validateCart ███
applyPricing ██████████████████████████████
saveOrder ██████

Le span applyPricing prend presque tout le temps. Pourquoi ? Le profil de ce span répond à l’autre question, « où, dans le code ? ». Chaque ligne est une fonction appelée par celle du dessus et sa largeur montre la part du temps passée dedans. Ce dessin s’appelle un flame graph, un graphe en flammes :

applyPricing ████████████████████████████████████████
loadRules ██████
computeDiscounts ██████████████████████████████████
readCache ████
matchPromo ██████████████████████████████
Pattern.compile ████████████████████████████

Dans cet exemple inventé, la version 4.2.0 a introduit la compilation d’une expression régulière (un motif de recherche dans du texte) à l’intérieur d’une boucle : elle est recompilée à chaque appel.

Ce qui relie les deux vues, c’est que chaque échantillon du profil peut porter les identifiants de la trace et du span en cours. On passe ainsi d’un span à sa pile d’appels. Le profil est rattaché à une Resource, comme les autres signaux : les mêmes attributs aujourd’hui, les mêmes entités demain, le relient au service, au pod et à la machine.

Tout commence par la capture. Un profileur eBPF échantillonne les piles d’appels de toute la machine Linux. eBPF désigne de petits programmes qui tournent dans le noyau Linux et observent sans modifier les applications. Ce profileur vient d’un don d’Elastic au projet et fonctionne comme récepteur du Collector, le relais qui reçoit, transforme et réexpédie les données.

Les piles partent ensuite en OTLP, dans un modèle construit sur pprof, un format de profils répandu venu de l’écosystème Go. Un profil pprof se convertit vers OTLP et inversement sans perte. Le Collector les achemine dans un circuit dédié, qu’il faut activer explicitement. Le stockage, enfin, affiche les graphes en flammes et compare les versions entre elles.

Le signal apporte plusieurs choses. Il donne le « pourquoi » d’une courbe de processeur ou de mémoire, au niveau du code. Il permet de sauter directement d’un span à sa pile d’appels. Son format est ouvert, donc les profils restent lisibles par un autre stockage. Il voit toute la machine, y compris le code que vous n’avez pas écrit.

Pour les équipes techniques : liens et activation dans le Collector

Les identifiants portés par chaque échantillon sont le trace_id et le span_id. Le pipeline des profils du Collector s’active par la feature gate service.profilesSupport.

Le signal est entré en alpha publique en mars 2026. L’annonce elle-même en tire les conséquences. Il ne doit pas servir pour des charges de production critiques. Son format peut encore changer de façon incompatible. Le support est à activer dans le Collector, à partir de la version 0.148.0. Enfin, à la date de l’annonce, aucun stockage prêt pour la production n’existait, même si plusieurs éditeurs y travaillent.

Pour un premier essai, je recommande un montage modeste : le profileur eBPF sur une machine de test, un Collector avec le circuit des profils activé et un stockage qui lit les profils OTLP. Vérifiez quelle version du format chaque élément prend en charge, car elle bouge encore.

14 h 25. pricing tourne sur une machine virtuelle, sans agent. Pour le voir, il faudrait le modifier.

Pour qu’une application envoie des traces, des métriques et des logs, on lui ajoute en général un agent d’auto-instrumentation : un module qui se greffe sur le programme au démarrage et mesure ce qu’il fait, sans changer son code. Encore faut-il penser à le brancher sur chaque service, dans chaque langage, sur chaque nouvelle machine, puis maintenir tout cela.

Imaginez l’accueil d’un immeuble de bureaux. Plutôt que de demander à chaque visiteur d’apporter son badge, l’hôtesse en remet un à toute personne qui passe la porte. L’Injector joue ce rôle. Il ne remplace pas les agents : il se charge de les brancher automatiquement au démarrage de chaque programme, grâce à une fonction standard de Linux appelée LD_PRELOAD.

Quand un programme démarre, Linux charge les bibliothèques dont il a besoin. LD_PRELOAD lui demande d’en charger une avant toutes les autres. On peut l’indiquer pour un seul programme ou, pour toute la machine, dans un fichier système.

La bibliothèque de l’Injector entre donc en premier. Elle reconnaît le runtime du programme, c’est-à-dire le logiciel qui exécute l’application : la machine virtuelle Java, Node.js, .NET. Elle ajoute alors la variable d’environnement que ce runtime sait lire pour charger un agent. Le runtime charge l’agent et les traces, métriques et logs partent en OTLP, comme avec un agent installé à la main.

L’Injector lui-même ne produit ni trace, ni métrique, ni log. Il décide seulement quel agent brancher sur quel programme et peut compléter les attributs de la Resource. Pour pricing, service Java, il indique à Java le chemin de l’agent Java d’OpenTelemetry. Le service n’a pas été touché : il reçoit son agent au prochain redémarrage.

Pour les équipes techniques : avant, après et variables ajoutées par runtime

À la main, service par service :

# pricing.service (systemd)
ExecStart=/usr/bin/java \
-javaagent:/opt/otel/javaagent.jar \
-jar /opt/pricing/pricing.jar

Avec l’Injector, une fois par hôte :

/etc/ld.so.preload
/usr/lib/opentelemetry/libotelinject.so
# pricing.service : inchangé

Le chemin de la bibliothèque est celui de la documentation du projet ; la configuration se trouve par défaut dans /etc/opentelemetry/injector/injector.conf. LD_PRELOAD se déclare par une variable d’environnement ou, pour tout l’hôte, dans /etc/ld.so.preload.

libotelinject.so ajoute la variable que le runtime sait lire : JAVA_TOOL_OPTIONS pour Java, NODE_OPTIONS pour Node.js, les variables CORECLR_* et DOTNET_* pour .NET, RUBYOPT pour Ruby. Pour pricing, il ajoute à JAVA_TOOL_OPTIONS le chemin de l’agent Java OpenTelemetry.

Il ne sait rien faire pour les programmes compilés en un seul bloc, qu’on appelle des binaires statiques : sans chargement de bibliothèques au démarrage, pas de préchargement possible. Beaucoup de programmes écrits en Go sont dans ce cas.

Il ne crée pas d’agent pour les langages qui n’en ont pas : il branche un agent existant. Au 10 octobre 2026, le projet prend en charge Java, Node.js, .NET et Ruby ; Python existe mais reste désactivé par défaut.

Il n’agit pas sur les programmes déjà lancés. Tout se joue au démarrage : il faut redémarrer le service pour qu’il soit pris en charge.

Il ne passe pas sur les programmes privilégiés. Pour les exécutables qui démarrent avec des droits élevés (les exécutables setuid), Linux restreint LD_PRELOAD par sécurité.

Il ne prend pas non plus la décision de sécurité à votre place. Une bibliothèque chargée dans tous les programmes d’une machine se valide avec l’équipe sécurité, comme tout ce qu’on inscrit dans le fichier de préchargement du système.

Sa place parmi les façons d’instrumenter sans modifier le code

Section intitulée « Sa place parmi les façons d’instrumenter sans modifier le code »

L’Injector n’est pas seul sur ce terrain. Dans Kubernetes, l’Operator d’OpenTelemetry fait un travail voisin : il s’appuie sur un webhook d’admission, un contrôle que Kubernetes appelle à la création de chaque pod et qui peut le modifier pour y ajouter l’agent. L’instrumentation eBPF, elle, pose des sondes dans le noyau Linux, en dehors du programme : elle voit les échanges réseau de n’importe quel langage, mais moins l’intérieur de l’application.

Les trois approches se complètent. L’Operator couvre Kubernetes, l’instrumentation eBPF couvre tous les langages au niveau des échanges réseau, l’Injector couvre les machines Linux et les machines virtuelles avec les agents complets. pricing vit sur une machine virtuelle : l’Operator ne peut rien pour lui, c’est le terrain de l’Injector.

Pour les équipes techniques : Injector, Operator et instrumentation eBPF
InjectorOperatorInstrumentation eBPF
Oùhôte ou conteneur LinuxKubernetesnœud Linux
CommentLD_PRELOAD, puis l’agent du langagewebhook d’admission qui modifie le podsondes dans le noyau, hors du processus
Langagesceux que le projet prend en chargeceux qui disposent d’un agenttous, au niveau des protocoles

Le dépôt du projet n’affiche pas de niveau de stabilité. Ses versions sont numérotées en 0.x (0.11.0 pour la dernière que j’ai relevée) : attendez-vous à ce que la liste des langages et la configuration bougent encore. Les paquets d’installation pour Debian et Red Hat (DEB et RPM) ne sont plus construits dans ce dépôt mais dans le dépôt opentelemetry-packaging.

Pour un premier essai, je recommande une machine virtuelle hors Kubernetes, avec un service Java ou Node.js, en préproduction.

Pour les équipes d’exploitation chargées de la fiabilité (les SRE), Entities garde les alertes et les tableaux de bord intacts d’un déploiement à l’autre. Profiles donne la cause au niveau du code pendant l’incident, sans avoir à le reproduire. L’Injector réduit les zones aveugles en dehors de Kubernetes.

Pour les équipes plateforme et DevOps, Entities offre un inventaire commun à tous les outils, sans règles de correspondance maison. Profiles fait passer les quatre signaux par un seul circuit et rend visible le coût en processeur de chaque service. L’Injector instrumente un parc de machines en installant un paquet, sans retoucher chaque déploiement.

Pour les développeurs, Entities permet de retrouver son service et ses versions sans connaître l’infrastructure. Profiles montre la fonction qui coûte, vue en production et pas seulement sur leur poste. L’Injector n’exige rien dans le code ni dans l’image du conteneur.

Ce sont des gains attendus, à vérifier sur votre propre périmètre. Je n’en chiffre aucun : aucune des trois briques n’est stable.

Rejouons l’après-midi. À 14 h 02, checkout 4.2.0 reste checkout et l’historique n’est pas coupé. À 14 h 10, depuis le span lent, on ouvre la fonction qui coûte. À 14 h 25, pricing est déjà instrumenté sans que personne l’ait modifié.

HeureBriqueMaturité au 10 octobre 2026
14 h 02EntitiesDevelopment
14 h 10Profilesalpha publique
14 h 25Injectorversions 0.x

Les trois se complètent : l’Injector fait entrer traces, métriques et logs sans effort, Profiles ajoute le niveau du code et Entities dit à quel objet tout cela se rapporte. Lundi matin, la tâche la plus utile est aussi la moins chère : écrire, pour vos services, ce qui les identifie et ce qui les décrit.

Consultées le 10 octobre 2026.

  • OpenTelemetry, Entity Data Model : définition d’une entité, type, attributs d’identité et de description, exemples, statut Development.
  • OpenTelemetry, Entity Events : événements d’état et de suppression, entités sans signal associé, relations.
  • OpenTelemetry, Resource Data Model : la Resource comme composition d’entités.
  • OpenTelemetry, resource.proto : champ entity_refs, statut Development, depuis la version 1.6.0.
  • OpenTelemetry, conventions sémantiques du service : entité service, identifiée par service.name.
  • OpenTelemetry, OpenTelemetry Profiles Enters Public Alpha : statut alpha, mise en garde sur la production, profiler eBPF, conversion pprof, lien avec les traces, Collector 0.148.0, état des stockages.
  • OpenTelemetry, spécification des profils : modèle de données construit sur pprof, lien vers un span, Resource partagée.
  • OpenTelemetry, The State of Profiling : activation des profils dans le Collector par service.profilesSupport.
  • OpenTelemetry, opentelemetry-injector : rôle de l’Injector, chemins, variables ajoutées par runtime, langages pris en charge, versions.
  • Linux man-pages, ld.so(8) : LD_PRELOAD, /etc/ld.so.preload, restrictions en mode d’exécution sécurisé.