perf sentinelperf sentineldocs
FRENGitHub
Documentation / Instrumentation

Guide d'instrumentation perf-sentinel

Ce guide couvre les parties du pipeline qui transforment l'activité runtime d'une application en l'entrée OTLP / JSON consommée par perf-sentinel. Pour une vue d'ensemble de bout en bout, les quatre topologies supportées et les quatre démarrages rapides, voir Intégration. Pour le côté CI de l'intégration (mode CI, recettes GitHub Actions / GitLab CI / Jenkins, déploiement du rapport HTML interactif, détection de régressions sur PR), voir CI.

Vous n'utilisez pas de SDK OpenTelemetry ? Les équipes sur Datadog peuvent alimenter perf-sentinel en faisant le pont du trafic dd-trace via le datadogreceiver du Collector OTel, sans changement applicatif. Ce guide par langage ne s'applique pas à cette voie, voir Vous venez de Datadog.

Sommaire

Introduction à OpenTelemetry

Si vous n'avez jamais utilisé OpenTelemetry, cette introduction courte est un préalable pour la suite du guide. Elle suppose que vous savez ce qu'est une requête HTTP et une requête en base de données. Elle ne suppose pas que vous avez déjà instrumenté une application ni déployé un backend de tracing. Les autres docs perf-sentinel renvoient ici pour les concepts OTel, voir Intégration et Déploiement Helm.

Qu'est-ce qu'OpenTelemetry. OpenTelemetry (abrégé "OTel") est un projet de la Cloud Native Computing Foundation (CNCF) qui définit un standard ouvert pour collecter les données de télémétrie (traces, métriques, logs) depuis n'importe quel logiciel. C'est la fusion de deux projets antérieurs (OpenTracing et OpenCensus) consolidée en 2019, gouvernée sous la CNCF depuis. Les deux apports pratiques d'OTel :

  • Un protocole (OTLP, OpenTelemetry Protocol) qu'une application peut utiliser pour envoyer traces et métriques vers n'importe quel backend qui le parle. OTLP est stable en format wire, existe en variantes gRPC et HTTP+protobuf, et c'est ce que perf-sentinel ingère sur les ports 4317 (gRPC) et 4318 (HTTP).
  • Des SDK (Java, Python, Go, .NET, Rust, JavaScript, ...) qui gèrent les parties ennuyeuses : capturer chaque appel HTTP/SQL comme un span, propager le trace ID entre services, batcher, retry, envoyer en OTLP. La plupart des SDK incluent une auto-instrumentation pour les frameworks populaires (Spring, Quarkus, ASP.NET Core, Django, Express) donc le code applicatif change rarement.

Concepts clés.

  • Un span est une unité de travail, typiquement une requête HTTP ou une requête SQL. Il porte une durée, un statut, un nom (GET /api/orders) et un sac d'attributs structurés.
  • Une trace est l'arbre de spans qui partagent un trace_id. Une requête utilisateur traverse typiquement plusieurs services, chacun produisant plusieurs spans, tous liés par le même trace_id.
  • Les conventions sémantiques sont les noms d'attributs définis par OTel pour que tous les SDK émettent le même champ pour le même concept. http.request.method est toujours le verbe HTTP, db.system est toujours le nom du moteur de base de données, et ainsi de suite. perf-sentinel lit un petit sous-ensemble de ces attributs pour détecter les anti-patterns. La liste fermée des attributs lus par perf-sentinel est dans Attributs de span requis ci-dessous.

Le Collector. Un processus séparé, l'OpenTelemetry Collector, est la forme de déploiement recommandée entre les applications et les backends. Il reçoit l'OTLP venant d'une flotte d'applications, applique un sampling et un traitement d'attributs optionnels, et forwarde vers un ou plusieurs backends en parallèle (perf-sentinel, plus Tempo ou Jaeger pour le stockage, plus Prometheus pour les exemplars). Faire tourner un Collector central découple les applications des particularités de chaque backend et permet aux opérateurs de changer la politique de sampling sans toucher au code applicatif. Les formes de déploiement pertinentes sont couvertes dans Production : via OpenTelemetry Collector ci-dessous.

Pour aller plus loin. opentelemetry.io, spec OTLP, conventions sémantiques.

Déploiement Kubernetes

Un chart Helm packagé est disponible sous charts/perf-sentinel/. Voir Déploiement Helm pour le guide d'installation complet et examples/helm/ pour un exemple complet qui compose le chart avec le chart upstream OpenTelemetry Collector. Les manifests bruts ci-dessous restent utiles aux utilisateurs qui préfèrent déployer sans Helm.

perf-sentinel se déploie comme un Deployment Kubernetes standard derrière un Service. L'OTel Collector tourne en DaemonSet (par noeud) ou Deployment (centralisé), transmettant les traces à perf-sentinel.

Manifests minimaux

yaml
# Deployment perf-sentinel
apiVersion: apps/v1
kind: Deployment
metadata:
  name: perf-sentinel
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: perf-sentinel
  template:
    metadata:
      labels:
        app: perf-sentinel
    spec:
      containers:
        - name: perf-sentinel
          image: ghcr.io/robintra/perf-sentinel:latest
          ports:
            - containerPort: 4317   # OTLP gRPC
            - containerPort: 4318   # OTLP HTTP + /metrics
          readinessProbe:
            httpGet:
              path: /metrics
              port: 4318
            initialDelaySeconds: 5
          resources:
            requests:
              memory: "64Mi"
              cpu: "50m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          securityContext:
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            runAsNonRoot: true
---
apiVersion: v1
kind: Service
metadata:
  name: perf-sentinel
  namespace: monitoring
spec:
  selector:
    app: perf-sentinel
  ports:
    - name: otlp-grpc
      port: 4317
    - name: otlp-http
      port: 4318

Config exporteur OTel Collector

Dans votre config Collector existante (DaemonSet ou Deployment), ajoutez perf-sentinel comme exporteur :

yaml
exporters:
  otlp/perf-sentinel:
    endpoint: perf-sentinel.monitoring:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      exporters: [otlp/perf-sentinel, otlp/votre-backend]

Instrumentation des applications

