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
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
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
Ce service nous permet d'accéder à Jaeger. Il n'a pas d'authentification et on arrive directement sur une interface de recherche.
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()
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()
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
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:
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:
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.
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.
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)
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.
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
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:
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:
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.
Top comments (0)