OpenTelemetry : standard commun pour les traces, logs et métriques

1. Aperçu

Quand un système se divise en plusieurs services, une requête peut traverser cinq, dix processus différents. Quand une erreur se produit, vous devez savoir par où la requête est passée, où elle s’est ralentie et quel log appartient à cette requête. OpenTelemetry (OTel) est un standard ouvert qui résout précisément ce problème. Il unifie la façon de collecter les traces, les métriques et les logs, puis les envoie à n’importe quel backend.

Après avoir lu cet article, vous comprendrez les concepts clés d’OTel et pourquoi il suscite beaucoup d’intérêt. Vous saurez aussi comment intégrer OTel dans une application Spring Boot 3, lancer Collector, écrire des tests pour l’instrumentation et reconnaître quand ne pas l’utiliser.

2. Qu’est-ce qu’OpenTelemetry ?

OpenTelemetry est un projet open source appartenant à la CNCF. Il définit comment créer, décrire et transmettre les données de télémétrie. OTel n’est pas un lieu de stockage ni une interface de visualisation des données. Ces tâches sont assurées par des backends comme Jaeger, Prometheus, Grafana Tempo/Loki ou les services commerciaux.

2.1. Trois types de signaux

  • Trace : le trajet d’une requête. Chaque trace se compose de plusieurs spans, chaque span est une unité de travail, par exemple « appeler PaymentService » ou « interroger la table orders ». Un span a un temps de début, un temps de fin et des attributs.
  • Métrique : données agrégées au fil du temps, comme le nombre de requêtes par seconde, la latence p99 ou le nombre de commandes échouées.
  • Log : enregistrement d’événements discrets. OTel ajoute traceId et spanId au log, grâce à cela, vous pouvez sauter d’une ligne de log à sa trace correspondante.

La plus grande valeur réside dans l’interconnexion. Tous les trois signaux partagent un contexte commun et une description commune des ressources, par exemple service.name.

2.2. Composants principaux

Composant Rôle
API Interface permettant au code de créer des spans et des métriques, indépendante du fournisseur
SDK Implémentation de l’API : échantillonnage, batching, export
OTLP Protocole de transmission des données (gRPC ou HTTP/protobuf)
Collector Processus autonome qui reçoit, traite et transmet la télémétrie
Semantic conventions Conventions d’appellation pour les attributs comme http.request.method, db.system…
Instrumentation Bibliothèques ou agents qui génèrent automatiquement la télémétrie pour les frameworks populaires

2.3. Propagation du contexte

Pour que les spans de plusieurs services se réunissent en une trace, le contexte doit accompagner la requête. Avec HTTP, OTel utilise par défaut le standard W3C Trace Context via l’en-tête traceparent :

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Cet en-tête comporte quatre parties : version, traceId, spanId du span parent et le drapeau d’échantillonnage. Le service qui reçoit la requête lit cet en-tête et crée un span enfant appartenant à la même trace.

3. Pourquoi OpenTelemetry suscite-t-il tant d’intérêt ?

  • Indépendant du fournisseur : vous instrumentez le code une fois. Pour changer de backend, il suffit de modifier la configuration de l’exporteur ou du Collector, pas besoin de modifier le code.
  • Fusion des anciennes normes : OTel est né de la fusion d’OpenTracing et d’OpenCensus. La communauté n’a plus à choisir entre deux directions.
  • Un seul standard pour les trois signaux : autrefois, chaque type de données avait généralement son propre agent et son propre format.
  • Écosystème large : de nombreux frameworks, fournisseurs de cloud et outils d’observabilité supportent OTLP. Spring Boot 3 s’intègre aussi via Micrometer.
  • Collector flexible : vous pouvez filtrer les données sensibles, échantillonner et router vers plusieurs backends au niveau de l’infrastructure, sans modifier l’application.

4. Intégrer OpenTelemetry à Spring Boot 3

Spring Boot 3 utilise Micrometer Observation comme couche d’abstraction. Vous écrivez une observation, Micrometer génère à la fois une métrique (timer) et un span. Le bridge vers OTel prend en charge la partie export.

4.1. Dépendances

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- Pont Micrometer Tracing -> OpenTelemetry -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing-bridge-otel</artifactId>
    </dependency>
    <!-- Export des traces via OTLP -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>
    <!-- Export des métriques via OTLP -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-otlp</artifactId>
    </dependency>
    <!-- Pour les tests -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-observation-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Vous n’avez pas besoin de spécifier les versions. Spring Boot gère déjà les versions des bibliothèques Micrometer et OpenTelemetry via le BOM.

4.2. Configuration

spring:
  application:
    name: order-service

management:
  tracing:
    sampling:
      probability: 1.0   # dev : échantillonner 100% ; en production, diminuez
  otlp:
    tracing:
      endpoint: http://localhost:4318/v1/traces
    metrics:
      export:
        url: http://localhost:4318/v1/metrics
        step: 30s

logging:
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