Les services envoient les traces au Collector via la variable d'env standard OTEL_EXPORTER_OTLP_ENDPOINT. Si vous utilisez l'OTel Operator, elle est injectée automatiquement. Sinon, définissez-la dans le spec de votre Deployment :

yaml
env:
  - name: OTEL_EXPORTER_OTLP_ENDPOINT
    value: "http://otel-collector.monitoring:4317"
  - name: OTEL_EXPORTER_OTLP_PROTOCOL
    value: "grpc"
  - name: OTEL_SERVICE_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.labels['app']

ServiceMonitor Prometheus

Si vous utilisez le Prometheus Operator, scrapez les métriques perf-sentinel avec un ServiceMonitor :

yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: perf-sentinel
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: perf-sentinel
  endpoints:
    - port: otlp-http
      path: /metrics
      interval: 15s

Intégrations cloud

perf-sentinel est agnostique au cloud : il reçoit des traces OTLP standard. L'essentiel est de router une copie de vos traces vers perf-sentinel en parallèle de votre backend de traces cloud.

AWS (X-Ray + OTel Collector)

AWS X-Ray utilise un format propriétaire, mais l'AWS Distro for OpenTelemetry (ADOT) Collector peut exporter à la fois vers X-Ray et vers perf-sentinel :

yaml
# Config ADOT Collector
exporters:
  awsxray:
    region: eu-west-1
  otlp/perf-sentinel:
    endpoint: perf-sentinel:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [awsxray, otlp/perf-sentinel]

Déployez perf-sentinel comme tâche ECS ou Deployment EKS. Pour ECS, utilisez l'image Docker basée sur scratch (ghcr.io/robintra/perf-sentinel:latest).

GCP (Cloud Trace + OTel Collector)

GCP Cloud Trace supporte l'ingestion OTLP nativement. Utilisez l'OTel Collector standard avec l'exporteur googlecloud et l'exporteur perf-sentinel :

yaml
exporters:
  googlecloud:
    project: mon-projet-gcp
  otlp/perf-sentinel:
    endpoint: perf-sentinel:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [googlecloud, otlp/perf-sentinel]

Déployez perf-sentinel comme service Cloud Run ou Deployment GKE. Pour Cloud Run, exposez les ports 4317 (gRPC) et 4318 (HTTP).

Azure (Application Insights + OTel Collector)

Azure Monitor supporte OTLP via l'Azure Monitor OpenTelemetry Exporter. Routez les traces vers Azure et perf-sentinel :

yaml
exporters:
  azuremonitor:
    connection_string: ${APPLICATIONINSIGHTS_CONNECTION_STRING}
  otlp/perf-sentinel:
    endpoint: perf-sentinel:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [azuremonitor, otlp/perf-sentinel]

Déployez perf-sentinel comme Deployment AKS ou Azure Container Instance.

Auto-hébergé (Jaeger, Tempo, Zipkin)

Si vous utilisez un backend de traces auto-hébergé, l'approche OTel Collector fonctionne de manière identique. Ajoutez perf-sentinel comme exporteur OTLP supplémentaire à côté de votre exporteur backend existant. Alternativement, utilisez le mode batch de perf-sentinel avec un dump OTLP JSON de l'exporter file du Collector, ou avec des fichiers de traces exportés depuis l'UI Jaeger (--input jaeger-export.json) ou Zipkin (--input zipkin-traces.json), les formats sont auto-détectés.


Production : via OpenTelemetry Collector

Si vous avez déjà un OTel Collector, vous pourrez ajouter perf-sentinel comme exporteur OTLP supplémentaire. Votre pipeline de tracing existant (Jaeger, Tempo, etc.) continue de fonctionner, perf-sentinel analyse une copie des mêmes spans.

yaml
# otel-collector-config.yaml
exporters:
  otlp/perf-sentinel:
    endpoint: "perf-sentinel:4317"
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlp/perf-sentinel, otlp/jaeger]   # envoyer aux deux

Le collecteur OTel envoie ses exports compressés en gzip par défaut, et les deux endpoints les acceptent, OTLP/gRPC (:4317) et OTLP/HTTP (POST /v1/traces), aucun override compression: none n'est requis. Les encodages acceptés sont gzip, deflate et non compressé. Tout autre encodage configurable sur l'exporteur, dont snappy et zstd, est refusé par une erreur permanente et doit être ramené à gzip ou none. Le payload décompressé reste soumis à la limite [daemon] max_payload_size (16 Mio par défaut), et un lot au-dessus est refusé par un ResourceExhausted que seuls les logs du collecteur rapportent. Sur un pod à mémoire plafonnée, cette limite borne désormais des tampons de décodage et non des octets réellement téléversés, donc c'est [daemon] memory_high_water_pct (désactivé par défaut) qui empêche l'admission d'une rafale d'exports compressés, voir Configuration.

Jusqu'à la 0.9.26 incluse, l'endpoint gRPC refusait tout export compressé. Le collecteur le journalise en rpc error: code = Unimplemented desc = Content is compressed with `gzip` which isn't supported, le traite comme une erreur permanente et jette le lot, la perte n'apparaît donc nulle part ailleurs. Sur ces versions, il faut soit poser compression: none sur l'exporteur, soit le pointer vers l'endpoint HTTP, qui accepte le gzip depuis la 0.5.5.

