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 prises en charge 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
- Déploiement Kubernetes : manifests pour le daemon et le sidecar OTel Collector.
- Intégrations cloud : AWS X-Ray, GCP Cloud Trace, Azure Application Insights, Jaeger / Tempo / Zipkin auto-hébergés.
- Production : via OpenTelemetry Collector : configuration du collector central, sampling et précision de détection.
- Attributs de span requis : les conventions sémantiques OTel legacy et stables que Perf Sentinel lit.
- Dev/staging : instrumentation par langage :
- Java
- .NET (ASP.NET Core + Entity Framework Core)
- Go (pgx)
- Python
- Node.js (Nest.js + Prisma)
- Rust (Diesel, SeaORM)
- Ruby (Rails + ActiveRecord)
- PHP (Laravel / Eloquent, Symfony / Doctrine)
- Styles de placeholders SQL et détection : comment Perf Sentinel associe le placeholder SQL de chaque instrumentation au chemin de détection N+1 sanitizer-aware.
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 (souvent 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 a un format de transmission stable, 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, regrouper en lots, réessayer, 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.
- 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êmetrace_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.methodest toujours le verbe HTTP,db.systemest 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 transmet à 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 nœud) ou Deployment (centralisé), transmettant les traces à Perf Sentinel.
Manifests minimaux
# 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/institut-du-numerique-responsable/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: 4318Config exporteur OTel Collector
Dans votre config Collector existante (DaemonSet ou Deployment), ajoutez Perf Sentinel comme exporteur :
exporters:
otlp/perf-sentinel:
endpoint: perf-sentinel.monitoring:4317
tls:
insecure: true
service:
pipelines:
traces:
exporters: [otlp/perf-sentinel, otlp/your-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 :
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 :
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: 15sIntégrations cloud
Perf Sentinel est agnostique au cloud : il reçoit des traces OTLP standard. Routez 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 :
# 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/institut-du-numerique-responsable/perf-sentinel:latest).
GCP (Cloud Trace + OTel Collector)
GCP Cloud Trace prend en charge l'ingestion OTLP nativement. Utilisez l'OTel Collector standard avec l'exporteur googlecloud et l'exporteur Perf Sentinel :
exporters:
googlecloud:
project: my-gcp-project
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 prend en charge OTLP via l'Azure Monitor OpenTelemetry Exporter. Routez les traces vers Azure et Perf Sentinel :
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 pouvez 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.
# 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 deuxLe 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), donc aucune surcharge compression: none n'est requise. 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 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 :
- Zéro modification de code dans vos services
- Pas de reconstruction, 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
- Démarrer la stack :
docker compose -f examples/docker-compose-collector.yml up -d- Configurer vos applications pour exporter les traces OTLP vers le collector :
- gRPC :
localhost:4317 - HTTP :
localhost:4318 - Vérifier que Perf Sentinel reçoit des spans :
curl -s http://localhost:14318/metrics | grep perf_sentinel_events_processed_total- Voir les findings émis par Perf Sentinel sur stdout :
docker compose -f examples/docker-compose-collector.yml logs -f perf-sentinelSampling et filtrage
Pour les environnements à fort trafic, l'OTel Collector prend en charge 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 :
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: 10Processeur filter : supprime les spans correspondant à des conditions spécifiques :
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 :
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 est pris en charge, mais cela entraîne 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 saut 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 fondées sur des comptages. 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-signalé.
- 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 du processeur
tail_samplingci-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 reste sans biais. En revanche, les politiques
errorsetslowd'un tail sampler biaisent la rétention vers les traces lourdes et le ratio dérive avec elles. Perf Sentinel ne peut pas détecter un sampling en amont, donc il ne peut alerter sur aucun des deux cas. Ne publiez pas ces nombres comme des chiffres de trafic complet, ce qui compte surtout pourdisclose, dont l'objet même est de publier un chiffre mesuré. Le réglage[daemon] sampling_ratedu daemon est le seul cas qu'il voit, et il émet bien un avertissementtuningpour celui-là. - La corrélation cross-trace se tait.
[daemon.correlation] min_co_occurrencesa 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 manquer une régression à cause du sampling annule l'intérêt du gate CI. Les démarrages rapides de Intégration supposent un sampling à 100%.
- Le mode
pg-statest immunisé contre le sampling.pg_stat_statementsagrè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. Utiliserperf-sentinel pg-stat ...(ou passer--pg-statàanalyzeetreport) comme repli 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 prises en charge.
| Usage | Attribut legacy (pré-1.21) | Attribut stable (1.21+) | Exemple |
|---|---|---|---|
| Texte requête SQL | db.statement | db.query.text | SELECT * FROM player WHERE game_id = 42 |
| Système SQL | db.system | db.system.name | postgresql, mysql |
| URL cible HTTP | http.url | url.full | http://account-svc:5000/api/account/123 |
| Méthode HTTP | http.method | http.request.method | GET, POST |
| Statut HTTP | http.status_code | http.response.status_code | 200, 404 |
| Appelé RPC | rpc.system + rpc.service/rpc.method | (idem) | grpc, order.v1.OrderService/GetOrder |
| Système de broker | messaging.system | (idem) | kafka, rabbitmq, pulsar, aws_sqs |
| Destination broker | messaging.destination | messaging.destination.name | orders, signature.jobs |
| Taille du message | messaging.message.body.size | (idem) | 4096 |
| Endpoint source | http.route, url.path | http.route, url.path | POST /api/game/{id}/start |
| Nom du service | service.name (ressource) | service.name (ressource) | game, account-svc |
| Namespace de service | service.namespace (ressource) | (idem) | commerce |
| Namespace Kubernetes | k8s.namespace.name (ressource) | (idem) | prod-eu |
| Fonction du code | code.namespace + code.function | code.function.name | com.example.OrderService.place |
| Fichier et ligne | code.filepath, code.lineno | code.file.path, code.line.number | OrderService.java, 42 |
Les services Spring Boot tracés par Micrometer Observation (le starter spring-boot-starter-opentelemetry, ou le pont Micrometer vers Zipkin) posent method et status sur leurs spans HTTP sortants au lieu des noms OTel. Perf Sentinel lit ces deux tags en dernier recours, et seulement sur un span déjà classé comme appel sortant par son URL. Un status non numérique comme CLIENT_ERROR laisse le statut vide. Avec le starter, en OTLP, chaque span est rangé sous le scope org.springframework.boot, et par défaut aucun span ne porte d'attribut code.*, donc le scope ne nomme que le langage : un SELECT généré par Hibernate reçoit quand même le correctif JPA, car Hibernate 6 le signe de ses alias de table (d1_0.id), et les autres findings dont le correctif dépend du framework reçoivent le correctif Java générique. Le pont Zipkin ne porte aucun scope, donc ses findings gardent la seule suggestion générique. Un service qui ajoute des attributs code.* à ses spans, comme le décrit la section du starter, reçoit le correctif que désigne sa localisation de code.
Les attributs de code sont facultatifs. Un finding porte une localisation de code quand son span, ou en OTLP son plus proche ancêtre dans le même service (jusqu'à huit niveaux au-dessus), porte l'un d'eux. Les noms stables l'emportent sur les noms legacy, un code.function.name qualifié donne aussi le namespace, et en OTLP code.line.number n'est lu que comme entier : un numéro de ligne envoyé sous forme de chaîne est ignoré. Jaeger et Zipkin lisent les attributs de code sur le span d'I/O lui-même, et acceptent un numéro de ligne écrit sous forme de chaîne.
Les spans qui ne portent aucun attribut SQL, HTTP, RPC ou messaging sont ignorés : ce ne sont pas des opérations d'I/O. 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.
Le HTTP sortant est réservé au côté client. Un span dont le kind est SERVER ne devient jamais un appel HTTP sortant, même s'il porte http.url ou url.full. La convention stable ne pose url.full que sur les spans CLIENT, mais les instrumentations legacy posent aussi http.url sur le span de traitement entrant, et les admettre compterait chaque saut instrumenté deux fois en créditant un service d'appels qu'il n'a jamais émis. Cela reprend la règle CLIENT seul du RPC ci-dessous. Trois conséquences. Un span SERVER portant db.statement est toujours analysé, parce que le SQL est classé avant le HTTP. Un span dont le kind n'est jamais posé reste éligible au HTTP, une instrumentation qui omet le kind n'est donc pas affectée. Et un span SERVER rejeté fournit toujours son http.route comme endpoint entrant auquel les findings sont rattachés. Jaeger lit le tag span.kind (server), Zipkin le champ kind (SERVER).
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 key=value. Un cluster mutualisé où le namespace ne distingue pas les tenants peut grouper par tenant.id à la place, à 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 queSpanKind::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é commen_plus_one_httpet 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 masquée (voir Limites), ces appels sont lus comme
redundant_httpplutôt quen_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. Leur 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 redondant, car 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
CONSUMERdécrit un travail effectué sur un message reçu, pas un appel émis par le service, et un span messagingCLIENTest 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
CONSUMERest reporté sur les spans d'I/O du handler et rendu parexplainsous la formetriggered 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 sansdb.statement/db.query.text, un span HTTP sanshttp.url/url.full, ou un span SERVER dont l'URL décrit sa propre requête entrante ne donne aucun finding. Un rapport maigre ou vide peut donc signifier aucun problème ou aucune instrumentation exploitable. Lancezperf-sentinel inspectpour voir ce qui a été extrait, et voir La qualité de l'instrumentation borne les findings.
La stabilité des acks dépend dehttp.route. La signature d'acquittement repose sur le template de route, pas sur l'URL instanciée. Les services qui émettenthttp.route(Spring Boot avec l'agent Java, ASP.NET Core, Express, toute auto-instrumentation moderne) conservent des acks qui survivent aux redémarrages et aux ids de requête tournants. Le starter Spring Boot a besoin d'une convention serveur pour cela, voir Route entrante. Les services qui se rabattent surhttp.urlouurl.fullperdent 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 ordonnés 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.
Sur Spring Boot 4, le starter spring-boot-starter-opentelemetry est l'autre voie, par une dépendance Maven et sans agent. Il convient aux applications qui utilisent le cache AOT ou les images natives, ou qui ne doivent pas dépendre d'une instrumentation de bytecode alignée sur les versions de leurs bibliothèques, au prix de quelques ajouts décrits dans sa section. L'agent reste la voie la plus courte pour Spring Boot 3, pour les parcs mixtes, et pour les bibliothèques que les projets Spring n'observent pas.
1. Téléchargez l'agent
curl -L -o opentelemetry-javaagent.jar \
https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar2. Lancez votre application avec l'agent
export JAVA_TOOL_OPTIONS="-javaagent:/path/to/opentelemetry-javaagent.jar"
export OTEL_SERVICE_NAME=my-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.jarL'agent capture automatiquement :
- La requête SQL depuis JDBC (Spring Data JPA, Hibernate) et R2DBC (Spring WebFlux réactif), dans
db.statementpar défaut et dansdb.query.textavecotel.semconv-stability.opt-in=database. Perf Sentinel lit les deux url.fulldepuis les clients HTTP (WebClient, RestTemplate, HttpClient)http.routedepuis 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 détection N+1 sanitizer-aware fonctionne correctement.
3. Exemple Docker Compose
services:
my-service:
build: ./my-service
environment:
- JAVA_TOOL_OPTIONS=-javaagent:/app/opentelemetry-javaagent.jar
- OTEL_SERVICE_NAME=my-service
- OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4317
- OTEL_EXPORTER_OTLP_PROTOCOL=grpc
- OTEL_TRACES_SAMPLER=always_on
- OTEL_METRICS_EXPORTER=none
- OTEL_LOGS_EXPORTER=noneAjoutez le JAR de l'agent à votre Dockerfile :
ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar /app/opentelemetry-javaagent.jarSur une plateforme partagée
- Ne comptez pas sur un
JAVA_TOOL_OPTIONSdéfini dans l'image. Quand la plateforme définit elle-même la variable (l'environnement supplémentaire d'un chart Helm, un préréglage mémoire), elle remplace la valeur du Dockerfile et l'agent ne se charge jamais, sans erreur. Placez-javaagent:dans l'entrypoint, ou dans une variable que la plateforme ne gère pas, et vérifiez les flags de la JVM en cours d'exécution. - Configurez l'agent par de vraies variables d'environnement ou des propriétés système. Il démarre en
premain, avant Spring, donc les valeurs deapplication.properties, ou d'une ConfigMap montée comme tel, ne l'atteignent jamais. - Une seule image, l'agent activé par environnement. Embarquez le jar dans chaque image et activez-le avec
OTEL_JAVAAGENT_ENABLED, àfalsepar défaut et àtruelà où les traces sont voulues, plutôt que de construire une image par cas. - Laissez de la place dans le Metaspace. L'agent charge beaucoup de classes. Un
-XX:MaxMetaspaceSizedimensionné pour la seule application peut finir enOutOfMemoryError: Metaspacepeu après l'activation de l'agent. Augmentez-le et mesurez. - Un seul traceur par JVM. Si l'application a aussi un pont de tracing Micrometer, les spans HTTP et messaging sont émis deux fois. Désactivez le traceur applicatif là où l'agent tourne :
management.tracing.enabled=falsesur Spring Boot 3,management.tracing.export.enabled=falsesur Spring Boot 4.
Localisation du code. L'agent pose des attributs code.* sur ses spans de repository Spring Data, par défaut sous les noms legacy code.namespace et code.function, et sous le nom stable code.function.name avec otel.semconv-stability.opt-in=code. Perf Sentinel lit les deux, et en OTLP le SQL sous un span de repository hérite de sa localisation de code (voir Attributs de span requis). Les chargements paresseux, les flushs et les appels HTTP sortants n'ont pas d'ancêtre repository, donc leurs findings restent sans site d'appel, sauf si les spans de contrôleur sont activés avec otel.instrumentation.common.experimental.controller-telemetry.enabled=true, qui nomme alors la méthode du handler.
Limitations connues
Incompatibilité Project Leyden / AOT cache. Le flag -javaagent: est incompatible avec les AOT caches JEP 483 (-XX:AOTCache). Désactivez le cache AOT quand l'agent est actif :
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
fiLes clients sortants que l'agent voit, le starter peut ne pas les voir. L'agent instrumente la bibliothèque HTTP sous-jacente, donc chaque appel RestClient, RestTemplate ou WebClient est tracé, quelle que soit la façon dont le client a été construit. Avec le starter Spring Boot, seuls les clients construits à partir des builders auto-configurés le sont, voir Clients HTTP sortants.
Tests d'intégration en CI (Maven Failsafe)
La configuration ci-dessus suppose un processus qui tourne en continu et parle à un endpoint OTLP actif. 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.
Avant l'agent 2.32.0, Java n'a pas d'exporteur fichier, et une JVM de test forkée ne vous donne pas non plus sa sortie standard. Jusqu'au SDK 1.65, celui qu'embarque l'agent 2.31.1, aucun exporteur Java n'écrit les spans dans un fichier au chemin de votre choix. L'exporteur otlp_file/development de la configuration déclarative définit un champ output_stream: file://..., mais le SDK Java ne l'implémente qu'à partir de la 1.66, voir l'option 3 plus bas. Reste experimental-otlp/stdout, qui écrit du JSON OTLP sur System.out. Maven s'y interpose, car 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/<timestamp>-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.
Faute d'agent 2.32.0, les traces doivent donc quitter la JVM comme elles le font en production, par le réseau, et quelque chose doit écouter. perf-sentinel capture joue ce rôle et fonctionne de la même façon quelle que soit la version de l'agent.
Attachez l'agent à la JVM de test, pas seulement à l'image construite. Si les tests d'intégration tournent en process contre @SpringBootTest (Maven Failsafe, integrationTest Gradle) plutôt que contre le conteneur construit, 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 :
<!-- 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>my-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. Seul l'écouteur change.
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 :
perf-sentinel capture --output target/traces.json -- mvn verify
perf-sentinel analyze --ci --input target/traces.jsonsoit, quand l'étape de test ne peut pas être préfixée parce qu'elle appartient à votre pipeline, vous écoutez à côté :
perf-sentinel capture --output target/traces.json &
CAPTURE=$!
mvn verify
kill -TERM $CAPTURE && wait $CAPTURE
perf-sentinel analyze --ci --input target/traces.jsonPréfixez votre étape de test existante, n'en ajoutez jamais une seconde. capture -- mvn verify lance les tests une fois et 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.captureouvre le fichier avant de lancer la commande,cleansupprime ensuitetarget/sous lui, et le run se termine par une erreur nommant le fichier disparu plutôt que par un nombre de spans pour un inode qu'aucun chemin ne désigne. Retirezcleande 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, la même forme que celle de l'exporteur file du Collector, et la détection automatique de format le lit sans flag supplémentaire. 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 un 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, export fichier depuis l'agent (2.32.0 et suivants)
L'agent 2.32.0 est la première version à embarquer le SDK 1.66, où otlp_file/development implémente output_stream. Au moment de l'écriture (septembre 2026), seuls des builds 2.32.0-SNAPSHOT existent. Une valeur file:// fait écrire le fichier de traces par la JVM forkée elle-même, une requête OTLP par ligne, la forme NDJSON que analyze lit sans flag supplémentaire. Rien n'écoute sur un port, et le fork reste tel quel puisque le fichier ne passe jamais par le canal de commande.
L'exporteur n'est accessible que par la configuration déclarative. Placez un fichier comme otel-ci.yaml à côté du POM :
file_format: "1.1"
resource:
attributes:
- name: service.name
value: my-service
tracer_provider:
processors:
- simple:
exporter:
otlp_file/development:
output_stream: file://${TRACES_FILE}et pointez l'agent dessus depuis la configuration failsafe, à la place des variables OTEL_* ci-dessus :
<environmentVariables>
<OTEL_CONFIG_FILE>${project.basedir}/otel-ci.yaml</OTEL_CONFIG_FILE>
<TRACES_FILE>${project.build.directory}/traces.jsonl</TRACES_FILE>
</environmentVariables>rm -f target/traces.jsonl
mvn verify
perf-sentinel analyze --ci --input target/traces.jsonl- Le fichier est ouvert en mode ajout. Un second run sans
cleanajoute ses spans à ceux du premier, et chaque finding sort en double. Partez d'un fichier vide, comme le fait lerm -fci-dessus.mvn clean verifyconvient aussi, contrairement àcapture, puisque la JVM forkée ouvre le fichier après le passage declean. - Dès que
OTEL_CONFIG_FILEest défini, l'agent ignore toutes les variablesOTEL_*. Nom de service, sampler et exporteur viennent tous du YAML. - Un agent plus ancien échoue en silence. L'agent 2.31.1 avec la même configuration termine en
BUILD SUCCESS, n'écrit aucun fichier et dévie les spans vers le canal du fork. Épinglez 2.32.0 ou plus dans le blocmaven-dependency-pluginci-dessus.
Un test en échec laisse quand même un fichier complet et analysable.
Option 4, aucun fork
<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, mais elle a un coût. 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 et où l'agent est antérieur à 2.32.0.
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, donc 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 pas du tout, malgré leurs noms plausibles. Le vrai mécanisme est otlp_file/development avec output_stream (option 3).
Java (Spring Boot 4 starter, spring-boot-starter-opentelemetry)
Spring Boot 4.0 fournit son propre support OpenTelemetry, le starter spring-boot-starter-opentelemetry, et l'équipe Spring le présente comme son option préférée pour les applications Spring Boot. Il n'y a pas d'agent : les projets Spring créent les spans par Micrometer Observation, le pont de tracing Micrometer les transmet au SDK OpenTelemetry que Spring Boot configure, et le SDK les exporte en OTLP. Rien ne modifie le bytecode, donc le cache AOT et les images natives continuent de fonctionner, et l'instrumentation correspond toujours aux versions des bibliothèques qu'elle observe.
La contrepartie est la couverture. Le starter trace moins que l'agent par défaut, et ses spans suivent les conventions Spring plutôt que celles d'OpenTelemetry.
| Ce que lit Perf Sentinel | Par défaut | Ce qu'il faut ajouter |
|---|---|---|
Texte SQL (db.query.text) | Non, Spring Boot n'instrumente pas JDBC | datasource-micrometer-spring-boot et datasource-micrometer-opentelemetry (étape 1) |
| Appels HTTP sortants | Oui, pour les clients construits à partir des builders Spring Boot | Construire chaque client à partir du builder injecté (étape 3) |
Route entrante (http.route) | Non, le span serveur ne porte que le chemin brut | Une convention d'observation serveur (étape 4) |
| Publications RabbitMQ et Kafka | Non, l'observation est désactivée par défaut | Les propriétés observation-enabled (étape 2) |
SQL des méthodes @Async et des jobs | Non, l'observation ne suit pas le changement de thread | La propagation de contexte, un listener Quartz (étape 5) |
Localisation du code (code.*) | Seulement sur les méthodes @Scheduled | Des observations de repository et l'attribution du site d'appel (étape 6) |
Un seul traceur par JVM. Avec l'agent Java attaché à un service qui a aussi le starter, les spans HTTP et messaging sont émis deux fois. Partout où l'agent tourne, désactivez l'export du starter avec management.tracing.export.enabled=false.
1. Dépendances
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
<!-- Spring Boot n'instrumente pas JDBC : un span par requête. -->
<dependency>
<groupId>net.ttddyy.observation</groupId>
<artifactId>datasource-micrometer-spring-boot</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Place la requête dans db.query.text, l'attribut que lit Perf Sentinel. -->
<dependency>
<groupId>net.ttddyy.observation</groupId>
<artifactId>datasource-micrometer-opentelemetry</artifactId>
<version>2.3.0</version>
</dependency>La ligne 2.x de datasource-micrometer cible Spring Boot 4, la ligne 1.x Spring Boot 3. Gardez les deux modules sur la même version. Sans le module OpenTelemetry, la requête va dans un tag jdbc.query[0] que Perf Sentinel ne lit pas, donc les spans SQL sont ignorés. Avec lui, la requête est sanitisée avant l'export (? à la place des littéraux), ce que gère la détection N+1 sanitizer-aware.
2. Configuration
spring.application.name=my-service
# OTLP en HTTP/protobuf, le transport par défaut : port 4318 et chemin /v1/traces.
management.opentelemetry.tracing.export.otlp.endpoint=http://otel-collector:4318/v1/traces
# La valeur par défaut est 0.1, ce qui supprime la plupart des appels répétés dont dépend la détection N+1.
management.tracing.sampling.probability=1.0
# Un span par requête SQL, pas de span de connexion ni de result set.
jdbc.includes=query
# Les spans messaging et la propagation de trace dans les en-têtes de message sont désactivés par défaut.
spring.rabbitmq.listener.simple.observation-enabled=true
spring.rabbitmq.template.observation-enabled=true
spring.kafka.listener.observation-enabled=true
spring.kafka.template.observation-enabled=true
# Le starter pousse aussi les métriques en OTLP. Désactivez-le quand Prometheus les collecte.
management.otlp.metrics.export.enabled=false- Sans endpoint, pas d'export. L'endpoint n'a pas de valeur par défaut. Tant qu'il n'est pas défini, les spans sont créés et propagés mais ne quittent jamais la JVM.
service.namevient despring.application.name, etservice.namespacedespring.application.group. Ajoutez d'autres attributs de ressource, commek8s.namespace.namepourgrouping_attributes, avecmanagement.opentelemetry.resource-attributes.*.- La propagation produit uniquement le W3C
traceparentet accepte W3C et B3. Modifiez-la avecmanagement.tracing.propagation.produceetconsume. L'ancienne propriétémanagement.tracing.propagation.typeremplace les deux quand elle est définie, donc une valeur oubliée les écrase sans prévenir. - Variables
OTEL_*. Spring Boot 4.1 en reporte une partie sur ces propriétés, dontOTEL_EXPORTER_OTLP_TRACES_ENDPOINT,OTEL_EXPORTER_OTLP_ENDPOINT(avec/v1/tracesajouté),OTEL_SERVICE_NAME,OTEL_RESOURCE_ATTRIBUTES,OTEL_TRACES_SAMPLERetOTEL_PROPAGATORS. Les autres variablesOTEL_*sont ignorées. Spring Boot 4.0 ne lit queOTEL_SERVICE_NAMEetOTEL_RESOURCE_ATTRIBUTES. - Activer l'export par environnement.
management.tracing.export.otlp.enableddésactive l'export OTLP sans retirer l'instrumentation, donc le même artefact tourne partout et seul le déploiement décide où vont les traces.
3. Clients HTTP sortants
Un RestClient, RestTemplate ou WebClient n'est tracé, et n'envoie traceparent, que s'il porte l'ObservationRegistry. Spring Boot le pose sur les builders qu'il auto-configure. Un client construit par une fabrique statique n'a ni span ni en-têtes de trace, donc ses appels sont invisibles pour les détecteurs N+1 et fan-out, et le service en aval démarre une nouvelle trace.
@Bean
RestClient inventoryClient(RestClient.Builder builder) { // injecté, observé
return builder.baseUrl("http://inventory").build();
}
// Non tracé : RestClient.builder().baseUrl("http://inventory").build()
// Sauf si le registry est posé à la main : RestClient.builder().observationRegistry(registry)Ces spans nomment la méthode et le statut dans les tags Micrometer method et status plutôt que dans les attributs OpenTelemetry, et Perf Sentinel lit les deux formes.
4. Route entrante
La convention serveur par défaut ne pose la route que sur les métriques. Le span porte le chemin brut, donc les findings sont regroupés par URL instanciée, un endpoint par id, et les acquittements perdent leur stabilité. Deux façons de poser http.route sur le span :
- Déclarer un bean
OpenTelemetryServerRequestObservationConvention. Le span suit alors les conventions HTTP d'OpenTelemetry, de même que les métriques HTTP serveur, renomméeshttp.server.request.duration. - Étendre la convention par défaut et ajouter la route comme clé à haute cardinalité. Les clés à haute cardinalité ne vont que dans les traces, donc les métriques Prometheus gardent leurs noms et leurs tags.
@Bean
ServerRequestObservationConvention routeConvention() {
return new DefaultServerRequestObservationConvention() {
@Override
public KeyValues getHighCardinalityKeyValues(ServerRequestObservationContext context) {
KeyValues keyValues = super.getHighCardinalityKeyValues(context);
String route = context.getPathPattern();
return route == null ? keyValues : keyValues.and("http.route", route);
}
};
}5. Threads et jobs
Une requête n'appartient à une trace que si une observation est courante sur son thread quand elle s'exécute.
- Méthodes
@Async. Sur Spring Boot 4.1, définissezspring.task.execution.propagate-context=truepour l'executor auto-configuré. Sur Spring Boot 4.0, ou pour un executor que vous construisez vous-même, enregistrez unContextPropagatingTaskDecorator. Capturer tous les contextes transmet aussi le contexte de sécurité de l'appelant à la tâche. Pour ne propager que l'observation :java@Bean TaskDecorator observationOnlyTaskDecorator() { return new ContextPropagatingTaskDecorator(ContextSnapshotFactory.builder() .captureKeyPredicate(ObservationThreadLocalAccessor.KEY::equals) .build()); } - Les méthodes
@Scheduledreçoivent une observation automatiquement. - Les jobs Quartz n'en reçoivent aucune, et
@Observedsur une classe de job ne fait rien car Quartz instancie les jobs hors de Spring AOP. Enregistrez unJobListenerglobal par unSchedulerFactoryBeanCustomizer: ouvrez un scope d'observation dansjobToBeExecutedet fermez-le dansjobWasExecuted, puisque les deux s'exécutent sur le thread du job.
Le SQL exécuté hors de toute observation (démarrage, migrations, un check-in de cluster) devient une trace racine par requête. Cela coûte du volume et ajoute des requêtes sur lesquelles on ne peut pas agir. Un ObservationPredicate peut écarter les observations jdbc.* qui démarrent sans observation courante sur le thread. Regardez l'observation courante plutôt que le parent : avec jdbc.includes=query, le parent d'une requête est l'observation de connexion, un no-op.
@Bean
ObservationPredicate skipSqlOutsideAnyObservation(ObjectProvider<ObservationRegistry> registry) {
return (name, context) -> !name.startsWith("jdbc.")
|| registry.getObject().getCurrentObservation() != null;
}6. Localisation du code
Le starter ne pose aucun attribut code.* sur les spans HTTP ou SQL, donc les findings arrivent sans site d'appel, et le correctif suggéré Java ne repose que sur le scope org.springframework.boot et la forme du SQL (voir Attributs de span requis). Perf Sentinel lit d'abord les noms stables code.function.name, code.file.path et code.line.number, puis les noms legacy. En OTLP, un span d'I/O sans attribut code.* prend la localisation de code de son plus proche ancêtre qui en a une, jusqu'à huit niveaux au-dessus dans le même service. Deux ajouts couvrent la plupart des spans d'I/O.
Un span par appel de repository. Spring Data ne crée aucun span pour une méthode de repository. Un MethodInterceptor ajouté à chaque proxy de repository peut ouvrir une observation portant code.function.name, et le SQL émis par la méthode se range dessous. Ajoutez l'advice en position 0 pour que le span couvre aussi le flush quand le repository valide sa propre transaction.
@Bean
static BeanPostProcessor repositoryObservation(ObjectProvider<ObservationRegistry> registry) {
return new BeanPostProcessor() {
@Override
public Object postProcessBeforeInitialization(Object bean, String name) {
if (bean instanceof RepositoryFactoryBeanSupport<?, ?, ?> factoryBean) {
factoryBean.addRepositoryFactoryCustomizer(factory -> factory.addRepositoryProxyPostProcessor(
(proxy, info) -> proxy.addAdvice(0, (MethodInterceptor) invocation -> {
ObservationRegistry r = registry.getIfAvailable(() -> ObservationRegistry.NOOP);
if (r.getCurrentObservation() == null) {
return invocation.proceed(); // aucune trace à laquelle se rattacher
}
String function = info.getRepositoryInterface().getName() + "." + invocation.getMethod().getName();
return Observation.createNotStarted("repository.invocation", r)
.highCardinalityKeyValue("code.function.name", function)
.observeChecked(invocation::proceed);
})));
}
return bean;
}
};
}Le site d'appel depuis la pile. Les spans de repository laissent de côté les chargements paresseux, l'initialisation des proxies et chaque appel HTTP sortant. La méthode applicative responsable est encore sur la pile quand le span se termine, car les filtres d'observation s'exécutent sur le thread appelant à l'arrêt de l'observation. Un ObservationFilter sur le QueryContext de datasource-micrometer et sur le ClientRequestObservationContext de Spring peut parcourir la pile avec StackWalker et garder la première frame qui appartient à l'application. Trois règles gardent le résultat fiable :
- S'arrêter aux frontières. Si un appel de repository apparaît en premier sur la pile, laissez le span tel quel : son ancêtre repository nomme déjà le code. Si le flush d'Hibernate (
org.hibernate.engine.spi.ActionQueue.executeActions) ou un commit (AbstractPlatformTransactionManager.processCommit) apparaît en premier, la pile ne dit pas où l'entité a changé, donc ne posez rien. - Reconnaître les classes applicatives par leur emplacement de chargement, pas par leur package. Quand le service et ses bibliothèques partagées ont le même package racine, comparez le
CodeSourceduProtectionDomainde chaque classe avec celui de la classe@SpringBootConfiguration, ignorez les sous-classes générées (proxies$$, proxies Hibernate) et mettez la réponse en cache par classe avecClassValue. Si l'emplacement de l'application est inconnu, ne traitez aucune classe comme du code applicatif : les classes du JDK n'ont pas non plus deCodeSource. - Écrire le numéro de ligne comme un entier. Les valeurs de clé d'observation sont des chaînes, et Perf Sentinel ne lit
code.line.numberque comme entier en OTLP. Posez les attributs sur le span de tracing lui-même, par leTracingObservationHandler.TracingContextdu contexte d'observation :javaTracingObservationHandler.TracingContext tracing = context.get(TracingObservationHandler.TracingContext.class); Span span = tracing == null ? null : tracing.getSpan(); if (span == null || span.isNoop()) { return context; // non tracé, ou non échantillonné : pas de parcours de pile } // frame : la première frame applicative trouvée par StackWalker span.tag("code.function.name", frame.getClassName() + "." + frame.getMethodName()); span.tag("code.file.path", frame.getFileName()); span.tag("code.line.number", (long) frame.getLineNumber());
Avec les deux en place, la plupart des findings SQL et HTTP d'un service sous starter portent une localisation de code, davantage qu'avec l'agent par défaut, qui ne nomme que les méthodes de repository. Deux limites demeurent. Un helper HTTP générique du service devient la localisation rapportée de chaque appel qui passe par lui, et un getter d'entité qui déclenche un chargement paresseux est rapporté à la place de son appelant. Le parcours de pile coûte quelques microsecondes par appel d'I/O tracé.
7. Tests d'intégration
@SpringBootTest tourne dans la JVM de Failsafe, donc le starter trace les tests d'intégration sans agent à attacher. Pointez le fork vers perf-sentinel capture par des propriétés système :
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<management.opentelemetry.tracing.export.otlp.endpoint>http://localhost:4318/v1/traces</management.opentelemetry.tracing.export.otlp.endpoint>
<management.tracing.export.otlp.enabled>true</management.tracing.export.otlp.enabled>
<management.tracing.sampling.probability>1.0</management.tracing.sampling.probability>
</systemPropertyVariables>
</configuration>
</plugin>perf-sentinel capture --grace-ms 3000 --output target/traces.json -- mvn verify
perf-sentinel analyze --ci --input target/traces.jsonSans écouteur, l'export échoue en arrière-plan sans faire échouer de test, donc cette configuration peut rester dans le POM. Le délai de grâce laisse au batch span processor le temps de vider son tampon après le dernier test.
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 :
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>Configurez dans application.properties :
quarkus.otel.exporter.otlp.endpoint=${OTLP_GRPC_ENDPOINT:http://localhost:4317}
quarkus.otel.exporter.otlp.protocol=grpc
quarkus.otel.service.name=my-service
quarkus.otel.enabled=${OTEL_ENABLED:false}
quarkus.otel.metrics.exporter=none
quarkus.otel.logs.exporter=noneActivez 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 (par exemple QUARKUS_OTEL_EXPORTER_OTLP_ENDPOINT).
.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.
<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.
var otlpEndpoint = Environment.GetEnvironmentVariable("OTLP_GRPC_ENDPOINT");
if (!string.IsNullOrEmpty(otlpEndpoint))
{
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("my-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()avecOpenTelemetry.Instrumentation.EntityFrameworkCore - SqlClient (SQL Server) :
.AddSqlClientInstrumentation()avecOpenTelemetry.Instrumentation.SqlClient
Les deux instrumentations capturent toujours le texte des requêtes, avec les valeurs littérales remplacées par ?. L'option SetDbStatementForText que posaient les anciens guides a été supprimée (SqlClient 1.12.0-beta.3, Entity Framework Core 1.13.0-beta.1) et ne compile plus. SqlClient place le texte dans db.query.text. Entity Framework Core le place dans db.statement par défaut, ou dans db.query.text avec OTEL_SEMCONV_STABILITY_OPT_IN=database. Perf Sentinel lit les deux. Un appel de procédure stockée SqlClient porte db.stored_procedure.name et aucun texte de requête, donc Perf Sentinel ne peut pas l'analyser.
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 détecter des requêtes répétées comme redundant_sql (même template, mêmes params visibles) plutôt que n_plus_one_sql (même template, params différents).
Le url.full sortant a sa query string masquée 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. Qui masque dépend du framework cible. À partir de .NET 9, l'activité System.Net.Http du runtime pose elle-même url.full et remplace la query par *. Pour la conserver, posez DOTNET_SYSTEM_NET_HTTP_DISABLEURIREDACTION=true ou le switch AppContext System.Net.Http.DisableUriRedaction (changement cassant de .NET 9). Avant .NET 9, c'est le paquet OpenTelemetry.Instrumentation.Http qui masque, et OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION=true conserve la query (README du paquet). Les deux réglages exposent aussi les secrets que la query transporte. L'autre option est de modéliser l'identifiant variable comme un segment de chemin (/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 enveloppement explicite plutôt que l'auto-instrumentation. HTTP et SQL nécessitent chacun une bibliothèque dédiée.
Dépendances (go.mod) :
go.opentelemetry.io/otel
go.opentelemetry.io/otel/sdk
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
github.com/exaring/otelpgxInstrumentation du serveur HTTP :
mux := http.NewServeMux()
mux.HandleFunc("/api/orders", handleOrders)
// Enveloppe le mux avec le middleware HTTP OTel
handler := otelhttp.NewHandler(mux, "server",
otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string {
return r.Method + " " + r.URL.Path
}),
)
http.ListenAndServe(":8080", handler)Instrumentation SQL avec pgx :
cfg, _ := pgxpool.ParseConfig(os.Getenv("DB_DSN"))
cfg.ConnConfig.Tracer = otelpgx.NewTracer()
pool, _ := pgxpool.NewWithConfig(ctx, cfg)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 détection N+1 sanitizer-aware. Aucune configuration supplémentaire n'est nécessaire.
Variables d'environnement (exemple Docker Compose) :
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
OTEL_SERVICE_NAME: go-svcPython (Django 5.x + psycopg, OTel SDK 1.42)
Les applications Django utilisent les packages d'auto-instrumentation pour le HTTP comme pour le SQL.
Dépendances (requirements.txt) :
opentelemetry-sdk
opentelemetry-exporter-otlp-proto-grpc
opentelemetry-instrumentation-django
opentelemetry-instrumentation-psycopgInitialisation (manage.py ou wsgi.py) :
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.django import DjangoInstrumentor
from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider) # sans cela, les instrumentations n'exportent rien
DjangoInstrumentor().instrument()
PsycopgInstrumentor().instrument()psycopg émet db.statement avec les placeholders Python DB-API %s. Perf Sentinel reconnaît %s comme placeholder de driver, donc le chemin de détection N+1 sanitizer-aware se déclenche sans configuration supplémentaire.
Variables d'environnement :
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_SERVICE_NAME: django-svcPython (FastAPI + SQLAlchemy 2.x + asyncpg, OTel SDK 1.42)
FastAPI avec SQLAlchemy utilise les packages d'auto-instrumentation. SQLAlchemy figure dans la liste d'autorisation des scopes ORM, donc le chemin de détection sanitizer-aware le reconnaît comme une stack pilotée par un ORM.
Dépendances (requirements.txt) :
opentelemetry-sdk
opentelemetry-exporter-otlp-proto-grpc
opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-sqlalchemy
opentelemetry-instrumentation-asyncpgInitialisation (main.py) :
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider) # sans cela, les instrumentations n'exportent rien
FastAPIInstrumentor.instrument_app(app)
SQLAlchemyInstrumentor().instrument(engine=engine.sync_engine) # un AsyncEngine s'instrumente via son moteur synchroneasyncpg émet db.statement avec les paramètres positionnels PostgreSQL natifs ($1, $2). Perf Sentinel les normalise en $? avec des params vides. Le scope d'instrumentation sqlalchemy figure dans la liste d'autorisation des scopes ORM, donc la détection N+1 sanitizer-aware se déclenche via le chemin ORM pour cette stack.
Variables d'environnement :
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_SERVICE_NAME: fastapi-svcNode.js (Nest.js + Prisma, OTel SDK 0.57)
Les applications Nest.js utilisent le package @opentelemetry/sdk-node avec des instrumentations propres au framework. Prisma génère le SQL et le client pg l'envoie.
Dépendances (package.json) :
{
"@opentelemetry/sdk-node": "^0.57",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.57",
"@opentelemetry/instrumentation-http": "^0.57",
"@opentelemetry/instrumentation-pg": "^0.44"
}Initialisation (tracing.ts, chargé via --require) :
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(),
instrumentations: [
new HttpInstrumentation(),
new PgInstrumentation({ enhancedDatabaseReporting: true }),
],
});
sdk.start();PgInstrumentation avec enhancedDatabaseReporting: true émet db.statement avec la requête SQL complète, valeurs de paramètres résolues comprises. Le scope d'instrumentation prisma figure dans la liste d'autorisation des scopes ORM, donc la détection sanitizer-aware se déclenche via le chemin ORM.
Variables d'environnement :
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_SERVICE_NAME: nest-svc
NODE_OPTIONS: --require ./tracing.jsRust (tracing-opentelemetry 0.31, 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.
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "registry"] }
tracing-opentelemetry = "0.31"
opentelemetry = { version = "0.30", features = ["trace"] }
opentelemetry_sdk = { version = "0.30", features = ["rt-tokio", "trace"] }
opentelemetry-otlp = { version = "0.30", features = ["grpc-tonic"] }use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::WithExportConfig;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint("http://127.0.0.1:4317")
.build()
.expect("failed to create OTLP exporter");
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.build();
let tracer = provider.tracer("my-service");
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(otel_layer)
.init();Pour les applications Rust utilisant Diesel ou SeaORM, le crate ORM émet le SQL directement dans le span tracing. Ajoutez db.statement et db.system à vos spans de requête à la main ou via l'intégration tracing de l'ORM. diesel et sea-orm figurent tous deux dans la liste d'autorisation des scopes ORM.
let _span = tracing::info_span!("db.query",
db.statement = "SELECT * FROM player WHERE game_id = 42",
db.system = "postgresql"
);Ruby (Rails + ActiveRecord, opentelemetry-ruby)
Les applications Rails utilisent les gems d'instrumentation opentelemetry-ruby. L'instrumentation ActiveRecord fournit le scope ORM, et l'instrumentation du driver sous-jacent (pg, mysql2) émet le db.statement SQL.
Dépendances (Gemfile) :
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) :
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 }
endL'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 apparaît 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 enregistrements (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 se rabattent sur le fix ruby_generic.
Variables d'environnement :
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_SERVICE_NAME: rails-svcPHP (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) :
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-doctrineCorrespondance des frameworks.
- Laravel/Eloquent : le span SQL feuille porte le scope PDO, mais le scope applicatif
io.opentelemetry.contrib.php.laravelapparaît dans la chaîne de spans, donc les findings portent des fixesphp_laravel_eloquent(eager loadingwith()/load()) sur tous les anti-patterns. - Symfony/Doctrine : le scope
io.opentelemetry.contrib.php.doctrineest émis directement sur le span SQL (DBAL est instrumenté), donc les findings SQL portent des fixesphp_doctrine(fetch-join DQL). Une application Symfony qui utilise du PDO brut au lieu de Doctrine se rabat surphp_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 :
environment:
OTEL_PHP_AUTOLOAD_ENABLED: "true"
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_SERVICE_NAME: php-svcStyles de placeholders SQL et détection
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 convertit en $? ou ? dans le template normalisé, avec params vide pour les requêtes paramétrées. Cela active le chemin de détection N+1 sanitizer-aware (qui exige params == [] et un placeholder reconnu dans le template).
| Placeholder | Produit par | Normalisé en | Exemple |
|---|---|---|---|
? | 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, $2 | PostgreSQL natif (pgx, asyncpg, sqlx, node-pg) | $? | WHERE id = $? |
%s | Python 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 |
:name | Oracle, SQLAlchemy named | :name (conservé) | WHERE id = :oid |
Ce que cela signifie pour les opérateurs. Aucune configuration n'est nécessaire pour activer la détection sur ces stacks. Le normalizer et la vérification template_has_placeholder dans le pipeline de détection gèrent la correspondance automatiquement. La détection exige en revanche que l'instrumentation OTel émette db.statement sur les spans SQL. Si db.statement est absent (certaines instrumentations l'omettent par défaut pour des raisons de sécurité), Perf Sentinel ne peut pas détecter les anti-patterns SQL. Consultez la documentation de votre bibliothèque d'instrumentation pour activer la capture des requêtes.
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 comme des instrumentations de niveau ORM, ce qui renforce la probabilité qu'une requête paramétrée répétée soit une itération de boucle plutôt qu'un motif de préchauffage de cache :
spring-data, hibernate, jpa, micronaut-data, jdbi, r2dbc, entityframeworkcore, entity-framework, sqlalchemy, django, active-record, activerecord, gorm, sequelize, prisma, typeorm, mongoose, sea-orm, diesel.
Les stacks sans scope ORM (driver seul : otelpgx, asyncpg, node-pg, psycopg sans Django/SQLAlchemy) s'appuient plutôt sur les signaux de variance temporelle et de forte occurrence. Voir 04 · Détection pour l'algorithme de classification complet.