Le traçage, un composant critique, suit les demandes à travers des systèmes complexes. Cette visibilité révèle les embouts de verre et les erreurs, permettant des résolutions plus rapides. Dans une précédente publication de notre série sur les services Web Go, nous avons exploré l’importance de l’observabilité. Aujourd’hui, nous nous concentrons sur le traçage. Jaeger collecte, stocke et visualise les trajets de systèmes distribués. Il fournit des insights cruciaux sur les flux de demandes entre les services. En intégrant Jaeger avec OpenTelemetry, les développeurs peuvent unifier leur approche de traçage, garantissant une visibilité cohérente et complète. Cette intégration simplifie la diagnose des problèmes de performance et améliore la fiabilité du système. Dans ce post, nous allons configurer Jaeger, l’intégrer à OpenTelemetry dans notre application et explorer la visualisation des trajets pour des aperçus plus profonds.
Motivation
Ce que nous cherchons à réaliser est un tableau de bord Jaeger qui ressemble à ceci:
Lorsque nous naviguons dans différentes parties de l’application (sur le frontend Onehub), les trajets des diverses demandes sont collectés (à partir du moment où elles atteignent le grpc-gateway) avec une synthèse de chacun d’entre eux. Nous pouvons même approfondir un des trajets pour une vue plus détaillée. Considérez la première POST
demande (pour créer/envoyer un message dans un sujet):
Ici, nous voyons tout les composants que la demande Create
touche ainsi que leurs temps d’entrée/de sortie et le temps mis à l’intérieur et à l’extérieur des méthodes. Très puissant, en effet.
Pour commencer
TL;DR : Pour voir cela en action et valider le reste du blog :
- La source de cela se trouve dans le branche PART11_TRACING.
- Construisez tout ce qui est nécessaire (une fois que vous avez vérifié la branche) :
make build
- Nous avons divisé le docker-compose en deux parties (cela sera expliqué plus bas), veillez à ce que vous ayez deux fenêtres en cours d’exécution.
- Terminal 1 :
make updb dblogs
- Terminal 2 :
make up logs
- Naviguez sur localhost:7080, et voilà.
Aperçu haut niveau
Notre système est actuellement :
Avec l’instrumentation OpenTelemetry, notre système évoluera vers :
Comme nous l’avons mentionné plus tôt, il est assez onéreux pour chaque service d’utiliser des clients distincts pour envoyer vers des fournisseurs spécifiques. Au lieu de cela, avec un collecteur OTel exécuté séparément, nous pouvons nous assurer que tous les services (intéressés) peuvent simplement envoyer des métriques/journaux/ traces à ce collecteur, qui peut ensuite les exporter vers divers backends selon besoin — dans ce cas, Jaeger pour les traces.
Commencez donc.
Configurer le Collecteur OTel
Le premier pas est d’ajouter notre collecteur OTel exécuté dans l’environnement Docker ainsi que Jaeger pour qu’ils soient accessibles.
Remarque : Nous avons divisé notre configuration initiale docker-compose.yml
en deux parties :
- db-docker-compose.yml : Contient tout les composants liés aux bases de données et à l’infrastructure (non-application) comme les bases de données (Postgres, Typesense) et les services de surveillance (collecteur OTel, Jaeger, Prometheus, etc.).
- docker-compose.yml: Contient tous les services liés à l’application (Nginx, gRPC Gateway, dbsync, frontend, etc.)
Les deux environnements docker-compose sont connectés par une plateforme réseau partagée (onehubnetwork
), grâce à laquelle les services de ces environnements peuvent communiquer les uns avec les autres. Avec cette séparation, nous n’avons besoin de redémarrer qu’une sous-sélection de services suite à des modifications, ce qui accélère notre développement.
Revenons à notre configuration : dans notre db-docker-compose.yml
, ajoutons les services suivants :
services
...
otel-collector
networks
onehubnetwork
image otel/opentelemetry-collector-contrib0.105.0
command"--config=/etc/otel-collector.yaml"
volumes
./configs/otel-collector.yaml:/etc/otel-collector.yaml
environment
POSTGRES_DB $ POSTGRES_DB
POSTGRES_USER $ POSTGRES_USER
POSTGRES_PASSWORD $ POSTGRES_PASSWORD
jaeger
networks
onehubnetwork
image jaegertracing/all-in-one1.59
container_name jaeger
environment
QUERY_BASE_PATH'/jaeger'
COLLECTOR_OTLP_GRPC_HOST_PORT'0.0.0.0:4317'
COLLECTOR_OTLP_HTTP_HOST_PORT'0.0.0.0:4318'
COLLECTOR_OTLP_ENABLEDtrue
prometheus
networks
onehubnetwork
image prom/prometheus v2.53.1
command
'--config.file=/etc/prometheus/prometheus.yml'
'--web.external-url=/prometheus/'
'--web.route-prefix=/prometheus/'
volumes
./configs/prometheus.yaml:/etc/prometheus/prometheus.yml
ports
9090:9090
Assez simple, cela configure deux services dans notre environnement Docker :
otel-collector
: Le point de convergence de tous les signaux (métriques/logs/ traces) envoyés par les divers services supervisés (nous continuerons à ajouter à cette liste au fil du temps), il utilise l’image standard d’OTel ainsi que notre configuration OTel personnalisée (ci-dessous) qui décrit diverses pipelines d’observabilité (c’est-à-dire comment les signaux doivent être reçus, traitées et exportées de diverses manières).jaeger
: Notre instance Jaeger qui recevra et stockera les traces (exportées par l’otel-collector
), il héberge à la fois le stockage ainsi que le tableau de bord (UI) que nous exportons sur le préfixe d’URL HTTP/jaeger
pour être accessible via Nginx.prometheus
: Malgré qu’il ne soit pas nécessaire pour cet article, nous exportons également les métriques afin qu’elles puissent être récupérées par Prometheus. Nous ne discuterons pas de cela en détail dans cet article.
Quelques choses à noter :
- Malgré qu’il ne soit pas nécessaire pour cet article, nous passons les détails de la connexion
POSTGRES
(en tant que variables d’environnement) à l’otel-collector
afin qu’il puisse scraper les métriques de santé de Postgres. - Jaeger (à partir de v1.35) supporte OTLP nativement.
- La beauté de OTLP est qu’il est possible de chaîner des collecteurs OTel, formant ainsi une sorte de réseau de collecteurs/traitants/transferts/routes, etc.
- OTLP peut être expédié soit via un point de terminaison GRPC ou HTTP (sur les ports 4317 et 4318 respectivement).
- Par défaut, les services OTLP sont démarrés sur
localhost:4317/4318
. C’est bien si Jaeger est exécuté sur le même hôte/pod où les services (surveillés) sont en cours d’exécution. Cependant, comme Jaeger est exécuté dans un pod séparé, il doit être lié à des adresses externes (0.0.0.0). Cela n’était pas clair dans la documentation et a entraîné beaucoup de difficultés. COLLECTOR_OTLP_ENABLED: true
est désormais le paramètre par défaut et n’est pas nécessaire de le spécifier explicitement.
Configuration d’OTel
OTel doit également être configuré avec des récepteurs spécifiques, des traitements et des exportateurs. Nous ferons cela dans configs/otel-collector.yaml.
Ajout de Récepteurs
Nous devons indiquer au collecteur OTel quels récepteurs doivent être activés. Cela est spécifié dans la section receivers
:
receivers
otlp
protocols
http
endpoint 0.0.0.04318
grpc
endpoint 0.0.0.04317
postgresql
endpoint postgres5432
transport tcp
username $ POSTGRES_USER
password $ POSTGRES_PASSWORD
databases
$ POSTGRES_DB
collection_interval 10s
tls
insecuretrue
Ceci active un récepteur OTLP sur les ports 4317 et 4318 (grpc
, http
respectivement). Il existe de nombreux types de récepteurs qui peuvent être démarrés. Comme exemple, nous avons également ajouté un récepteur “postgresql
” qui s’engage à extraire activement les métriques de Postgres ( bien que cela ne soit pas pertinent pour cet article). Les récepteurs peuvent également être basés sur le tirage ou sur l’envoi. Les récepteurs basés sur le tirage extraient périodiquement des cibles spécifiques (par ex., postgres
), tandis que les récepteurs basés sur l’envoi écoutent et reçoivent les métriques/journaux/suivis envoyés par les applications en utilisant le SDK client OTel.
C’est tout. Maintenant, notre collecteur est prêt à recevoir (ou à scraper) les métriques appropriées.
Ajouter des Traitements
Les traitements dans OTel sont une manière de transformer, mapper, grouper, filtrer et/ou enrichir les signaux reçus avant d’exportation. Par exemple, les traitements peuvent effectuer un échantillonnage des métriques, filtrer les journaux ou même grouper les signaux pour assurer l’efficacité. Par défaut, aucun traitement n’est ajouté (le collecteur fonctionne donc en mode passage à travers). Nous ignorerons cela pour l’instant.
Ajouter des Exportateurs
Maintenant, il est temps d’identifier où nous voulons que les signaux soient exportés : vers les backends les mieux adaptés à chacun de ces signaux. Comme pour les récepteurs, les exportateurs peuvent également être basés sur le pull ou le push. Les exportateurs basés sur le push sont utilisés pour émettre des signaux vers un autre récepteur agissant en mode push. Ces exportateurs sont de type sortant. Les exportateurs basés sur le pull exposent des extrémités qui peuvent être scrappées par d’autres récepteurs basés sur le pull (par exemple, prometheus
). Nous ajouterons un exporteur de chaque type : un pour le suivi et un pour que Prometheus puisse le scraper (bien que Prometheus ne soit pas le sujet de ce poste) :
exporters
otlp/jaeger
endpoint jaeger4317
tls
insecuretrue
prometheus
endpoint 0.0.0.09090
namespace onehub
debug
Ici, nous avons un exporteur vers Jaeger exécutant le collecteur OTLP, comme indiqué par otlp/jaeger
. Cet exporteur pushera régulièrement des traces vers Jaeger. Nous ajoutons également une extrémité « scraper » sur le port 9090 que Prometheus scrapera régulièrement.
L’exporteur « debug » est simplement utilisé pour déverser les signaux vers les flux d’entrée/erreur standards.
Définir des Pipelines
Les sections récepteur, traitement et exporteur définissent simplement les modules qui seront activés par le collecteur. Ils ne sont toujours pas invoqués. Pour les invoquer/activer concrètement, ils doivent être références comme des « pipelines ». Les pipelines définissent comment les signaux passent et sont traités par le collecteur. Notre définitions de pipeline (dans la section services
) préciseront cela :
service
extensions health_check
pipelines
traces
receivers otlp
processors
exporters otlp/jaeger debug
metrics
receivers otlp
processors
exporters prometheus debug
Ici, nous définissons deux pipelines. Notez combien ils sont semblables mais permettent deux modes d’exportation différents (Jaeger et Prometheus). Maintenant, nous voyons l’avantage d’OTel et de la création de pipelines à l’intérieur.
traces
:
- Reçois des signaux des SDK clients
- Pas de traitement
- Exporter des traces vers la console et Jaeger
metrics
:
- Recevoir des signaux des SDK clients
- Aucun traitement
- Exporter des métriques vers la console et Prometheus (en exposant un point d’entrée pour qu’il puisse s’y grappiller).
Exposer des tableaux de bord via Nginx
Jaeger fournit un tableau de bord pour visualiser les données de traces sur toutes nos demandes. Cela peut être visualisé dans un navigateur en activant ce qui suit dans notre configuration de Nginx Nginx config. Encore une fois, ce n’est pas le sujet de cet article – nous exposons également l’interface utilisateur de Prometheus via nginx au préfixe d’chemin HTTP /prometheus
.
...
location ~ ^/jaeger
if ($request_method = OPTIONS ) return 200;
proxy_pass http://jaeger:16686; # Note that JaegerUI starts on port 16686 by default
proxy_pass_request_headers on;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host-With-Port $http_host;
proxy_set_header Connection '';
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-HTTPS on;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /;
proxy_http_version 1.1;
chunked_transfer_encoding off;
location ~ ^/prometheus
if ($request_method = OPTIONS ) return 200;
proxy_pass http://prometheus:9090;
proxy_pass_request_headers on;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host-With-Port $http_host;
proxy_set_header Connection '';
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-HTTPS on;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /;
proxy_http_version 1.1;
chunked_transfer_encoding off;
...
Visualiser les traces dans Jaeger
L’interface utilisateur de Jaeger est assez complète et offre plusieurs fonctionnalités que vous pouvez explorer. Naviguez vers l’interface utilisateur de Jaeger dans le navigateur. Vous verrez une interface complète pour rechercher et analyser les traces. allez-y et familiarisez-vous avec les principales sections, y compris la barre de recherche et la liste des traces. Vous pouvez rechercher différentes traces en utilisant des critères de recherche et filtrer par service, durées de temps, composants, etc.
Analysez les lignes de temps des traces dans les différentes demandes pour comprendre la séquence d’opérations. Chaque étendue représente une unité de travail, montrant les temps de début et de fin, la durée et les métadonnées associées. Cette vue détaillée est très utile pour identifier les goulets d’étranglement de performance et les erreurs au sein de la trace.
Intégrer le SDK client
Jusqu’à présent, nous avons configuré nos systèmes pour visualiser, consommer des signaux, etc. Cependant, nos services ne sont toujours pas mis à jour pour émettre les signaux vers OTel. Ici, nous integrons le client SDK (Golang) dans différentes parties de notre code. La documentation du SDK est un excellent endroit pour vous familiariser avec certains des concepts.
Les concepts clés avec lesquels nous traiterons sont décrits ci-dessous.
Ressources
Ressources sont les entités qui produisent le signal. Dans notre cas, la portée de la ressource est l’exécutable hébergeant les services. Actuellement, nous avons une seule ressource pour l’ensemble du service Onehub, mais cela pourrait être divisé par la suite.
Cela est défini dans cmd/backend/obs.go. Notez que le client SDK n’a pas besoin que nous entrions dans les détails de la définition de la ressource explicitement. L’assistant standard (sdktrace.WithResource
) nous permet de créer une définition de ressource en inférissant les parties les plus utiles (comme le nom du processus, le nom du pod, etc.) à runtime.
Nous avons seulement dû remplacer une chose : la variable d’environnement OTEL_RESOURCE_ATTRIBUTES: service.name=onehub.backend,service.version=0.0.1
pour le service onehub
dans docker-compose.yml.
La propagation des contextes
Propagation des contextesest un sujet très important dans le domaine de l’observabilité. Les différents pilars sont exponentiellement puissants lorsque nous pouvons corréler les signaux issus de chacun des pilars pour identifier les problèmes affectant notre système. Envisagez les contextes comme des extraits de données qui peuvent être rattachés aux signaux : c’est-à-dire, qui peuvent être « rejoints » de manière unique pour relier les différents signaux à un groupe particulier (par exemple une requête).
Fournisseurs/Exportateurs
Pour chacun des signaux, OTel fournit une interface fournisseur (par exemple, TracerProvider
pour l’exportation de traces, MeterProvider
pour l’exportation de métriques, LoggerProvider
pour l’exportation des journaux et ainsi de suite). Pour chacune de ces interfaces, il peut y avoir plusieurs implémentations, par exemple, un fournisseur de débogage pour envoyer vers les flux stdout/err, un fournisseur OTel pour exporter vers un autre point de terminaison OTel (dans une chaîne) ou même directement via une varieté d’exportateurs. Cependant, dans notre cas, nous voulons déferler le choix de tous les fournisseurs hors de nos services et, à la place, envoyer tous les signaux au collecteur OTel exécuté dans notre environnement.
Pour abstraire cela, nous créerons un type « OTELSetup
» qui garde trace des différents fournisseurs que nous pourrions souhaiter utiliser ou remplacer. Dans cmd/backend/obs.go, nous avons :
type OTELSetup[C any] struct {
ctx context.Context
shutdownFuncs []ShutdownFunc
Resource *resource.Resource
Context C
SetupPropagator func(o *OTELSetup[C])
SetupTracerProvider func(o *OTELSetup[C]) (trace.TracerProvider, ShutdownFunc, error)
SetupMeterProvider func(o *OTELSetup[C]) (otelmetric.MeterProvider, ShutdownFunc, error)
SetupLogger func(o *OTELSetup[C]) (logr.Logger, ShutdownFunc, error)
SetupLoggerProvider func(o *OTELSetup[C]) (*log.LoggerProvider, ShutdownFunc, error)
}
Ceci est un simple enveloppeur qui suit les aspects communs nécessaires par l’API OTel. Ici, nous avons des fournisseurs (Journaliste, Traceur et Métrique) ainsi que des manières de fournir du contexte (pour la trace). Le Resource parent utilisé par tous les fournisseurs est également spécifié ici. Les fonctions de déchargement sont intéressantes. Il s’agit de fonctions appelées par les fournisseurs lorsque l’exporteur sous-jacent est terminé (avec grâce ou à cause d’une sortie). L’enveloppeur lui-même prend un genre générique donc les instantiateurs spécifiques de ce Setup peuvent utiliser leurs propres données personnalisées.
Le dépôt contient deux implémentations de ceci :
- Le signal de journalisation vers la sortie standard/erreur – cmd/backend/stdout.go
- L’exportation de signaux vers un autre collecteur OTel – cmd/backend/otelcol.go
Nous instantiâmes le second dans notre application. Nous ne ferons pas d’explications sur les implémentations spécifiques car elles ont été prises de l’exemple dans l’API avec des petites corrections et des refactorings. En particulier, jetez un œil à l’exemple otel-collector pour de l’inspiration.
Initialisez les fournisseurs OTel.
L’essence de l’activation du collecteur dans nos services est que quelque chose de lié au « contexte » OTel est démarré à tous les points d’entrée. Si ce contexte est créé au début, il sera envoyé à tous les cibles appelées ici, qui est ensuite propagé par la suite ( tant que nous faisons le bon choix).
En prenant l’appel API simple ListTopics
(api/vi/topics
), notre requête prend le chemin suivant et revient en arrière :
[ Browser ] ---> [ Nginx ] ---> [ gRPC Gateway ] ---> [ gRPC Service ] ---> [ Database ]
Dans notre cas, les points d’entrée ici sont au début lorsque le gRPC Gateway reçoit une requête API de Nginx (nous pourrions démarrer la traces d’entrée dès que la requête HTTP atteint Nginx pour mieux mettre en évidence les latences à Nginx, mais nous repousserons pour un peu).
ce qui est nécessaire est :
- gRPC Gateway Reçoit une requête.
- Il crée une instance « personnalisée » de
context.Context
spécifique à OTel. - Créer une connexion personnalisée au service gRPC correspondant (par exemple,
TopicService
) en passant ce contexte au lieu de celui par défaut - Le service correspondant utilise ensuite ce contexte lors de l’émission des traces.
Allons-y pas à pas.
Initialiser et préparer l’SDK OTel pour l’utilisation
Dans main.go, initialisons d’abord la connexion au collecteur :
func main() {
flag.Parse()
// Handle SIGINT (CTRL+C) gracefully.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
collectorAddr := cmdutils.GetEnvOrDefault("OTEL_COLLECTOR_ADDR", "otel-collector:4317")
conn, err := grpc.NewClient(collectorAddr,
// Note the use of insecure transport here. TLS is recommended in production.
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Println("failed to create gRPC connection to collector: %w", err)
return
}
setup := NewOTELSetupWithCollector(conn)
err = setup.Setup(ctx)
if err != nil {
log.Println("error setting up otel: ", err)
}
defer func() {
err = setup.Shutdown(context.Background())
}()
ohdb := OpenOHDB()
srvErr := make(chan error, 2)
httpSrvChan := make(chan bool)
grpcSrvChan := make(chan bool)
go startGRPCServer(*addr, ohdb, srvErr, httpSrvChan)
go startGatewayServer(ctx, *gw_addr, *addr, srvErr, grpcSrvChan)
// Wait for interruption.
select {
case err = <-srvErr:
log.Println("Server error: ", err)
// Error when starting HTTP server or GRPC server
return
case <-ctx.Done():
// Wait for first CTRL+C.
// Stop receiving signal notifications as soon as possible.
stop()
}
// When Shutdown is called, ListenAndServe immediately returns ErrServerClosed.
httpSrvChan <- true
grpcSrvChan <- true
...
}
- Lignes 8-16 : Nous créons une connexion au
otel-collector
exécuté dans notre environnement Docker. - Lignes 17-21 : Nous configurons ensuite le système OTel avec des fournisseurs de trace et de métriques afin que notre collecteur puisse maintenant envoyer toutes les traces et les métriques (n’oubliez pas que nous avons défini des récepteurs dans la configuration OTel plus tôt).
- Lignes 23-25 : Nous configurons des finalisateurs pour nettoyer les connexions/fournisseurs OTel, etc., lors du redémarrage.
- Ligne 27 : Nous configurons notre base de données et les connexions comme avant.
- Lignes 29 + : Précédemment, nous avions simplement démarré les services GRPC et Gateway en arrière-plan, et c’était tout. Nous n’étaient pas vraiment préoccupés par leur état de retour ou de sortie. Pour un système plus résilient, il est important de disposer de meilleures informations sur le cycle de vie des services que nous démarrons. Ainsi, nous passons maintenant le canal de « callback » pour chacun des services que nous démarrons. Lorsque les serveurs se terminent, les méthodes respectives appellent vers ces canaux disponibles pour indiquer qu’elles se sont arrêtées avec succès. Notre binaire entier quittera lorsque l’un de ces services se termine.
Prenons l’exemple de la manière dont notre service Gateway utilise ce canal.
Ainsi que de démarrer le serveur HTTP (pour le grpc-gateway) :
1 http.ListenAndServe(gw_addr, mux)
Nous avons maintenant :
server := &http.Server{
Addr: gw_addr,
BaseContext: func(_ net.Listener) context.Context { return ctx },
Handler: otelhttp.NewHandler(mux, "gateway", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s %s", operation, r.Method, r.URL.Path)
})),
}
go func() {
<-stopChan
if err := server.Shutdown(context.Background()); err != nil {
log.Fatalln(err)
}
}()
srvErr <- server.ListenAndServe()
Attention aux lignes 9-14 où la fermeture du serveur est surveillée dans un goroutine séparé et à la ligne 15 où, si il y avait une erreur lorsque le serveur s’est arrêté, elle est retournée via le canal « notification » passé en argument de cette méthode.
Maintenant, les différentes parties de nos services ont accès à une connexion « active » OTLP pour envoyer des signaux quand ils le font.
Middleware OTel pour gRPC Gateway
Au-dessus, l’instance de http.Server
utilisée pour démarrer le gRPC Gateway utilise un gestionnaire personnalisé : le http.Handler
dans le package OTel HTTP. Ce gestionnaire prend une instance existante de http.Handler
, la décore avec le contexte OTel et s’assure de sa propagation à tout autre downstream appelé.
server := &http.Server{
Addr: gw_addr,
BaseContext: func(_ net.Listener) context.Context { return ctx },
Handler: otelhttp.NewHandler(mux, "gateway", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s %s", operation, r.Method, r.URL.Path)
})),
}
Notre gestionnaire HTTP est simple :
- Ligne 4 : Nous créons un nouveau wrapper spécifique OTel pour gérer les requêtes HTTP.
- Ligne 5 : Nous définissons l’option
SpanFormatter
afin que les traces puissent être identifiées de manière unique par la méthode et les chemins de requête HTTP. Sans ceSpanNameFormatter
, la définition par défaut du « nom » pour nos traces au gateway serait simplement"gateway"
, ce qui donnerait toutes les traces une apparence comme ceci :
Enveloppement du Gateway vers les appels gRPC avec OTel
Par défaut, la bibliothèque gRPC Gateway crée un « plain » context lors de la création/gestion des connexions vers les services gRPC sous-jacents. Après tout, le gateway ne sait rien de OTel. En mode standard, une connexion (de l’utilisateur/navigateur) vers le gRPC Gateway et la connexion du gateway vers le service gRPC seront traitées comme deux traces différentes.
Ainsi, il est important de retirer de la responsabilité de la création des connexions gRPC du Gateway et de fournir plutôt une connexion déjà compatible OTel. Nous ferons cela maintenant.
Avant l’intégration d’OTel, nousregistions un gestionnaire de passerelle pour nos gRPC avec :
ctx := context.Background()
mux := runtime.NewServeMux() // Not showing the interceptors
opts := []grpc.DialOption{grpc.WithInsecure()}
// grpc_addr = ":9090"
v1.RegisterTopicServiceHandlerFromEndpoint(ctx, mux, grpc_addr, opts)
v1.RegisterMessageServiceHandlerFromEndpoint(ctx, mux, grpc_addr, opts)
// And other servers
Maintenant, passer une connexion différente est simple :
mux := // Creat the mux runtime as usual
// Use the OpenTelemetry gRPC client interceptor for tracing
trclient := grpc.WithStatsHandler(otelgrpc.NewClientHandler())
conn, err := grpc.NewClient(grpc_addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
trclient)
if err != nil {
srvErr <- err
}
err = v1.RegisterTopicServiceHandler(ctx, mux, conn)
if err != nil {
srvErr <- err
}
// And the Message and User server too...
Ce que nous avons fait, c’est d’abord créer un client
(Ligne 6) qui agit comme une usine de connexions pour notre serveur gRPC. Le client est assez simple. Seule la classe gRPC ClientHandler
(otelgrpc.NewClientHandler
) est utilisée pour créer la connexion. Cela garantit que le contexte de la trace en cours qui a commencé sur une nouvelle requête HTTP est maintenant propagé vers le serveur gRPC via ce gestionnaire.
C’est tout. Maintenant, nous devrions commencer à voir les nouvelles requêtes vers le passerelle et la requête Passerelle->gRPC dans une seule trace consolidée plutôt que de les voir comme deux traces différentes.
Débuter et terminer des Spans
Nous y avons presque. Jusqu’à présent :
- Nous avons activé l’collecteur OTel et Jaeger pour recevoir et stocker les données de trace (spans) (dans docker-compose).
- Nous avons configuré l’collecteur OTel de base (en cours d’exécution en tant que pod séparé) en tant que fournisseur de traceurs, de métriques et de journaux (c’est-à-dire que l’intégration OTel de notre application utilisera ce point de terminaison pour déposer toutes les signaux).
- Nous avons enveloppé le gestionnaire HTTP de la passerelle pour qu’il soit compatible OTel, de sorte que les traces et leurs contextes sont créés et propagés.
- Nous avons remplacé le client (gRPC) de la passerelle de sorte qu’il emballle maintenant le contexte OTel de notre configuration OTel au lieu d’utiliser un contexte par défaut.
- Nous avons créé des instances de traceur/compteur/journaliste globales pour nous permettre d’envoyer des signaux réels en utilisant celles-ci.
Maintenant, nous devons émettre des étiquettes pour toutes les « endroits intéressants » dans notre code. Prenons l’exemple de la méthode ListTopics
(dans services/topics.go) :
func (s *TopicService) ListTopics(ctx context.Context, req *protos.ListTopicsRequest) (resp *protos.ListTopicsResponse, err error) {
results, err := s.DB.ListTopics(ctx, "", 100)
if err != nil {
return nil, err
}
resp = &protos.ListTopicsResponse{Topics: gfn.Map(results, TopicToProto)}
return
}
Nous appelons la base de données pour récupérer les sujets et les retournons. Analoguement à la méthode d’accès à la base de données (dans datastore/topicds.go) :
func (tdb *OneHubDB) ListTopics(ctx context.Context, pageKey string, pageSize int) (out []*Topic, err error) {
query := tdb.storage.Model(&Topic{}).Order("name asc")
if pageKey != "" {
count := 0
query = query.Offset(count)
}
if pageSize <= 0 || pageSize > tdb.MaxPageSize {
pageSize = tdb.MaxPageSize
}
query = query.Limit(pageSize)
err = query.Find(&out).Error
return out, err
}
Ici, nous serions principalement intéressés à savoir combien de temps est passé dans chacune de ces méthodes. Nous créons simplement des étiquettes dans chacune d’entre elles, et c’est fini. nos ajouts aux méthodes du service et du stockage de données (respectivement) sont :
services/topics.go
:
func (s *TopicService) ListTopics(ctx context.Context, req *protos.ListTopicsRequest) (resp *protos.ListTopicsResponse, err error) {
ctx, span := Tracer.Start(ctx, "ListTopics")
defer span.End()
... rest of the code to query the DB and return a proto response
}
datastore/topicds.go
:
func (tdb *OneHubDB) ListTopics(ctx context.Context, pageKey string, pageSize int) (out []*Topic, err error) {
_, span := Tracer.Start(ctx, "db.ListTopics")
defer span.End()
... rest of the code to fetch rows from the DB and return them
}
Le schéma général est le suivant :
1. Créer une étiquette:
ctx, span := Tracer.Start(ctx, "<span name>")
Dans ce cas, le contexte donné (ctx
) est « emballé » et un nouveau contexte est retourné. Nous pouvons (et devrions) passer ce nouveau contexte enveloppé à d’autres méthodes. Nous faisons exactement cela lorsque nous appelons la méthode de stockage de données ListTopics
.
2. Terminer l’étiquette:
defer span.End()
Terminer une étiquette (lorsque la méthode retourne) assure que les bonnes fins de temps/codes, etc. sont enregistrés. Nous pouvons également faire d’autres choses telles queajouter des étiquettes et des statuts à celui-ci si nécessaire pour transporter plus d’information à des fins de débugage.
C’est tout. Vous pouvez voir vos magnifiques traces dans Jaeger et obtenir de plus en plus d’insights sur la performance de vos requêtes, de bout en bout !
Conclusion
Nous avons couvert beaucoup de terrain dans ce billet et n’avons pas encore brossé le portrait de tous les détails derrière OTel et le suivi. Au lieu de surcharger ce billet (déjà très riche), nous présenterons de nouveaux concepts et détails complexes dans des billets futurs. Pour l’instant, essayez cela dans vos propres services et essayez de jouer avec d’autres exportateurs et récepteurs dans le dossier otel-contrib.
Source:
https://dzone.com/articles/tracing-with-opentelemetry-and-jaeger