Signification de chaque groupe de configuration :

  • spring.application.name est utilisé comme service.name. C’est l’attribut le plus important pour que le backend distingue les services.
  • management.tracing.sampling.probability détermine la proportion de traces enregistrées.
  • Les deux points de terminaison otlp pointent vers Collector via OTLP/HTTP, le port par défaut est 4318.
  • logging.pattern.level insère traceId et spanId depuis MDC dans chaque ligne de log. C’est le moyen le plus simple de lier les logs aux traces.

4.3. Instrumenter la logique métier avec l’API Observation

Les requêtes HTTP vers les contrôleurs et les appels via RestClient/WebClient créés par Spring sont déjà instrumentés automatiquement. Ce que vous devez ajouter vous-même, ce sont les étapes métier importantes :

public record Order(String id, String paymentMethod, long amount) {}
@FunctionalInterface
public interface PaymentService {
    String charge(Order order);
}
@Service
public class CheckoutService {

    private static final Logger log = LoggerFactory.getLogger(CheckoutService.class);

    private final ObservationRegistry registry;
    private final PaymentService paymentService;

    public CheckoutService(ObservationRegistry registry, PaymentService paymentService) {
        this.registry = registry;
        this.paymentService = paymentService;
    }

    public String checkout(Order order) {
        return Observation.createNotStarted("order.checkout", registry)
                .lowCardinalityKeyValue("payment.method", order.paymentMethod())
                .highCardinalityKeyValue("order.id", order.id())
                .observe(() -> {
                    log.info("Processing order {}", order.id());
                    return paymentService.charge(order);
                });
    }
}

Points à noter dans le code ci-dessus :

  • Observation.createNotStarted("order.checkout", registry) crée une observation nommée order.checkout.
  • lowCardinalityKeyValue ajoute un attribut avec peu de valeurs (CARD, CASH, WALLET). Cet attribut apparaît à la fois dans la métrique et le span.
  • highCardinalityKeyValue ajoute un attribut avec beaucoup de valeurs, comme l’identifiant de la commande. Il n’est rattaché qu’au span, sans gonfler la métrique.
  • observe(...) ouvre le span, mesure le temps, enregistre l’exception le cas échéant, puis ferme le span quand le supplier retourne.

À l’exécution, les logs ressembleront à ceci :

 INFO [order-service,4bf92f3577b34da6a3ce929d0e0e4736,a1b2c3d4e5f60718] ... Processing order A-1001

Le backend reçoit deux choses :

  • Un span order.checkout se trouvant sous le span HTTP de la requête.
  • Une métrique de timer order.checkout, répartie selon payment.method. À partir de cette métrique, vous pouvez calculer le nombre d’appels, le temps total et le temps maximal.

Important : Préférez l’API Observation plutôt que d’appeler directement io.opentelemetry.api.trace.Tracer dans le code Spring. Votre code ne dépendra pas d’une bibliothèque de tracing spécifique. Une seule mesure produit à la fois la métrique et la trace.

4.4. Solution alternative : agent Java

Si vous ne voulez pas modifier le code ou ajouter de dépendances, vous pouvez utiliser l’agent Java OpenTelemetry. L’agent instrumente automatiquement de nombreuses bibliothèques populaires comme JDBC, HTTP client et Kafka :

export OTEL_SERVICE_NAME=order-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

java -javaagent:./opentelemetry-javaagent.jar -jar order-service.jar

L’agent convient aux applications existantes ou quand l’équipe d’exploitation veut contrôler l’instrumentation de manière centralisée. Le compromis est que vous avez moins de contrôle fin, le temps de démarrage augmente, et l’agent agit comme une « boîte noire » qui manipule le bytecode.

Important : N’exécutez pas simultanément l’agent Java et le bridge Micrometer pour le même type de signal. Vous recevrez des spans dupliqués ou un contexte décalé.

5. Lancer OpenTelemetry Collector

L’application devrait envoyer les données au Collector plutôt que directement au backend. Collector est l’endroit où vous pouvez grouper par lot, filtrer et router les données. Voici une configuration minimale :

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch: {}
  attributes/strip-pii:
    actions:
      - key: customer.email
        action: delete

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [attributes/strip-pii, batch]
      exporters: [debug]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

Cette configuration comporte quatre parties :

  • receivers.otlp reçoit les données via gRPC (4317) et HTTP (4318).
  • attributes/strip-pii supprime l’attribut customer.email avant que les données ne quittent votre infrastructure.
  • batch regroupe les données avant leur envoi pour réduire le nombre de requêtes.
  • L’exporteur debug affiche les données sur la console pour que vous les vérifiez. Dans un environnement réel, remplacez-le par l’exporteur du backend que vous utilisez. Les anciennes versions du Collector utilisaient le nom logging pour cet exporteur.

Lancez Collector avec Docker :

docker run --rm -p 4317:4317 -p 4318:4318 \
  -v "$(pwd)/otel-collector.yaml:/etc/otelcol-contrib/config.yaml" \
  otel/opentelemetry-collector-contrib

