TP - appareiller une application pour Prometheus
Dans ce TP nous allons essayer et commenter plusieurs code minimaux d'application qui démontrent comment créer les différents types de métrique de base pour un application web (nombre et moyenne au fil du temps des requêtes, nombre d'exceptions déclenchées dans une portion de code, latence, etc).
Ces exemples pourraient être présentés dans la plupart des langages. Nous utiliserons Python ici car c'est un langage assez simple et direct.
Créez un dossier de projet
tp_app_instrumentationet ouvrez le avec VSCod(ium)Cherchez et installez la librairie python prometheus client avec
apt searchetapt installou a défaut avecpip install prometheus_client
Utiliser la librairie client prometheus Python
Démarrons avec un simple serveur web :
- Créez le fichier
exemple1.pyavec le code suivant.
import http.server
class MyHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"Application d'exemple 1")
if __name__ == "__main__":
server = http.server.HTTPServer(('localhost', 8000), MyHandler)
server.serve_forever()
En lançant le programme avec python3 exemple1.py le programme sert une page sur http://localhost:8000/
Pour exposer une route de métrique au format OpenMetrics de prometheus on peut utiliser (dans les cas simples) la fonction start_http_server qui va démarrer un autre serveur http dédié à la route de métrique. Pour cela ajouter
- la premiere ligne du fichier:
from prometheus_client import start_http_server - comme première ligne du main:
start_http_server(8001)
On peut ensuite visualiser les métriques sur http://localhost:8001/ sur la route /metrics (route par défaut pour OpenMetrics/Prometheus)
On obtient ainsi comme pour le node_exporter une liste des metriques disponibles. Mais ce qui nous intéresse c'est de scraper cette route avec Prometheus. Pour cela ajoutez au fichier de configuration Prometheus le bloc suivant (et redémarrez prometheus):
- job_name: example
static_configs:
- targets:
- localhost:8001
On peut ensuite visiter Prometheus à l'adresse http://localhost:9090/.
- Pour tester entrez l'expression PromQL
python_infoet exécutez. On peut voir ainsi des metriques d'information sur le client Python. (Nous n'avons pas encore défini de métrique spécifique pour notre app)
Ajouter un compteur de requête
- Copiez le fichier
exemple1.pyenexemple2.py - Ajoutez un import
from prometheus_client import Counteren début de fichier - Ajoutez une ligne de définition de la métrique juste après les imports :
REQUESTS = Counter('example_app_total', 'Nombre de requete sur notre application d'exemple') - Ajoutez en début de la méthode
do_GETla ligne d'incrémentation de la métrique :REQUESTS.inc(). Du coup à chaque requête cette ligne est executée et le total du compteur est augmenté de 1.
Quelques remarques:
- Les métriques des doivent être définies en amont du programme avant d'être utilisées
- L'aide explicative de la métrique sert à la documenter et est visible quand on fait une requete sur
/metrics - Les noms des métriques doivent être descriptifs et uniques (pas si facile quand on a un grosse app avec plein de métriques)
- On peut faire plusieurs registries pour classer les métriques, si ce n'est pas précisé les métriques vont dans le registry par défaut.
Utilisons maintenant notre compteur dans Prometheus.
- Lancez le programme exemple2.
- Visitez prometheus et executez
example_app_totalpuisrate(example_app_total[1m]).
Compter les exceptions ou des quantité non entières
Les libraires d'"appareillages" ont des outils plus spécifiques adaptés à chaque langage. Par exemple la libraraire Python propose un contexte count_exceptions qui incrémentera le compteur seulement lorsqu'une exception est lancée (a l'intérieur du contexte) :
- Ajoutez le compteur d'exception suivant en dessous de la déclaration du compteur REQUEST:
EXCEPTIONS = Counter('example_app_exceptions_total', "Nombre d'exceptions pendant l'éxecution de l'app d'exemple")`
- Ajoutez le bloc suivant en dessous de la ligne
REQUEST.inc():
with EXCEPTIONS.count_exceptions():
if random.random() < 0.2:
raise Exception
- Que fait ce code précisément ?
- Comment profiter de notre compteur d'exception dans Prometheus ?
Réponse
Le code précédent va déclencher une exception à chaque fois que le tirage aléatoire de la fonction random est inférieur à 0.2 (donc 1/5 des requêtes). La requête est captée par le context de notre compteur d'exception (le with) qui est incrémenté.
On peut ensuite aller regarder le compteur avec rate comme précédemment: rate(example_app_exceptions_total[1m])
Cependant la fréquence d'erreur n'a pas vraiment de sens dans l'absolu indépendamment du nombre de requête. L'information intéressante ici est plutôt le taux d'erreur, c'est à dire le nombre d'erreurs rapporté au nombre de requêtes. On peut calculer ce taux dans une expression PromQL: rate(example_app_exceptions_total[1m])/rate(example_app_total[1m]). Dans notre cas, avec un grand nombre de requêtes ce taux devrait se stabiliser autour de 0,2.
Compter une valeur non entière
On peut incrémenter un compteur d'une valeur non unitaire ou même non entière comme dans l'exemple suivant :
from prometheus_client import Counter
import random
REQUESTS = Counter('example_app_total', "Requêtes sur l'app d'exemple")
VENTES = Counter('example_sales_total', "Chiffre d'affaire cumulé sur les ventes")
class MyHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
REQUESTS.inc()
sale_value = random.random() # CA de la vente en €
SALES.inc(sale_value)
self.send_response(200)
self.end_headers()
self.wfile.write(f"Un vente de {sale_value}€ a été effectuée.".encode())
Les Jauges
Les jauges sont une capture instantanée d'un état actuel. Alors que pour les compteurs, la vitesse à laquelle il augmente est ce qui vous préoccupe, pour les jauges, c'est la valeur actuelle de la jauge. En conséquence, les valeurs peuvent augmenter ou diminuer.
Exemples de jauges comprennent :
- Le nombre d'éléments dans une file d'attente
- L'utilisation de la mémoire d'un cache
- Nombre de fils d'exécution actifs (threads)
- La dernière fois qu'un enregistrement a été traité
- Le nombre moyen de demandes par seconde au cours de la dernière minute
Les jauges ont trois principales méthodes que vous pouvez utiliser : inc, dec et set. Similairement aux méthodes sur les compteurs, inc et dec augmentent ou diminuent la valeur d'une jauge d'une valeur.
Essayez le code suivant:
import time
from prometheus_client import Gauge
INPROGRESS = Gauge('app_request_inprogress',
'Nombre de requetes en cours.')
LAST = Gauge('app_request_last_time_seconds',
"La dernière fois qu'une requete a été servie.")
class MyHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
INPROGRESS.inc()
self.send_response(200)
self.end_headers()
self.wfile.write(b"Salut !")
LAST.set(time.time())
INPROGRESS.dec()
Autre version avec un peu de sucre syntaxique Python:
from prometheus_client import Gauge
INPROGRESS = Gauge('app_request_inprogress',
'Nombre de requetes en cours.')
LAST = Gauge('app_request_last_time_seconds',
"La dernière fois qu'une requete a été servie.")
class MyHandler(http.server.BaseHTTPRequestHandler):
@INPROGRESS.track_inprogress()
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"salut !")
LAST.set_to_current_time()
Un Summary pour la latence moyenne
import time
from prometheus_client import Summary
LATENCY = Summary('app_latency_seconds',
'Time for a request')
class MyHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
start = time.time()
self.send_response(200)
self.end_headers()
self.wfile.write(b"salut !")
LATENCY.observe(time.time() - start)
app_latency_seconds_count est le nombre d'appels "observe" qui ont été effectués. Ainsi, rate(app_latency_seconds_count[1m]) dans le navigateur d'expressions retourne le taux par seconde des requêtes.
app_latency_seconds_sum est la somme des valeurs passées à "observe", donc rate(app_latency_seconds_sum[1m]) est le temps passé à répondre aux requêtes par seconde.
Si vous divisez ces deux expressions, vous obtenez la latence moyenne sur la dernière minute :
rate(app_latency_seconds_sum[1m])
/
rate(app_latency_seconds_count[1m])
Un Histogramme pour les quantile de latence
L'instrumentation pour les histogrammes est proche de celle des summary: La méthode observe() vous permet de faire d'enregistrer des évènement avec leur valeurs manuelles, et un décorateur de fonction permettent de faciliter l'utilisation.
from prometheus_client import Histogram
LATENCY = Histogram('app_latency_seconds',
'Temps pour une requête')
class MyHandler(http.server.BaseHTTPRequestHandler):
@LATENCY.time()
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"Salut")
Ce code produira un ensemble de séries temporelles avec le nom app_latency_seconds_bucket. Un histogramme a un ensemble de buckets, tels que 1 ms, 10 ms, et 25 ms. L'histogramme fonctionne en suivant le nombre d'événements qui tombent dans chaque bucket.
Ensuite, la fonction PromQL histogram_quantile permet de calculer un quantile à partir des buckets. Par exemple :
histogram_quantile(0.95, rate(app_latency_seconds_bucket[1m]))
Le taux (rate) est nécessaire car les séries temporelles des buckets sont des compteurs.
Les buckets par défaut couvrent une plage de latences de 1 ms à 10 s. Pour ajouter un bucket personnalisé on fournit une liste triée :
LATENCY = Histogram('app_latency_seconds',
'Temps pour une requête.',
buckets=[0.0001, 0.0002, 0.0005, 0.001, 0.01, 0.1])
Une correction possible pour le TP:
app.py:
from prometheus_client import Gauge, Counter, Histogram, start_http_server
import http.server
import random, time
REQUESTS = Counter('example_app_total', "Nombre de requete sur notre application d'exemple")
LATENCY = Histogram('example_app_latency_seconds', 'Temps pour une requête')
# INPROGRESS = Gauge('example_app_request_inprogress', 'Nombre de requetes en cours.')
# LAST = Gauge('example_app_request_last_time_seconds', "La dernière fois qu'une requete a été servie.")
EXCEPTIONS = Counter('example_app_exceptions_total', "Nombre d'exceptions pendant l'éxecution de l'app d'exemple")
class MyHandler(http.server.BaseHTTPRequestHandler):
# @INPROGRESS.track_inprogress()
@LATENCY.time()
def do_GET(self):
with EXCEPTIONS.count_exceptions():
if random.random() < 0.2:
raise Exception
if random.random() < 0.05:
time.sleep(3)
if random.random() < 0.1:
time.sleep(0.4)
self.send_response(200)
self.end_headers()
self.wfile.write(b"salut !")
# LAST.set_to_current_time()
REQUESTS.inc()
if __name__ == "__main__":
start_http_server(8001)
server = http.server.HTTPServer(('localhost', 8000), MyHandler)
server.serve_forever()
Vous pouvez créer un script poll_app.sh pour faire des requêtes automatiquement sur votre application:
for i in {1..10000}
do
curl "localhost:8000"
sleep(0.5)
done
Vous pouvez tester les requêtes suivantes dans Prometheus et essayer des graphs:
example_app_totalrate(example_app_total)example_app_exceptions_totalrate(example_app_exceptions_total[5m])rate(example_app_latency_count[5m])rate(example_app_latency_sum[5m])rate(example_app_latency_sum[5m]) / rate(example_app_latency_count[5m])example_app_latency_seconds_buckethistogram_quantile(0.95, example_app_latency_seconds_bucket)