DEV Community

Gagnaire Charles-Edouard
Gagnaire Charles-Edouard

Posted on

Introduction à Jaeger

Le passage à des applications micro-services crée de nouvelles problématiques liées au monitoring et à l'observabilité. En effet, là où avant on pouvait suivre l'ensemble des appels sur un même serveur, maintenant ces appels sont répartis sur différents micro-services et il est de plus en plus complexe de faire de la corrélation d'événements. 
Dans cet article nous allons voir comment déployer Jaeger dans un cluster Kubernetes, puis comment modifier deux petites applications Python, un client et un serveur, afin d'envoyer des traces vers Jaeger.

Installation de Jaeger Operator for Kubernetes

Pour installer Jaeger, on va déployer l'opérateur officiel fournit par le projet

kubectl create namespace observability
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/crds/jaegertracing.io_jaegers_crd.yaml
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/service_account.yaml
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role.yaml
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role_binding.yaml
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/operator.yaml
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/cluster_role.yaml
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/cluster_role_binding.yaml
Enter fullscreen mode Exit fullscreen mode

On peut ensuite déployer Jaeger dans le namespace observability grâce à l'opérateur

kubectl apply -n observability -f - <<EOF
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: simplest
EOF
Enter fullscreen mode Exit fullscreen mode

L'opérateur va alors vous créer automatiquement un service de type ClusterIP pointant sur l'interface Jaeger.

kubectl get svc -n observability
NAME                           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                  AGE
my-jaeger-query                ClusterIP   10.110.142.232   <none>        16686/TCP
Enter fullscreen mode Exit fullscreen mode

Ce service nous permet d'accéder à Jaeger. Il n'a pas d'authentification et on arrive directement sur une interface de recherche.

alt

Déploiement d'applications dans Jaeger

Maintenant qu'on a déployé Jaeger dans notre cluster, on va déployer 2 applications python permettant de générer des traces. Pour ce faire on va déployer un serveur acceptant les requêtes et renvoyant une réponse après un court délai et un client qui va automatiquement appeler le serveur toutes les X secondes.

Le client

import requests
import sys
import time
from opentracing.ext import tags
from opentracing.propagation import Format
import logging
from jaeger_client import Config
import http.server

# Tracer init and config
def init_tracer(service):
    logging.getLogger('').handlers = []
    logging.basicConfig(format='%(message)s', level=logging.DEBUG)

    config = Config(
        config={
            'sampler': {
                'type': 'const',
                'param': 1,
            },
            'local_agent': {
                'reporting_host': "my-jaeger-agent.observability.svc.cluster.local",
            },
            'logging': True,
            'reporter_batch_size': 1,
        },
        service_name= service
    )

    # this call also sets opentracing.tracer
    return config.initialize_tracer()

class MyHttpRequestHandler():
    def http_get():
        # Create span with a unique name
        with tracer.start_span('my_super_cool_request') as span:
            url = 'http://server'

            headers = {}
            # Add headers to the tracer
            tracer.inject(span, Format.HTTP_HEADERS, headers)

            # Add a custom event
            span.log_kv({'event': 'sent request'})

            # For debug purpose
            print(headers)

            # Hit the server
            r = requests.get(url, headers=headers)

            # Check if we have a 200 response
            assert r.status_code == 200

            # Another random event
            span.log_kv({'event': 'return response'})
            return r.text


# Init tracer
tracer = init_tracer('client')

print('Client is running')

# Main function
getIt = MyHttpRequestHandler
print(getIt.http_get())

# Wait before closing
time.sleep(2)
tracer.close()
Enter fullscreen mode Exit fullscreen mode

Le client est une boucle bash lançant un script Python. Ce script va initialiser le traceur, ajouter des headers, logger un événement sent request et faire la requête vers le serveur en passant les headers contenant les informations du traceur.
Après cette requête, le client vérifie qu'il a bien reçu un code retour 200, et log un événement return response.

Le serveur

import http.server
import socketserver
import logging
from urllib.parse import urlparse
from urllib.parse import parse_qs
from jaeger_client import Config
import time
from opentracing.ext import tags
from opentracing.propagation import Format
from random import randrange

# Tracer init and config
def init_tracer(service):
    logging.getLogger('').handlers = []
    logging.basicConfig(format='%(message)s', level=logging.DEBUG)

    config = Config(
        config={
            'sampler': {
                'type': 'const',
                'param': 1,
            },
            'local_agent': {
                'reporting_host': "my-jaeger-agent.observability.svc.cluster.local",
            },
            'logging': True,
            'reporter_batch_size': 1,
        },
        service_name= service
    )

    # this call also sets opentracing.tracer
    return config.initialize_tracer()   

