Comment utiliser le traçage micrométrique dans Spring Boot 3 WebFlux avec Kotlin réactif
Le traçage est un excellent outil pour observer les détails de votre système logiciel. Lorsqu’elle est effectuée correctement, elle permet au développeur de savoir quand, où et comment les différentes interactions se sont produites dans et entre vos applications. Cela facilite grandement l’observation de systèmes logiciels complexes.
Commençant par Botte de printemps 3l’ancien Détective des nuages printaniers solution de traçage dans Spring Boot sera remplacée par la nouvelle Traçage au micromètre une bibliothèque.
Vous savez peut-être déjà Micromètre car il était auparavant utilisé comme solution par défaut pour exposer des métriques et une surveillance indépendantes de la plate-forme pour les microservices basés sur JVM (par exemple, Prométhée). Le plus récent ajout étend l’écosystème Micrometer avec une solution de traçage indépendante de la plate-forme. Cela permet au développeur d’utiliser une API générique pour instrumenter son application et l’exporter dans différents formats vers des collecteurs de traçage tels que Jaeger, Zipkin, ou alors OpenTelemetry.
Table of Contents2. Testing
3. Problems
Le code source complet de l’exemple de service présenté dans cet article est disponible ici.
Dans ce qui suit, nous allons créer un microservice Spring Boot simple qui fournit un point de terminaison REST réactif qui interroge en interne un autre service tiers pour obtenir des informations. L’objectif est d’exporter les traces connectées pour les deux opérations.
Nous allons commencer par le projet Spring Boot Initializr suivant, que vous pouvez trouver ici. Il comprend Spring Boot 3.0.1 avec Kotlin Gradle DSL, Ressort Web réactif (WebFlux), et Spring Actuator avec Prometheus. Même si nous utilisons Kotlin dans cet article, la plupart des approches sont les mêmes si vous choisissez plutôt Java.
Définition du point final
Nous allons commencer par ajouter une simple classe de contrôleur REST avec un point de terminaison de test qui appelle une API externe avec Client Web de printemps. Nous utilisons le suspend
mot-clé pour utiliser Kotlin Coroutines. Cela nous permet d’écrire du code impératif tout en utilisant les flux réactifs de Spring WebFlux.
Dans l’exemple suivant, nous appelons une API TODO externe avec Spring WebClient, qui renvoie un élément TODO sous forme de chaîne JSON. Nous allons également créer un message de journal, qui contiendra ultérieurement des informations de traçage.
@RestController
class Controller {
val log = LoggerFactory.getLogger(javaClass)val webClient = WebClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.build()
@GetMapping("/test")
suspend fun test(): String {
// simulate some complex calculation
delay(1.seconds)
log.info("test log with tracing info")
// make web client call to external API
val externalTodos = webClient.get()
.uri("/todos/1")
.retrieve()
.bodyToMono(String::class.java)
.awaitSingle()
return externalTodos
}
}
Ajout du traçage micrométrique
Dans l’étape suivante, nous ajouterons des dépendances Micrometer Tracing à notre build.gradle.kts
dossier. Comme Micrometer prend en charge différents formats de traçage et fournisseurs, les dépendances sont divisées, nous n’importons donc que ce dont nous avons besoin. Pour synchroniser toutes les dépendances, nous utilisons la nomenclature Micrometer Tracing (Nomenclature). De plus, nous ajoutons la dépendance principale et un pont pour traduire les traces micrométriques au format OpenTelemetry (d’autres formats sont également disponibles).
implementation(platform("io.micrometer:micrometer-tracing-bom:1.0.0"))
implementation("io.micrometer:micrometer-tracing")
implementation("io.micrometer:micrometer-tracing-bridge-otel")
Nous devons également ajouter une dépendance d’exportateur pour exporter les traces créées. Pour cet exemple, nous utiliserons l’exportateur Zipkin géré par OpenTelemetry et pris en charge par Micrometer Tracing.
implementation("io.opentelemetry:opentelemetry-exporter-zipkin")
Configuration
La dernière chose qui manque pour configurer le traçage est la configuration. Il peut être créé comme un ressort application.yaml
situé sous src/main/resources
.
Tout d’abord, nous devons activer le traçage dans les paramètres de gestion. Nous allons également définir le taux d’échantillonnage de suivi sur 1 (la valeur par défaut est 0,1) pour créer des suivis pour chaque appel reçu par le service. Dans un système de production avec de nombreuses demandes, vous souhaiterez peut-être suivre uniquement certains appels. De plus, nous pouvons définir l’URL du point de terminaison où nous voulons que l’exportateur Zipkin envoie les traces.
Enfin, nous devons mettre à jour le modèle de journalisation par défaut pour inclure les ID de traçage et d’étendue.
management:
tracing:
enabled: true
sampling.probability: 1.0zipkin.tracing.endpoint: http://localhost:9411/api/v2/spans
logging.pattern.level: "trace_id=%mdc{traceId} span_id=%mdc{spanId} trace_flags=%mdc{traceFlags} %p"
Maintenant que nous avons configuré notre service, nous pouvons l’exécuter. Si vous démarrez l’application, le serveur doit, par défaut, démarrer sous le port 8080
. Vous pouvez ensuite appeler le point de terminaison que nous avons créé en ouvrant http://localhost:8080/test
. Vous devriez obtenir une réponse qui ressemble à ceci :
{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }
Pour afficher la trace réelle qui a été créée lors de l’appel du point de terminaison, nous devons les collecter et les afficher. Pour ce tutoriel, nous utiliserons Zipkin. Cependant, on pourrait également utiliser d’autres systèmes, par exemple, Grafana Loki ou alors Datadog.
Nous pouvons démarrer une instance Zipkin localement via Docker avec la commande suivante :
docker run -p 9411:9411 openzipkin/zipkin
Pour vérifier que Zipkin est en cours d’exécution, ouvrez dans votre navigateur. Cela devrait vous montrer l’interface utilisateur Zipkin.
Vous pouvez maintenant appeler à nouveau le point de terminaison de notre service Spring Boot. Après cela, lorsque vous recherchez des traces dans Zipkin, vous devriez pouvoir trouver la trace de la demande de point de terminaison.
A première vue, tout semble bien fonctionner. Cependant, nous avons deux problèmes.
Données manquantes dans les journaux
Si nous examinons nos journaux d’application, nous pouvons repérer le message de journal émis lorsque notre point de terminaison est appelé.
trace_id= span_id= trace_flags= INFO 43636 --- [DefaultExecutor] com.example.tracing.Controller : test log with tracing info
Comme tu peux le voir, trace_id
et span_id
ne sont pas définis. En effet, Micrometer Tracing ne peut pas encore gérer facilement le contexte de traçage dans les flux réactifs. De plus, le wrapper Kotlin Coroutine autour des flux réactifs masque le contexte de traçage. Par conséquent, nous devons différer le contexte du flux réactif actuel pour obtenir des informations de traçage. En pratique, cela ressemble à ceci :
Mono.deferContextual { contextView ->
ContextSnapshot.setThreadLocalsFrom(
contextView,
ObservationThreadLocalAccessor.KEY
).use {
log.info("test log with tracing info")
Mono.empty<String>()
}
}.awaitSingleOrNull()
Pour rendre les choses un peu plus pratiques, nous pouvons extraire le code passe-partout dans une fonction distincte.
@GetMapping("/test")
suspend fun test(): String {
// ...
observeCtx { log.info("test log with tracing info") }
// ...
}suspend inline fun observeCtx(crossinline f: () -> Unit) {
Mono.deferContextual { contextView ->
ContextSnapshot.setThreadLocalsFrom(
contextView,
ObservationThreadLocalAccessor.KEY
).use {
f()
Mono.empty<Unit>()
}
}.awaitSingleOrNull()
}
Si nous démarrons l’application maintenant et appelons notre point de terminaison, nous devrions pouvoir voir le trace_id
dans les journaux.
trace_id=6c0053eba01199f194f5f76ff8d61917 span_id=967d591266756905 trace_flags= INFO 45139 --- [DefaultExecutor] com.example.tracing.Controller : test log with tracing info
Appel WebClient non tracé
Le deuxième problème peut être repéré en regardant les traces dans Zipkin. Il affiche uniquement la trace parent du point de terminaison, mais pas d’étendue enfant pour le WebClient
appel. En théorie, Spring WebClient, ainsi que RestTemplate, sont instrumentés automatiquement par Micrometer. Cependant, si nous regardons le code, nous utilisons une méthode de construction statique pour WebClient
. Pour obtenir un traçage automatisé de WebClient
, nous devons utiliser un bean builder fourni par Spring Framework. Il peut être injecté via le constructeur de notre Controller
classer.
@RestController
class Controller(
webClientBuilder: WebClient.Builder
) {val webClient = webClientBuilder // use injected builder
.baseUrl("https://jsonplaceholder.typicode.com")
.build()
// ...
}
Si nous appelons à nouveau notre point de terminaison avec ce changement, nous pouvons maintenant voir le WebClient
‘s span à Zipkin. Micrometer Tracing ajoutera également automatiquement des en-têtes de suivi pour l’appel sortant contenant le trace_id
. Par exemple, si nous avions appelé un autre microservice instrumenté avec le traçage, il pourrait récupérer l’ID et envoyer des informations supplémentaires à Zipkin.
Micrometer Tracing fait beaucoup automatiquement pour nous au printemps. Cependant, nous pouvons parfois souhaiter ajouter des informations spécifiques à la durée de trace ou observer une partie spécifique de notre application qui n’est pas un appel entrant ou sortant.
Ajout de balises span
Nous pouvons définir des balises personnalisées et les ajouter à l’observation actuelle pour améliorer les données de traçage. Pour récupérer la trace courante, on peut utiliser un bean de la classe ObservationRegistry
. Semblable au problème de journalisation, nous devons utiliser notre fonction wrapper pour obtenir le contexte correct.
@GetMapping("/test")
suspend fun test(): String {observeCtx {
val currentObservation = observationRegistry.currentObservation
currentObservation?.highCardinalityKeyValue("test_key", "test sample value")
}
// ...
}
Après avoir ajouté ce code, nous pouvons voir notre balise personnalisée et sa valeur dans le Zipkin.
Observation personnalisée
La création d’une observation personnalisée (étendue) avec l’API Micrometer est généralement facile. Cependant, nous avons besoin d’aide pour tracer le contexte lorsque nous travaillons avec des flux réactifs et des coroutines. Si nous créons une nouvelle observation dans notre gestionnaire de points de terminaison, elle sera considérée comme une trace distincte. Pour rendre le code réutilisable, nous pouvons écrire une simple fonction wrapper pour créer une nouvelle observation. Cela fonctionne de la même manière que la fonction wrapper que nous avons créée précédemment pour écrire des journaux avec trace_id
.
suspend fun runObserved(
name: String,
observationRegistry: ObservationRegistry,
f: suspend () -> Unit
) {
Mono.deferContextual { contextView ->
ContextSnapshot.setThreadLocalsFrom(
contextView,
ObservationThreadLocalAccessor.KEY
).use {
val observation = Observation.start(name, observationRegistry)
Mono.just(observation).flatMap {
mono { f() }
}.doOnError {
observation.error(it)
observation.stop()
}.doOnSuccess {
observation.stop()
}
}
}.awaitSingleOrNull()
}
La fonction peut envelopper n’importe quelle fonction de suspension autour d’une nouvelle observation. Il arrêtera automatiquement l’observation une fois que la fonction donnée aura été exécutée. De plus, nous garderons une trace de toutes les erreurs qui pourraient survenir et les attacherons à la trace.
Nous pouvons maintenant appliquer cette fonction pour observer n’importe quel code, par exemple, l’exécution du delay
une fonction.
@GetMapping("/test")
suspend fun test(): String {runObserved("delay", observationRegistry) {
delay(1.seconds)
}
// ....
}
Après avoir ajouté ce code au gestionnaire de point de terminaison, Zipkin nous montrera une étendue personnalisée pour l’opération.
En cas d’erreur, cela ressemblerait à ce qui suit dans Zipkin.
Une application Spring Boot typique se connecte souvent à une base de données dans une application réelle. Pour utiliser la pile réactive, il est recommandé d’utiliser le R2DBC API au lieu de JDBCName.
Étant donné que Micrometer Tracing est assez nouveau, aucun traçage automatisé n’est actuellement disponible. Cependant, l’équipe Spring envisage de créer une configuration automatique. Le dépôt expérimental se trouve ici.
Pour notre projet, nous allons ajouter les dépendances suivantes au build.gradle.kts
. Pour faciliter la configuration des tests, nous n’utiliserons pas une vraie base de données mais un Base de données en mémoire H2 au lieu.
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
runtimeOnly("com.h2database:h2")
runtimeOnly("io.r2dbc:r2dbc-h2")// R2DBC micrometer auto tracing
implementation("org.springframework.experimental:r2dbc-micrometer-spring-boot:1.0.2")
À notre code Kotlin, nous ajoutons un référentiel CRUD simple avec prise en charge de la coroutine. Voici à quoi cela ressemble :
@Table("todo")
data class ToDo(
@Id
val id: Long = 0,
val title: String,
)interface ToDoRepository : CoroutineCrudRepository<ToDo, Long>
@RestController
class Controller(
val todoRepo: ToDoRepository,
// ...
) {
@GetMapping("/test")
suspend fun test(): String {
// ...
// Sample traced DB call
val dbtodos = todoRepo.findAll().toList()
// ...
return "${dbtodos.size} $externalTodos"
}
}
L’appel de notre point de terminaison entraînera l’ajout d’une étendue supplémentaire. La nouvelle travée, nommée query
contient plusieurs balises, y compris la requête SQL exécutée par Données de printemps R2DBC.
Micrometer et la nouvelle extension de traçage unifient la pile d’observabilité pour Spring Boot 3 et les versions ultérieures. Il donne une grande abstraction sur les différentes solutions de traçage utilisées dans différentes entreprises et leurs piles. Cela simplifie donc le travail de nous développeurs.
En termes de programmation réactive avec Spring WebFlux, il y a encore un potentiel d’amélioration, notamment avec Kotlin. L’équipe Micrometer est en pourparlers actifs avec l’équipe derrière le Réacteur de projet (bibliothèque réactive utilisée par Spring WebFlux) pour simplifier l’utilisation de Micrometer Tracing pour la pile réactive.