Introduction à Jaeger
Tags: kubernetes, jaeger, tutorial, tracing
Published: 2021-10-15 12:30:03

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

``` bash
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
```

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

``` bash
kubectl apply -n observability -f - <<EOF
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: simplest
EOF
```

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

``` bash
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
```

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

![alt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y4ryiu4n7ssdl5quii17.png "Interface de Jaeger")
 
## 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

``` python
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()
```

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

``` python
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()
```

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:

``` batch
kubectl apply -f https://raw.githubusercontent.com/baalooos/jaeger-example/master/deployment.yaml
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f21gnfkute95q099qmbe.png "Menu déroulant permettant de sélectionner un service")

`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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ak7vba8b2q353ikmw6l7.png "Timeline et durée des traces")

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/21c374d4e4s2fbtojtq2.png "Information sur une trace")

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ztd2ahniwa98mifu0kih.png "Détail d'une trace")

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rvbrnt717khcgxnbyan5.png "Détail d'un service")

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jg78kudj6jt2pb1xab06.png "Deep dependency graph")

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.

``` bash
kubectl delete deploy server
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ntq8g3iqyxy0lv1m2qth.png "Trace client avec erreur")

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kflrhayoax9k0bdjiyp4.png "Logs d'erreur")

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.