class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        # Extract informations for Jaeger
        span_ctx = tracer.extract(Format.HTTP_HEADERS, self.headers)
        span_tags = {tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER}
        with tracer.start_span('format', child_of=span_ctx, tags=span_tags) as span:

            # Sending an '200 OK' response
            self.send_response(200)

            # Setting the header
            self.send_header("Content-type", "text/html")

            # Whenever using 'send_header', you also have to call 'end_headers'
            self.end_headers()

            # Forge html response
            html = f"<html><head></head><body><h1>Hello</h1></body></html>"

            span.log_kv({'event': 'this is a span'})

            # Add some random latency
            time.sleep(randrange(5))

            # Writing the HTML contents with UTF-8
            self.wfile.write(bytes(html, "utf8"))
            return

# Init tracer
tracer = init_tracer('server')

# Create an object of the above class
handler_object = MyHttpRequestHandler

PORT = 80

# Configure the server
my_server = socketserver.TCPServer(("0.0.0.0", PORT), handler_object)

print('Backend server is running')

# Start the server
my_server.serve_forever()
Enter fullscreen mode Exit fullscreen mode

Le serveur est lancé au démarrage du pod et attend les connexions. Quand il reçoit une connexion, il lit les données du traceur, forge une réponse avec un code retour 200, ajoute un événement this is a span. Le serveur est configuré pour rajouter de la latence de manière aléatoire et renvoyer la réponse au client.

Déploiement

On peut déployer les applications grâce à la commande suivante:

kubectl apply -f https://raw.githubusercontent.com/baalooos/jaeger-example/master/deployment.yaml
Enter fullscreen mode Exit fullscreen mode

Cette commande va déployer dans le namespace default:

  • Le client
  • Le serveur
  • Un service permettant au client de joindre le serveur

Affichage dans Jaeger

Après quelques secondes, on voit dans l'interface Jaeger qu'on a des services disponibles dans la recherches:

alt

Client et server correspondent respectivement à nos deux micro-services, alors que jaeger-query est un indicateur interne affichant des informations sur les requêtes utilisateurs dans Jaeger. 
En sélectionnant le service client on peut afficher les différentes traces recueillies par Jaeger:

image

On voit ici en abscisse le temps et en ordonnée la durée de la trace. Comme on peut le voir, la durée des traces est aléatoire et dure entre 1 et 5 secondes.

Détail d'une trace

L'affichage des traces permet de voir les différents micro-services impliqués dans la trace, l'heure à laquelle la trace a été enregistrée et la durée précise de la trace.

image

Cliquer sur une trace nous permet d'afficher une vue détaillée. Dans cette capture d'écran j'ai rajouté de la latence avant et après l'appel serveur afin de voir le temps d'appel du serveur.

image

En bleu on voit la durée de la trace du client, et en jaune la durée de la trace du serveur. Sur cette exemple on voit bien que la trace côté client dure 3.01s, alors que la trace côté service ne dure que 1s. 
On peut cliquer sur client ou server en dessous de la timeline, afin d'avoir les dernières informations concernant notre trace. Ces informations sont:

  • les tags (qui permettent aussi de faire une recherche)
  • les méta-datas du process
  • les logs (commande span.log_kv({'event': 'Some Event'}) des scripts python)

image

Deep Dependency Graph

Dans Jaeger UI, il est possible d'accéder au deep dependency graph, un graphe permettant de voir les interactions entre les différents micro-services. Notre exemple étant très simple le graphe est composé de seulement deux nœuds.

image

En passant au dessus des nœuds, il est possible de réaliser certaines actions tel que:

  • rechercher les traces liés à ce nœud
  • changer le focus du nœud courant vers un autre nœud
  • masquer un nœud (peut être intéressant sur des schémas avec beaucoup de micro-services pour améliorer la lisibilité de l'ensemble)

Affichage de messages d'erreurs

Même si Jaeger n'est pas un outil de gestion de logs, il est intéressant de noter que Jaeger est capable d'afficher les messages d'erreurs remontés dans les traces. Dans notre exemple, nous allons supprimer le déploiement server afin de générer une erreur côté client.

kubectl delete deploy server
Enter fullscreen mode Exit fullscreen mode

Au bout de quelques secondes, si l'on refait une recherche dans Jaeger UI, on remarque que l'affichage n'est plus le même:

image

Dans cette affiche on peut voir qu'il n'y a plus que le client et que la trace contient une erreur. Il est possible de voir le détail de l'erreur en allant dans les logs de la trace:

image

Dans notre cas le message d'erreur est composé:

  • du type évènement
  • du type d'erreur
  • de l'objet et du message d'erreur proprement
  • de la stack trace python liée à l'erreur

Conclusion

Dans cet article, nous avons vu comment installer Jaeger mais aussi comment instrumentaliser une application afin de renvoyer des traces vers notre installation. Nous avons aussi vu comment afficher et exploiter ces traces et enfin nous avons vu que les traces contenaient de nombreuses informations utiles et même, le cas échéant, des messages d'erreurs.

Discussion (0)