Appelez l’API checkout de test, puis vérifiez la console du Collector. Vous verrez un span similaire :

Span #0
    Trace ID       : 4bf92f3577b34da6a3ce929d0e0e4736
    Parent ID      : 9a8b7c6d5e4f3a2b
    Name           : order.checkout
    Attributes:
         -> payment.method: Str(CARD)
         -> order.id: Str(A-1001)

6. Tester l’instrumentation

L’instrumentation est aussi du code, elle doit donc être testée. TestObservationRegistry vous permet de vérifier l’observation sans avoir besoin de démarrer le contexte Spring ou le Collector :

import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class CheckoutServiceTest {

    @Test
    void whenCheckout_thenObservationIsRecordedWithPaymentMethod() {
        TestObservationRegistry registry = TestObservationRegistry.create();
        PaymentService paymentService = order -> "PAID-" + order.id();
        CheckoutService service = new CheckoutService(registry, paymentService);

        String result = service.checkout(new Order("A-1001", "CARD", 250_000));

        assertThat(result).isEqualTo("PAID-A-1001");
        TestObservationRegistryAssert.assertThat(registry)
                .hasObservationWithNameEqualTo("order.checkout")
                .that()
                .hasLowCardinalityKeyValue("payment.method", "CARD")
                .hasHighCardinalityKeyValue("order.id", "A-1001")
                .hasBeenStarted()
                .hasBeenStopped();
    }
}

Ce test affirme trois choses :

  • Le résultat métier est toujours correct.
  • L’observation order.checkout a été créée, avec les bons attributs.
  • L’observation a été ouverte puis fermée. Un span oublié de fermer est une erreur très difficile à détecter en production.

7. Erreurs courantes

  • Explosion de cardinalité : placer userId, orderId ou une URL contenant des paramètres dans les tags de métrique fera exploser le nombre de métriques uniques et le coût de stockage augmentera en conséquence. Ces valeurs ne doivent être attachées qu’aux spans.
  • Échantillonnage 100% en production : le volume de données et les coûts augmentent linéairement avec le trafic. Réduisez la probabilité, ou utilisez l’échantillonnage par queue au Collector pour conserver les traces d’erreur ou les traces lentes.
  • Perte de contexte lors de l’exécution asynchrone : avec @Async, CompletableFuture ou un thread pool créé manuellement, le contexte ne suit pas automatiquement vers le nouveau thread. Vous devez envelopper l’executor avec un mécanisme de propagation du contexte, par exemple ContextExecutorService de la bibliothèque context-propagation, ou utiliser un executor géré par Spring qui est déjà configuré.
  • Fuite de données sensibles : les adresses email, les numéros de carte ou les tokens peuvent se retrouver dans les attributs ou les logs. Filtrez-les dans le code et ajoutez une couche de filtrage supplémentaire au Collector.
  • Nommage arbitraire : ne pas suivre les conventions sémantiques rend les tableaux de bord et les alertes difficiles à réutiliser entre les services.
  • Ignorer Collector : envoyer directement depuis l’application vers le backend signifie que chaque fois que vous changez de backend, vous devez redéployer tous les services.

8. Quand NE PAS l’utiliser (ou pas encore)

  • Application petite, une instance, peu de dépendances : les logs structurés avec quelques métriques d’Actuator suffisent généralement. Le traçage distribué apporte peu de valeur s’il n’y a rien de « distribué ».
  • L’équipe n’a pas encore de backend ni de personnel d’exploitation : OTel n’est qu’un pipeline. Si personne ne regarde les données, vous ne payez que du CPU, du réseau et du stockage supplémentaires.
  • Système extrêmement sensible à la latence ou aux ressources : par exemple les applications embarquées ou les chemins critiques mesurés en microsecondes. Mesurez les frais généraux avant d’activer l’instrumentation à grande échelle.
  • S’attendre à ce que OTel remplace tous les outils existants en une seule fois : le niveau de maturité de chaque signal et de chaque langage peut différer. Vérifiez l’état du support pour la partie que vous envisagez d’utiliser et migrez étape par étape, en commençant par le traçage.

9. Conclusion

OpenTelemetry unifie la façon de créer et de transmettre les traces, les métriques et les logs via une API commune, le protocole OTLP et Collector. Cela vous permet de séparer l’instrumentation dans le code du choix du backend.

Avec Spring Boot 3, le chemin le plus simple se compose de quatre étapes. Premièrement, utilisez Micrometer Observation pour qu’une seule mesure produise à la fois une métrique et un span. Deuxièmement, exportez via OTLP vers Collector. Troisièmement, insérez traceId dans les logs. Quatrièmement, écrivez des tests pour les observations importantes. En parallèle, contrôlez la cardinalité, l’échantillonnage et les données sensibles dès le départ.

Si votre système est encore petit, pas besoin de vous précipiter. Quand le nombre de services augmente et que le débogage commence à prendre des heures, OTel est une plateforme digne d’investissement.

opentelemetryobservabilityspring-boottracingtech-trends