Cette approche est recommandée pour les déploiements en production car :

  • Zero modification de code dans vos services
  • Pas de rebuild, pas de redéploiement
  • Fonctionne quel que soit le langage (Java, C#, Rust, Go, Python, Node.js)
  • Le sampling et le filtrage se font au niveau du collector
  • perf-sentinel peut être ajouté ou retiré sans toucher au code applicatif

Une configuration de référence complète est fournie dans examples/otel-collector-config.yaml avec un fichier Docker Compose associé dans examples/docker-compose-collector.yml.

Mise en place de bout en bout avec Docker Compose

  1. Démarrer la stack :
bash
docker compose -f examples/docker-compose-collector.yml up -d
  1. Configurer vos applications pour exporter les traces OTLP vers le collector :
    • gRPC : localhost:4317
    • HTTP : localhost:4318
  2. Vérifier que perf-sentinel reçoit des spans :
bash
curl -s http://localhost:14318/metrics | grep perf_sentinel_events_processed_total
  1. Voir les findings émis par perf-sentinel sur stdout :
bash
docker compose -f examples/docker-compose-collector.yml logs -f perf-sentinel

Sampling et filtrage

Pour les environnements à fort trafic, l'OTel Collector supporte le sampling tail-based et le filtrage pour réduire le volume de traces transmises à perf-sentinel.

Sampling tail-based : conserve les traces complètes selon des critères évalués après l'arrivée de tous les spans :

yaml
processors:
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: specific-services
        type: string_attribute
        string_attribute:
          key: service.name
          values: [game, account, gateway]
      - name: probabilistic
        type: probabilistic
        probabilistic:
          sampling_percentage: 10

Processeur filter : supprime les spans correspondant à des conditions spécifiques :

yaml
processors:
  filter:
    error_mode: ignore
    traces:
      span:
        - 'attributes["service.name"] == "health-check"'

Où placer le sampler. Le sampling existe pour borner ce qu'un magasin de traces conserve, et perf-sentinel ne conserve rien : il tient une fenêtre par trace en mémoire pendant trace_ttl_ms puis la jette. La disposition correcte la moins chère consiste donc à répartir depuis le même receiver et à ne sampler que la branche qui alimente le stockage :

yaml
service:
  pipelines:
    # Stockage : samplé, parce que Tempo se paie à l'octet retenu.
    traces/tempo:
      receivers: [otlp]
      processors: [tail_sampling, batch]
      exporters: [otlp/tempo]
    # Analyse : non samplé, parce que c'est la qualité de détection qui paie.
    traces/perf-sentinel:
      receivers: [otlp]
      processors: [filter, batch]
      exporters: [otlp/perf-sentinel]

Sampler devant perf-sentinel reste possible, c'est simplement une perte que le daemon ne peut pas signaler : une trace conservée est indiscernable d'une trace complète, donc rien dans la sortie ne dit que les chiffres couvrent un dixième du trafic. Si le volume impose de restreindre la branche d'analyse, restreignez-la par périmètre plutôt que par hasard : routez les namespaces ou les services sur lesquels vous travaillez et gardez leurs chiffres entiers, au lieu d'un échantillon probabiliste qui rend partiels les chiffres de tous les services.

Sampling et précision de détection.

La détection d'anti-patterns repose sur du comptage d'événements. Le sampling qui supprime des événements affecte directement les patterns que perf-sentinel peut signaler.

  • Dans une trace conservée, tous les spans sont préservés. OTel et Jaeger samplent par-trace, pas par-span, donc une boucle N+1, un hop vers un service bavard ou un fanout à l'intérieur d'une seule requête se détectent proprement tant que la trace parente est conservée.
  • Le head-sampling casse les détections count-based. Une politique head-sampling à 1% écarte 99% des traces avant qu'elles n'arrivent au collector, donc une boucle N+1 de 50 appels est observée comme 3 appels, bien sous tout seuil raisonnable. Pareil pour les services bavards, le fanout, les parallélisables sérialisés, la saturation de pool. Tout ce qui est piloté par seuil est silencieusement sous-reporté.
  • Le tail-sampling reste compatible avec la détection parce que les politiques qu'on écrirait pour la revue d'incident (garder les erreurs, garder les traces lentes, garder certains services) sont exactement celles qui font remonter les anti-patterns. L'exemple tail_sampling ci-dessus garde tout sous ces politiques plus un échantillonnage probabiliste de 10% du reste.
  • Les comptes sont sous-estimés par tout sampling, silencieusement. Les comptes de findings, les comptes d'occurrences et les totaux Prometheus décrivent les traces arrivées, et rien ne les remet à l'échelle. Les ratios sont plus subtils : un sampler uniforme touche numérateur et dénominateur de la même façon, donc le ratio de gaspillage I/O y survit, mais les politiques errors et slow d'un tail sampler biaisent la rétention vers les traces lourdes et le ratio dérive avec elles. perf-sentinel ne peut détecter ni l'un ni l'autre, donc il ne peut pas alerter dessus. Ne publiez pas ces nombres comme des chiffres de trafic complet, ce qui compte surtout pour disclose, dont l'objet même est de publier un chiffre mesuré. Le knob [daemon] sampling_rate du daemon est le seul cas qu'il voit, et il émet bien un avertissement tuning pour celui-là.
  • La corrélation cross-trace se tait. [daemon.correlation] min_co_occurrences a besoin qu'une paire de findings se répète dans la fenêtre. À 10% d'échantillon, les co-occurrences répétées survivent rarement, donc le corrélateur ne remonte rien même quand le couplage est réel. Ce silence n'est pas la preuve d'une topologie saine.
  • Les runs CI doivent garder 100% des traces. Le volume est bas (un run de tests d'intégration), le coût de l'instrumentation complète est négligeable, et louper une régression à cause du sampling annule l'intérêt du gate CI. Les sections Quick start ci-dessus supposent un sampling à 100%.
  • Le mode pg-stat est immunisé contre le sampling. pg_stat_statements agrège les compteurs de requêtes côté serveur dans PostgreSQL, indépendamment de ce que le tracer applicatif a capturé. Une requête qui s'exécute 10 000 fois apparaît comme 10 000 appels même si 99% des traces parentes ont été écartées au head. Utiliser perf-sentinel pg-stat ... (ou passer --pg-stat à analyze et report) comme fallback quand on ne peut pas faire confiance au volume de traces, ou comme signal principal pour les chemins de code que le tracer ne couvre même pas.
Note : le sampling tail-based nécessite l'image otel/opentelemetry-collector-contrib (pas l'image core).

Attributs de span requis

perf-sentinel détecte les anti-patterns I/O en examinant des attributs de span spécifiques. Les conventions sémantiques legacy et stables d'OpenTelemetry sont toutes deux supportées.

UsageAttribut legacy (pre-1.21)Attribut stable (1.21+)Exemple
Texte requête SQLdb.statementdb.query.textSELECT * FROM player WHERE game_id = 42
Système SQLdb.systemdb.systempostgresql, mysql
URL cible HTTPhttp.urlurl.fullhttp://account-svc:5000/api/account/123
Méthode HTTPhttp.methodhttp.request.methodGET, POST
Statut HTTPhttp.status_codehttp.response.status_code200, 404
Appelé RPCrpc.system + rpc.service/rpc.method(idem)grpc, order.v1.OrderService/GetOrder
Système de brokermessaging.system(idem)kafka, rabbitmq, pulsar, aws_sqs
Destination brokermessaging.destinationmessaging.destination.nameorders, signature.jobs
Taille du messagemessaging.message.body.size(idem)4096
Endpoint sourcehttp.routehttp.routePOST /api/game/{id}/start
Nom du serviceservice.name (ressource)service.name (ressource)game, account-svc
Namespace de serviceservice.namespace (ressource)(idem)commerce
Namespace Kubernetesk8s.namespace.name (ressource)(idem)prod-eu

Les spans qui ne portent aucun attribut SQL, HTTP, RPC ou messaging sont ignorés. Les agents OTel modernes (v2.x) émettent la convention stable par défaut. Les agents plus anciens émettent la convention legacy. perf-sentinel gère les deux de manière transparente.

Les attributs qui séparent un déploiement d'un autre relèvent de la configuration, pas d'une paire figée. [detection] grouping_attributes prend une liste ordonnée d'attributs de ressource ou de span, dont la valeur par défaut est ["k8s.namespace.name", "service.namespace"]. Le premier présent sur un span décide de l'identité : deux findings identiques dans deux regroupements restent deux findings, et la clé fait partie de cette identité afin que tenant.id=prod ne puisse pas entrer en collision avec k8s.namespace.name=prod. Chaque attribut listé et présent est capturé et chaque surface l'affiche sous la forme clé=valeur. Un cluster mutualisé où le namespace ne distingue pas les tenants peut donc grouper par tenant.id, à condition que l'application le pose sur ses spans. Le filtre HTML utilise le premier attribut configuré capturé ; si aucun n'est présent, le finding n'a pas de puce de regroupement. Les signatures d'acquittement ignorent complètement cette liste, un acquittement couvre donc toujours tous les déploiements et réordonner la liste n'invalide jamais un acquittement. Le même ordre configuré s'applique aux fichiers batch, aux transports OTLP gRPC et HTTP du daemon, à Tempo et à Jaeger Query. Jaeger lit les valeurs dans les tags du process avec repli sur ceux du span, et Zipkin dans les tags du span.

Les spans RPC (gRPC, Dubbo et frameworks similaires) ne portent ni statement ni URL, ils sont donc identifiés par rpc.system et modélisés comme des appels sortants : la cible est rpc.service/rpc.method (avec repli sur le nom du span quand l'un des deux manque), et les findings apparaissent sous les types _http. Cela garde les détecteurs topologiques (fanout, bavard, sérialisé) et d'occurrence (n+1, redondant) opérationnels sur les flottes à dominante RPC. Les spans RPC ne portent pas de texte de requête, donc n_plus_one_sql et le normalizer SQL ne s'y appliquent jamais.

Trois conséquences à connaître sur les findings RPC :

  • Seuls les spans CLIENT sont modélisés. Les attributs rpc.* sont posés à la fois sur le span SERVER entrant (le handler) et sur le span CLIENT sortant, donc perf-sentinel n'admet que SpanKind::Client. Un span RPC dont le kind est absent ou non-CLIENT est traité comme du travail entrant (pas un appel sortant), donc une instrumentation qui ne pose jamais le kind ne produit aucun finding RPC.
  • Les findings sortent sous les types _http. Un N+1 RPC est rapporté comme n_plus_one_http et sa remédiation mentionne un endpoint batch HTTP. Le finding est correct sur l'anti-pattern (l'appel de dépendance répété), seuls le label de protocole et la formulation batch-endpoint sont teintés HTTP.
  • Les arguments par appel sont invisibles. La charge utile d'une requête gRPC vit dans le corps du message protobuf, pas dans un attribut de span, donc N appels distincts à la même méthode partagent un unique template à paramètres vides. Comme une URL HTTP à query redactée (voir Limites), ces appels sont lus comme redundant_http plutôt que n_plus_one_http. Le signal d'appel répété est réel dans les deux cas, seule la remédiation "cache vs batch" diffère.

Les spans messaging (Kafka, RabbitMQ, Pulsar, SQS, NATS, JMS) ne portent eux non plus ni statement ni URL, ils sont donc identifiés par messaging.system et modélisés comme des appels sortants dont la cible est la destination, avec repli sur le nom du span quand l'attribut de destination manque. Une seule convention couvre toute la famille. Contrairement au RPC, ils ont leurs propres types de findings : n_plus_one_messaging et slow_messaging. Il n'y a pas d'équivalent redundant, une publication ne porte aucun paramètre à comparer.

Trois conséquences à connaître sur les findings messaging :

  • Seuls les spans PRODUCER sont modélisés. Un span CONSUMER décrit un travail effectué sur un message reçu, pas un appel émis par le service, et un span messaging CLIENT est un poll (receive) ou un acquittement (settle). Les admettre attribuerait au service des publications qu'il n'a jamais faites. Une instrumentation qui ne pose jamais le kind ne produit donc aucun finding messaging.
  • Les destinations sont comparées telles quelles. Un nom de topic ou de queue est déjà un template, il ne passe donc pas par le normaliseur de chemins HTTP. Un topic nommé par tenant donne un template par tenant. Voir Limites.
  • Le côté consommateur est relié, pas fusionné. Le span link OTel porté par un ancêtre CONSUMER est reporté sur les spans d'I/O du handler et rendu par explain sous la forme triggered by trace <id> en CLI, dans la TUI et dans /api/explain/{trace_id}, mais pas dans le dashboard HTML. Les traces producteur et consommateur restent distinctes, donc les détecteurs structurels ne voient jamais au travers du broker.
Écartement silencieux. Un span écarté pour un attribut porteur manquant ne produit ni avertissement ni erreur. Un span SQL sans db.statement / db.query.text, ou un span HTTP sans http.url / url.full, ne donne tout simplement aucun finding. Un rapport maigre ou vide peut donc signifier aucun problème ou aucune instrumentation exploitable. Lancez perf-sentinel inspect pour voir ce qui a réellement été extrait, et voir La qualité de l'instrumentation borne les findings.
http.route est porteur pour la stabilité des acks. La signature d'acknowledgment clé sur le template de route, pas sur l'URL instanciée. Les services qui émettent http.route (Spring Boot, ASP.NET Core, Express, toute auto-instrumentation moderne) conservent des acks qui survivent aux redémarrages et aux ids de requête tournants. Les services qui retombent sur http.url ou url.full perdent cette stabilité. Voir Flux d’acquittement pour la recette de vérification.

Dev/staging : instrumentation par langage

Quand aucun OTel Collector n'est disponible, instrumentez les services directement. Les guides ci-dessous sont ordonnes du plus simple au plus complexe.

Java (OpenTelemetry Java Agent v2.27+, Spring Boot, Helidon 4.x)

Le Java Agent OTel instrumente JDBC, R2DBC, les clients HTTP, Spring Web et la plupart des frameworks automatiquement, sans modification de code. C'est l'approche la plus proche du plug and play.

1. Téléchargez l'agent

bash
curl -L -o opentelemetry-javaagent.jar \
  https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

2. Lancez votre application avec l'agent

bash
export JAVA_TOOL_OPTIONS="-javaagent:/path/to/opentelemetry-javaagent.jar"
export OTEL_SERVICE_NAME=mon-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_TRACES_SAMPLER=always_on
export OTEL_METRICS_EXPORTER=none
export OTEL_LOGS_EXPORTER=none
java -jar my-app.jar

L'agent capture automatiquement :

  • db.query.text depuis JDBC (Spring Data JPA, Hibernate) et R2DBC (Spring WebFlux réactif)
  • url.full depuis les clients HTTP (WebClient, RestTemplate, HttpClient)
  • http.route depuis Spring MVC et Spring WebFlux (requêtes entrantes)
  • Propagation du trace context entre les appels asynchrones, les chaînes réactives et les appels inter-services

Validé sur Spring Boot 4 avec WebFlux/R2DBC, Virtual Threads/JPA et MVC/JDBC standard.

R2DBC et gestion des placeholders SQL. Les drivers R2DBC utilisent les marqueurs natifs de la base ($1, $2 pour PostgreSQL, ? pour MySQL/MariaDB). Le sanitizer intégré du Java Agent remplace tous les littéraux par ? avant de remplir db.statement, quel que soit le driver sous-jacent. Cela signifie que perf-sentinel reçoit des templates sanitisés avec ? et des params vides pour les stacks JDBC comme R2DBC. Sans l'agent (R2DBC SDK seul, sans auto-instrumentation), db.statement contiendrait les marqueurs natifs $1/$2, que perf-sentinel gère aussi (le normalizer SQL reconnaît $N comme placeholder depuis v0.7.7). Dans les deux cas, le chemin de detection N+1 sanitizer-aware fonctionne correctement.

Limitations connues

Incompatibilité Project Leyden / AOT cache. Le flag -javaagent: est incompatible avec les AOT caches JEP 483. Désactivez le cache AOT quand l'agent est actif :

bash
if echo "$JAVA_TOOL_OPTIONS" | grep -q "javaagent"; then
  exec java -jar /app/my-app.jar
else
  exec java -XX:AOTCache=/app/app.aot -jar /app/my-app.jar
fi

Le starter Spring Boot ne suffit pas. Le spring-boot-starter-opentelemetry (Spring Boot 4) n'instrumente pas les appels sortants WebClient ou RestTemplate avec propagation du trace context. Utilisez le Java Agent pour une détection N+1 HTTP complète.

Tests d'intégration en CI (Maven Failsafe)

La configuration ci-dessus suppose un processus qui tourne en continu et parle à un endpoint OTLP live. Les tests d'intégration sont différents : ils tournent dans la JVM du test runner lui-même, et il n'y a pas de daemon vers qui envoyer des traces en CI. Voir CI pour le mode batch que cette configuration alimente.

Java n'a pas d'exporteur fichier, et une JVM de test forkée ne vous donne pas non plus sa sortie standard. Aucun exporteur Java supporté n'écrit les spans dans un fichier au chemin de votre choix. L'exporteur otlp_file/development de la configuration déclarative définit bien un champ output_stream: file://..., mais l'implémentation Java le déclare non implémenté. Reste experimental-otlp/stdout, qui écrit du JSON OTLP sur System.out, et c'est là que Maven s'interpose : Surefire et Failsafe dialoguent avec la JVM forkée via un protocole encodé porté par la sortie standard de ce fork. L'agent s'initialise en premain et capture le System.out d'origine, c'est-à-dire le canal de commande lui-même, avant que Surefire n'installe le wrapper sur lequel agit redirectTestOutputToFile. Chaque export est alors classé comme corruption du canal et dévié dans target/failsafe-reports/<horodatage>-jvmRunN.dumpstream :

Corrupted channel by directly writing to native stream in forked JVM 1.
Stream '{"resourceSpans":[{"resource":{"attributes":[{"key":"host.arch",…}]}}]}'.

Rien d'exploitable n'atteint -output.txt, et rediriger le build avec tee n'aide pas davantage, la sortie standard du fork étant le canal et non la console. Ce n'est pas un artefact de version, Failsafe 3.5.0, 3.2.5 et 2.22.2 la dévient tous.

Les traces doivent donc quitter la JVM comme elles le font en production, par le réseau, et quelque chose doit écouter. C'est le rôle de perf-sentinel capture.

Attachez l'agent à la JVM de test, pas seulement à l'image buildée. Si les tests d'intégration tournent en process contre @SpringBootTest (Maven Failsafe, integrationTest Gradle) plutôt que contre le conteneur buildé, l'agent embarqué dans votre Dockerfile ne les voit jamais. Copiez le jar de l'agent dans le build, épinglé sur la version du Dockerfile pour que les deux environnements instrumentent de la même façon :

xml
<!-- Copie le jar de l'agent dans target/ avant la phase integration-test. -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-dependency-plugin</artifactId>
  <executions>
    <execution>
      <id>copy-otel-agent</id>
      <phase>pre-integration-test</phase>
      <goals><goal>copy</goal></goals>
      <configuration>
        <artifactItems>
          <artifactItem>
            <groupId>io.opentelemetry.javaagent</groupId>
            <artifactId>opentelemetry-javaagent</artifactId>
            <version>2.27.0</version> <!-- même version que celle du Dockerfile -->
            <destFileName>opentelemetry-javaagent.jar</destFileName>
          </artifactItem>
        </artifactItems>
        <outputDirectory>${project.build.directory}</outputDirectory>
      </configuration>
    </execution>
  </executions>
</plugin>

<!-- Ajoute -javaagent à l'argLine EXISTANT de failsafe, ne le remplace pas. -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <configuration>
    <argLine>@{argLine} -javaagent:${project.build.directory}/opentelemetry-javaagent.jar</argLine>
    <environmentVariables>
      <OTEL_TRACES_EXPORTER>otlp</OTEL_TRACES_EXPORTER>
      <OTEL_EXPORTER_OTLP_ENDPOINT>http://localhost:4317</OTEL_EXPORTER_OTLP_ENDPOINT>
      <OTEL_EXPORTER_OTLP_PROTOCOL>grpc</OTEL_EXPORTER_OTLP_PROTOCOL>
      <OTEL_SERVICE_NAME>mon-service</OTEL_SERVICE_NAME>
      <OTEL_TRACES_SAMPLER>always_on</OTEL_TRACES_SAMPLER>
      <OTEL_METRICS_EXPORTER>none</OTEL_METRICS_EXPORTER>
      <OTEL_LOGS_EXPORTER>none</OTEL_LOGS_EXPORTER>
    </environmentVariables>
  </configuration>
</plugin>

Conservez le contenu existant de votre <argLine> (flags de heap, placeholder JaCoCo @{argLine}) et ajoutez -javaagent:... à la fin. L'écraser est une erreur fréquente qui fait silencieusement disparaître l'instrumentation de couverture JaCoCo. OTEL_TRACES_SAMPLER=always_on compte plus ici qu'en production : le sampling supprimerait justement les appels répétés dont dépend la détection N+1.

Précisez le protocole, ne comptez pas sur le défaut. L'agent 2.0 l'a fait passer de grpc à http/protobuf, un même endpoint désigne donc des ports différents selon la version de l'agent. Un endpoint pointé sur le mauvais n'exporte rien et ne prévient que dans le log de l'agent lui-même, ce qui laisse une capture vide pour une raison que rien d'autre ne nomme. :4317 avec grpc, comme ci-dessus, ou :4318 avec http/protobuf, les deux conviennent.

Rien de tout cela n'est spécifique à perf-sentinel, c'est la configuration OTLP standard. Ce qui change, c'est qui écoute.

Option 1, perf-sentinel capture (recommandée)

La sous-commande capture reçoit de l'OTLP et écrit un fichier de traces, rien d'autre. Pas de Collector, pas de conteneur, pas de plugin, et le fork reste tel quel. Soit vous enveloppez l'étape de test :

bash
perf-sentinel capture --output target/traces.json -- mvn verify
perf-sentinel analyze --ci --input target/traces.json

soit, quand l'étape de test ne peut pas être préfixée parce que votre pipeline la génère, vous écoutez à côté :

bash
perf-sentinel capture --output target/traces.json &
CAPTURE=$!
mvn verify
kill -TERM $CAPTURE && wait $CAPTURE
perf-sentinel analyze --ci --input target/traces.json
Préfixez votre étape de test existante, n'en ajoutez jamais une seconde. capture -- mvn verify lance les tests une fois, il ne les relance pas. Ajouter une nouvelle étape à côté de l'existante ferait tourner toute la suite d'intégration deux fois, pour rien.
Un objectif de nettoyage ne peut pas envelopper une capture qui écrit dans ce qu'il nettoie. capture --output target/traces.json -- mvn clean verify échoue par construction : capture ouvre le fichier avant de lancer la commande, clean supprime ensuite target/ sous lui, et le run se termine par une erreur nommant le fichier disparu plutôt que par un compte de spans pour un inode qu'aucun chemin ne désigne. Retirez clean de la commande enveloppée, comme le fait la recette ci-dessus, ou écrivez le fichier de traces hors du répertoire nettoyé (--output /tmp/traces.json).

L'enveloppe est la plus solide des deux : les ports sont liés avant que la commande démarre, aucun export ne peut donc se perdre dans une course au démarrage, et la capture s'arrête à la fin de la commande plutôt que sur un délai deviné. La commande enveloppée hérite de stdout et stderr sans altération, et son code de sortie est propagé, un échec de tests reste donc un échec de job.

Le fichier est du NDJSON, une requête OTLP par ligne, exactement la forme que produit l'exporteur file du Collector, et la détection automatique de format le lit sans aucun flag. capture n'écrit que sur stderr, et annonce combien de spans il a reçus, ce qui permet de distinguer "aucun anti-pattern" de "rien n'a jamais été exporté". Un fichier de traces vide est rejeté par analyze au lieu d'être présenté comme une gate au vert.

Détails : Référence CLI, et perf-sentinel capture --help pour --listen-address, --max-file-size et --grace-ms.

Option 2, un OpenTelemetry Collector

Si un Collector fait déjà partie du job, gardez-le. Son exporteur file produit le même NDJSON, voir Production : via OpenTelemetry Collector. C'est la forme la plus lourde, un conteneur de plus à démarrer et à arrêter, et elle se justifie surtout quand les mêmes traces doivent atteindre un autre backend en même temps.

Option 3, aucun récepteur

<forkCount>0</forkCount> supprime le fork, donc le canal de commande, et experimental-otlp/stdout atteint alors la console : un grep sur le log de build donne le fichier de traces. Cette voie ne demande aucun écouteur, à un prix : l'isolation des tests disparaît, la capture porte alors les spans de Maven en plus de ceux de l'application, et tout ce qui reposait sur <argLine>, en particulier un placeholder JaCoCo @{argLine}, doit passer par MAVEN_OPTS sous peine de cesser silencieusement de s'appliquer. À réserver aux cas où rien ne peut écouter sur un port.

Trois noms d'exporteur voisins n'aident pas ici. logging affiche un résumé de span lisible par un humain et non du JSON OTLP, perf-sentinel ne peut pas le parser du tout. logging-otlp émet bien du JSON OTLP, mais via un logger, donc chaque ligne porte le préfixe ajouté par la configuration de logs de l'application. otlp_file et OTEL_EXPORTER_OTLP_FILE_PATH n'existent tout simplement pas, malgré leurs noms très plausibles.


Java (Quarkus 3.33 LTS + quarkus-opentelemetry + OTel Agent v2.27)

Pour les applications Quarkus (y compris les images natives GraalVM où le Java Agent ne peut pas être utilisé), ajoutez l'extension quarkus-opentelemetry :

xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-opentelemetry</artifactId>
</dependency>

Configurez dans application.properties :

properties
quarkus.otel.exporter.otlp.endpoint=${OTLP_GRPC_ENDPOINT:http://localhost:4317}
quarkus.otel.exporter.otlp.protocol=grpc
quarkus.otel.service.name=mon-service
quarkus.otel.enabled=${OTEL_ENABLED:false}
quarkus.otel.metrics.exporter=none
quarkus.otel.logs.exporter=none

Activez le tracing en définissant OTEL_ENABLED=true et OTLP_GRPC_ENDPOINT dans votre environnement. Pour les images natives, utilisez le préfixe QUARKUS_ pour les surcharges au runtime.


.NET (ASP.NET Core + Entity Framework Core + OpenTelemetry SDK 1.15)

Compatible NativeAOT (PublishAot=true). Nécessite l'ajout de packages NuGet et ~15 lignes dans Program.cs.

xml
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />

Pour les projets .NET 8, utilisez la version 1.9.0 au lieu de 1.12.0 pour éviter les conflits de dépendances.

csharp
var otlpEndpoint = Environment.GetEnvironmentVariable("OTLP_GRPC_ENDPOINT");
if (!string.IsNullOrEmpty(otlpEndpoint))
{
    builder.Services.AddOpenTelemetry()
        .ConfigureResource(r => r.AddService("mon-service"))
        .WithTracing(tracing => tracing
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddOtlpExporter(o =>
            {
                o.Endpoint = new Uri(otlpEndpoint);
                o.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
            }));
}

Pour la détection des requêtes SQL, ajoutez l'instrumentation correspondant à votre couche d'accès aux données :

  • Entity Framework Core (MySQL, PostgreSQL, SQLite) : .AddEntityFrameworkCoreInstrumentation(o => o.SetDbStatementForText = true) avec OpenTelemetry.Instrumentation.EntityFrameworkCore
  • SqlClient (SQL Server) : .AddSqlClientInstrumentation(o => o.SetDbStatementForText = true) avec OpenTelemetry.Instrumentation.SqlClient

L'option SetDbStatementForText = true est requise pour que perf-sentinel voie le texte des requêtes. Sans elle, les spans SQL sont émis mais db.statement est vide.

Note : Entity Framework Core utilise des paramètres nommés (@__param_0). Les valeurs réelles n'étant pas visibles dans le template, perf-sentinel peut detecter des requêtes répétées comme redundant_sql plutôt que n_plus_one_sql.

Note : System.Net.Http redacte la query string en ?* par défaut, donc les boucles N+1 HTTP sortantes qui font varier un paramètre de query (?seq=1, ?seq=2, ...) arrivent à perf-sentinel comme des URLs identiques et sont détectées comme redundant_http plutôt que n_plus_one_http. Pour obtenir n_plus_one_http sur ces boucles, posez OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION=true pour que la query survive, ou modélisez l'identifiant variable comme un segment de path (/api/resource/{id}). Voir Limites pour le raisonnement complet.


Go (otelhttp 0.68 + otelpgx 0.11, OTel SDK 1.43)

Le SDK Go OTel utilise un wrapping explicite. HTTP et SQL nécessitent chacun une bibliothèque dédiée. otelpgx émet db.statement avec les paramètres positionnels PostgreSQL natifs ($1, $2). perf-sentinel les normalise en $? avec des params vides, ce qui active le chemin de detection N+1 sanitizer-aware. Aucune configuration supplémentaire nécessaire.

Les variables d'environnement sont standard :

yaml
environment:
  OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
  OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
  OTEL_SERVICE_NAME: go-svc

Voir la section anglaise pour les exemples de code complets (Go SDK wrapping, pgx pool configuration).


Python (Django 5.x + psycopg, OTel SDK 1.42)

Les applications Django utilisent les packages d'auto-instrumentation. psycopg émet db.statement avec les placeholders Python DB-API %s. perf-sentinel reconnaît %s comme placeholder, donc le chemin sanitizer-aware fonctionne sans configuration supplémentaire.

opentelemetry-sdk==1.42.1
opentelemetry-instrumentation-django==0.63b1
opentelemetry-instrumentation-psycopg==0.63b1

Python (FastAPI + SQLAlchemy 2.x + asyncpg, OTel SDK 1.42)

FastAPI avec SQLAlchemy utilise les packages d'auto-instrumentation. asyncpg émet db.statement avec les paramètres PostgreSQL natifs ($1, $2). Le scope sqlalchemy est dans la liste des ORM reconnus, donc le chemin sanitizer-aware fire via le chemin ORM.

opentelemetry-sdk==1.42.1
opentelemetry-instrumentation-fastapi==0.63b1
opentelemetry-instrumentation-sqlalchemy==0.63b1
opentelemetry-instrumentation-asyncpg==0.63b1

Node.js (Nest.js + Prisma, OTel SDK 0.218)

Les applications Nest.js utilisent le package @opentelemetry/sdk-node. Prisma génère le SQL, le client pg l'envoie. Le scope prisma est dans la liste des ORM reconnus.

json
{
  "@opentelemetry/sdk-node": "0.218.0",
  "@opentelemetry/instrumentation-http": "0.218.0",
  "@opentelemetry/instrumentation-pg": "0.70.0"
}

Rust (tracing-opentelemetry 0.33, Diesel, SeaORM)

Nécessite l'ajout de 4 crates et ~20 lignes de code d'initialisation. Utilisez provider.tracer() (pas global::tracer()) pour éviter le problème de trait bound PreSampledTracer. Pour les applications Rust utilisant Diesel ou SeaORM, le crate ORM émet le SQL directement dans le span tracing. Les scopes diesel et sea-orm sont dans la liste des ORM reconnus.

toml
[dependencies]
tracing-opentelemetry = "0.33"
opentelemetry = "0.32"
opentelemetry_sdk = "0.32"
opentelemetry-otlp = { version = "0.32", features = ["http-proto", "reqwest-blocking-client"] }

Ruby (Rails + ActiveRecord, opentelemetry-ruby)

Les applications Rails utilisent les gems d'instrumentation opentelemetry-ruby. L'instrumentation ActiveRecord fournit le scope ORM, l'instrumentation du driver sous-jacent (pg, mysql2) émet le db.statement SQL.

Dépendances (Gemfile) :

ruby
gem 'opentelemetry-sdk'
gem 'opentelemetry-exporter-otlp'
gem 'opentelemetry-instrumentation-rails'
gem 'opentelemetry-instrumentation-active_record'
gem 'opentelemetry-instrumentation-pg'

Initialisation (config/initializers/opentelemetry.rb) :

ruby
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'opentelemetry/instrumentation/all'

OpenTelemetry::SDK.configure do |c|
  c.service_name = 'rails-svc'
  c.use 'OpenTelemetry::Instrumentation::Rails'
  c.use 'OpenTelemetry::Instrumentation::ActiveRecord'
  c.use 'OpenTelemetry::Instrumentation::PG', { db_statement: :include }
end

L'instrumentation pg a besoin de db_statement: :include (ou de :obfuscate par défaut, qui émet le template sanitisé) pour que le SQL parvienne à perf-sentinel. Le scope OpenTelemetry::Instrumentation::ActiveRecord circule dans la chaîne de spans et est reconnu comme un ORM, donc le chemin N+1 sanitizer-aware se déclenche et les findings portent des fixes suggérés spécifiques à ActiveRecord (includes / preload / eager_load).

L'instrumentation active_record n'émet ce scope que pour les requêtes qui chargent des records (find_by_sql, where(...).to_a). Les requêtes d'agrégat (count, sum) ne portent que le span du driver pg / mysql2, donc leurs findings retombent sur le fix ruby_generic.

Variables d'environnement :

yaml
environment:
  OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
  OTEL_SERVICE_NAME: rails-svc

PHP (Laravel / Eloquent, Symfony / Doctrine, opentelemetry-php)

Les applications PHP utilisent l'extension d'auto-instrumentation OpenTelemetry PHP plus les paquets d'instrumentation de framework de open-telemetry/opentelemetry-php-contrib. Les instrumentations enregistrent des scopes natifs (io.opentelemetry.contrib.php.pdo, io.opentelemetry.contrib.php.doctrine, io.opentelemetry.contrib.php.laravel) et posent code.function.name sous la forme Namespace\Class::method, ce sur quoi perf-sentinel s'appuie pour les fixes conscients du framework.

Dépendances (composer) :

bash
pecl install opentelemetry
composer require \
  open-telemetry/sdk open-telemetry/exporter-otlp \
  open-telemetry/opentelemetry-auto-pdo \
  open-telemetry/opentelemetry-auto-laravel    # ou -auto-symfony + -auto-doctrine

Correspondance des frameworks.

  • Laravel/Eloquent : le span SQL feuille est scope PDO, mais le scope applicatif io.opentelemetry.contrib.php.laravel circule dans la chaîne de spans, donc les findings portent des fixes php_laravel_eloquent (eager loading with() / load()) sur tous les anti-patterns.
  • Symfony/Doctrine : le scope io.opentelemetry.contrib.php.doctrine est émis directement sur le span SQL (DBAL est instrumenté), donc les findings SQL portent des fixes php_doctrine (fetch-join DQL). Une application Symfony qui utilise du PDO brut au lieu de Doctrine retombe sur php_generic.

L'instrumentation PDO émet le template SQL obfusqué par défaut, ce qui suffit pour la détection. Le scope io.opentelemetry.contrib.php.pdo seul (sans scope Laravel/Doctrine) route vers php_generic.

Vous venez de dd-trace-php ? Le pont via le datadogreceiver du Collector fonctionne pour la détection mais perd le signal de framework (pas de code.*, scope Datadog fixe), donc ces findings n'ont pas de fix conscient du framework. Voir Vous venez de Datadog.

Variables d'environnement :

yaml
environment:
  OTEL_PHP_AUTOLOAD_ENABLED: "true"
  OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
  OTEL_SERVICE_NAME: php-svc

Styles de placeholders SQL et detection

Les différents drivers de base de données émettent des syntaxes de placeholder différentes dans l'attribut db.statement du span. Le normalizer SQL de perf-sentinel reconnaît tous les styles courants et les mappe en $? ou ? dans le template normalisé, avec params vide pour les requêtes paramétrées. C'est ce qui active le chemin de detection N+1 sanitizer-aware (qui exige params == [] et un placeholder reconnu dans le template).

PlaceholderProduit parNormalisé enExemple
?Agent JDBC (Java), R2DBC via Java Agent, MySQL Connector/J 8.2+ OTel natif, Go go-sql-driver/mysql, Node.js mysql2?WHERE id = ?
$1, $2PostgreSQL natif (pgx, asyncpg, sqlx, node-pg)$?WHERE id = $?
%sPython DB-API (psycopg, MySQLdb, PyMySQL, mysql-connector-python)%s (conservé)WHERE id = %s
@p0, @Name.NET (Npgsql, SqlClient, MySqlConnector/Pomelo)@p0 (conservé)WHERE id = @p0
:nameOracle, SQLAlchemy named:name (conservé)WHERE id = :oid

Ce que cela signifie pour les opérateurs. Aucune configuration n'est nécessaire pour activer la detection sur ces stacks. Le normalizer et le check template_has_placeholder dans le pipeline de detection gèrent le mapping automatiquement. L'exigence clé est que l'instrumentation OTel émette db.statement sur les spans SQL.

Marqueurs de scope ORM. Le chemin sanitizer-aware consulte aussi le scope d'instrumentation OTel (le nom de la bibliothèque) pour décider si un groupe de requêtes sanitizées est probablement N+1 ou juste redondant. Les scopes suivants sont reconnus : spring-data, hibernate, jpa, micronaut-data, jdbi, r2dbc, entityframeworkcore, entity-framework, sqlalchemy, django, active-record, activerecord, gorm, sequelize, prisma, typeorm, mongoose, sea-orm, diesel.