prometheus : bien démarrer en java

temps de lecture ~14 min
  1. 1. Métriques java
    1. 1.1. VersionInfoExports
    2. 1.2. StandardExports
    3. 1.3. MemoryPoolsExports
    4. 1.4. BufferPoolsExports
    5. 1.5. GarbageCollectorExports
    6. 1.6. ThreadExports
    7. 1.7. ClassLoadingExports
  2. 2. Compilation
  3. 3. Point d’accès aux collectes
  4. 4. Instrumentation de notre code java
    1. 4.1. Métriques techniques
    2. 4.2. Métrique métier
  5. 5. Conclusion
  6. 6. à voir aussi

On a fait le tour des grands principes de prometheus et vu comment instrumenter du code python afin d’en tirer partie, ici on regarde les principes pour instrumenter du code java spring. N’étant pas un expert java, loin de là, ce code est probablement améliorable et optimisable, mais il fonctionne et permet de voir les grands principes.

L’intégration dans prometheus est bien entendu similaire à ce que nous avons vu avec python et c’est bien la force de cette solution, être agnostique au langage utilisé pour le développement. Nous sommes donc totalement intégré aux principes de développement de service au plus proche de la valeur finale et de la possibilité de refactoring ultérieur si nécessaire.

Cet article présente les métriques de base disponibles dans le module prometheus par défaut, elles sont assez riches pour nécessiter un chapitre complet. Puis nous verrons comment mettre en place le point d’accès au web service /metrics qui sera utilisé par prometheus pour la collecte, enfin, nous verrons comment instrumenter son code afin de mettre en oeuvre les métriques spécifiques (métier et technique).

Si vous n’avez pas vu toute la série d’article sur le sujet, voici la table des sujets abordés dans les différents articles :

  1. prometheus : concepts de base : introduction à prometheus, principes de fonctionnement, usages
  2. prometheus : bien démarrer en python : comment instrumenter son code python afin de fournir les services de métrologie pour prometheus
  3. prometheus et grafana : intégration de prometheus avec l’outil de dataviz grafana
  4. prometheus avec consul : intégration avec consul pour la découverte automatique des services à intégrer dans prometheus
  5. prometheus : bien démarrer en java : comment instrumenter du code java spring afin de remonter les métriques dans prometheus

Métriques java

Dans la section principale de notre application, il est nécessaire de mettre place les appels aux fonctions de base de prometheus. L’instrumentation est bien plus riche qu’en python et permet d’analyser plusieurs types de métriques. Elles sont regroupées dans la classe io.prometheus.client.hotspot :

  • VersionInfoExports : donne des informations sur la version de la JVM utilisée
  • StandardExports : informations similaires à celles des implémentations pour tous les langages
  • MemoryPoolsExports : utilisation de la mémoire par la JVM
  • BufferPoolsExports : utilisation des buffers
  • GarbageCollectorExports : informations sur le garbage collector de la JVM
  • ThreadExports : informations sur les threads
  • ClassLoadingExports : chargement des classes
  • DefaultExports : permet de charger tous les exporters, c’est riche mais suit automatiquement les versions afin de bénéficier des nouvelles implémentations dans la librarie

Les exemples de code présentés ci-dessous avec les résultats type sont basés sur le fichier Application.java, à la base se structure est la suivante :

Application.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import io.prometheus.client.hotspot.DefaultExports;
import io.prometheus.client.hotspot.StandardExports;
import io.prometheus.client.hotspot.GarbageCollectorExports;
import io.prometheus.client.hotspot.ThreadExports;
import io.prometheus.client.hotspot.ClassLoadingExports;
import io.prometheus.client.hotspot.VersionInfoExports;
import io.prometheus.client.hotspot.MemoryPoolsExports;
import io.prometheus.client.hotspot.BufferPoolsExports;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// prometheus register
SpringApplication.run(Application.class, args);
}
}

VersionInfoExports

Application.java main()
1
new VersionInfoExports().register();

exemple d’informations retournées :

1
2
3
4
5
# HELP jvm_info JVM version info
# TYPE jvm_info gauge
jvm_info{version="1.8.0_161-b14",\
vendor="Oracle Corporation",\
runtime="OpenJDK Runtime Environment",} 1.0

StandardExports

Application.java main()
1
new StandardExports().register();

exemple d’informations retournées :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 17.15
# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.528546237259E9
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 175.0
# HELP process_max_fds Maximum number of open file descriptors.
# TYPE process_max_fds gauge
process_max_fds 4096.0
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 5.121171456E9
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 4.66386944E8

On y retrouve le temps cpu consommé, l’heure de démarrage, les descripteurs de fichiers utilisés et totaux, les indicateurs de mémoire.

MemoryPoolsExports

Application.java main()
1
new MemoryPoolsExports().register();

Pour chaque type d’indicateur, on trouve des valeurs pour : used, committed, max, init, je ne mets que la partie used ici pour limiter l’affichage

1
2
3
4
5
6
7
8
9
10
11
12
13
# HELP jvm_memory_bytes_used Used bytes of a given JVM memory area.
# TYPE jvm_memory_bytes_used gauge
jvm_memory_bytes_used{area="heap",} 9.38982E7
jvm_memory_bytes_used{area="nonheap",} 7.3526512E7
# HELP jvm_memory_pool_bytes_used Used bytes of a given JVM memory pool.
# TYPE jvm_memory_pool_bytes_used gauge
jvm_memory_pool_bytes_used{pool="Code Cache",} 1.7278976E7
jvm_memory_pool_bytes_used{pool="Metaspace",} 5.0197608E7
jvm_memory_pool_bytes_used{pool="Compressed Class Space",} 6049928.0
jvm_memory_pool_bytes_used{pool="PS Eden Space",} 4.0022344E7
jvm_memory_pool_bytes_used{pool="PS Survivor Space",} 1.4148208E7
jvm_memory_pool_bytes_used{pool="PS Old Gen",} 3.9727648E7

BufferPoolsExports

Application.java main()
1
new BufferPoolsExports().register();
1
2
3
4
5
6
7
8
9
10
11
12
# HELP jvm_buffer_pool_used_bytes Used bytes of a given JVM buffer pool.
# TYPE jvm_buffer_pool_used_bytes gauge
jvm_buffer_pool_used_bytes{pool="direct",} 40960.0
jvm_buffer_pool_used_bytes{pool="mapped",} 0.0
# HELP jvm_buffer_pool_capacity_bytes Bytes capacity of a given JVM buffer pool.
# TYPE jvm_buffer_pool_capacity_bytes gauge
jvm_buffer_pool_capacity_bytes{pool="direct",} 40960.0
jvm_buffer_pool_capacity_bytes{pool="mapped",} 0.0
# HELP jvm_buffer_pool_used_buffers Used buffers of a given JVM buffer pool.
# TYPE jvm_buffer_pool_used_buffers gauge
jvm_buffer_pool_used_buffers{pool="direct",} 5.0
jvm_buffer_pool_used_buffers{pool="mapped",} 0.0

GarbageCollectorExports

Application.java main()
1
new GarbageCollectorExports().register();
1
2
3
4
5
6
# HELP jvm_gc_collection_seconds Time spent in a given JVM garbage collector in seconds.
# TYPE jvm_gc_collection_seconds summary
jvm_gc_collection_seconds_count{gc="PS Scavenge",} 12.0
jvm_gc_collection_seconds_sum{gc="PS Scavenge",} 0.101
jvm_gc_collection_seconds_count{gc="PS MarkSweep",} 2.0
jvm_gc_collection_seconds_sum{gc="PS MarkSweep",} 0.112

ThreadExports

Application.java main()
1
new ThreadExports().register();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# HELP jvm_threads_current Current thread count of a JVM
# TYPE jvm_threads_current gauge
jvm_threads_current 22.0
# HELP jvm_threads_daemon Daemon thread count of a JVM
# TYPE jvm_threads_daemon gauge
jvm_threads_daemon 20.0
# HELP jvm_threads_peak Peak thread count of a JVM
# TYPE jvm_threads_peak gauge
jvm_threads_peak 25.0
# HELP jvm_threads_started_total Started thread count of a JVM
# TYPE jvm_threads_started_total counter
jvm_threads_started_total 27.0
# HELP jvm_threads_deadlocked Cycles of JVM-threads that are in deadlock waiting to acquire object monitors or ownable synchronizers
# TYPE jvm_threads_deadlocked gauge
jvm_threads_deadlocked 0.0
# HELP jvm_threads_deadlocked_monitor Cycles of JVM-threads that are in deadlock waiting to acquire object monitors
# TYPE jvm_threads_deadlocked_monitor gauge
jvm_threads_deadlocked_monitor 0.0

ClassLoadingExports

Application.java main()
1
new ClassLoadingExports().register();
1
2
3
4
5
6
7
8
9
# HELP jvm_classes_loaded The number of classes that are currently loaded in the JVM
# TYPE jvm_classes_loaded gauge
jvm_classes_loaded 9368.0
# HELP jvm_classes_loaded_total The total number of classes that have been loaded since the JVM has started execution
# TYPE jvm_classes_loaded_total counter
jvm_classes_loaded_total 9368.0
# HELP jvm_classes_unloaded_total The total number of classes that have been unloaded since the JVM has started execution
# TYPE jvm_classes_unloaded_total counter
jvm_classes_unloaded_total 0.0

Compilation

Si vous avez l’habitude d’utiliser maven pour générer vos artefacts, voici la configuration spécifique que j’ai utilisée pour ce labo :

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient_hotspot</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient_httpserver</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient_pushgateway</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient_common</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient_spring_web</artifactId>
<version>0.4.0</version>
</dependency>

Si vous débutez, la commande de compilation et execution du service utilisée ici est :

1
mvn spring-boot:run

Point d’accès aux collectes

Dans le contrôleur mis en oeuvre dans notre web service simple, nous ajoutons le point d’accès pour /metrics. La chaîne de caractères retournée est directement construite par la librairie, on le prépare dans un writer de type String :

GreetingController.java
1
2
3
4
5
6
7
8
@RequestMapping("/metrics")
public String metrics() throws IOException {
StringWriter writer = new StringWriter();
TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples());
writer.flush();
return writer.toString();
}

Comme on ajoute un point d’accès, la méthode n’est pas modifiée, elle reprend l’ensemble de vos paramètres Spring, dans mon cas le service est disponible sur le port 8080 et répond comme on a pu le voir pour le python et dans l’incorporation des métriques dans prometheus.

Instrumentation de notre code java

Métriques techniques

Dans cette première partie on ne souhaite que récupérer des informations sur l’appel à la fonction servant l’appel au web service. La métrique est qualifiée de technique puisqu’elle ne véhicule pas de métrique relatives au métier (ce à quoi sert le web service).

Au niveau de la classe GreetingController, on ajoute un compteur d’appel (lignes 2) et on l’incrémente dans la fonction (ligne 7):

GreetingController.java
1
2
3
4
5
6
7
8
9
10
11
public class GreetingController {
static final Counter requests = Counter.build()
@RequestMapping("/greeting")
public Greeting greeting(@RequestParam(value="name",
defaultValue="World") String name) {
requests.inc();
return new Greeting(counter.incrementAndGet(),
String.format(template, name));
}
}

Le résultat, après 3 sollicitations au service, ressemble à :

1
2
3
# HELP ws_requests_total Total requests.
# TYPE ws_requests_total counter
ws_requests_total 3.0

Si on souhaite avoir des informations sur le temps de traitement, on remplace par une métrique de type summary avec un démarrage du chronomètre en début de fonction et l’arrêt avant le renvoi des résultats :

GreetingController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class GreetingController {
static final Summary requestLatencyS = Summary.build()
.name("ws_requests_latency_seconds_summary")
.help("Request latency in seconds.").register();
@RequestMapping("/greeting")
public Greeting greeting(@RequestParam(value="name",
defaultValue="World") String name) {
Summary.Timer requestTimerS = requestLatencyS.startTimer();
Greeting r = new Greeting(counter.incrementAndGet(),
String.format(template, name));
requestTimerS.observeDuration();
return r;
}
}

La fonction du web service comporte une boucle d’attente avec un délai aléatoire jusqu’à la seconde de façon à rendre les choses plus visibles.

Le résultat, après quelques sollicitations au service, ressemble à :

1
2
3
4
# HELP ws_requests_latency_seconds_summary Request latency in seconds.
# TYPE ws_requests_latency_seconds_summary summary
ws_requests_latency_seconds_summary_count 15.0
ws_requests_latency_seconds_summary_sum 8.221315953000001

Soit 15 sollicitations pour un peu plus de 8 secondes de délai de traitement.

On peut également ajouter la collecte de la répartition des valeurs en quantiles, beaucoup plus juste de le faire au niveau de la collecte que dans l’outil de traitement, qu’il soit prometheus ou grafana. Par exemple, sur les tranches min-max avec des écarts de 25%, on ajoute dans la définition du summary les lignes spécifiques :

GreetingController.java
1
2
3
4
5
6
7
8
static final Summary requestLatencyS = Summary.build()
.name("ws_requests_latency_seconds_summary")
.quantile(0, 0.0)
.quantile(0.25, 0.0)
.quantile(0.5, 0.05)
.quantile(0.75, 0.0)
.quantile(1, 0.0)
.help("Request latency in seconds.").register();

Le résultat précédent, après quelques sollicitations au service est enrichi des quantiles sur les lignes 5 à 9 :

1
2
3
4
5
6
7
8
9
# HELP ws_requests_latency_seconds_summary Request latency in seconds.
# TYPE ws_requests_latency_seconds_summary summary
ws_requests_latency_seconds_summary_count 15.0
ws_requests_latency_seconds_summary_sum 8.221315953000001
ws_requests_latency_seconds_summary{quantile="0.0",} 0.012073516
ws_requests_latency_seconds_summary{quantile="0.25",} 0.065027916
ws_requests_latency_seconds_summary{quantile="0.5",} 0.550409632
ws_requests_latency_seconds_summary{quantile="0.75",} 0.73307131
ws_requests_latency_seconds_summary{quantile="1.0",} 0.955002637

Métrique métier

Dans cette partie on souhaite récupérer des informations relatives au fonctionnement métier de notre web service, ce à quoi il sert donc. On commence par un simple compteur sur le nombre d’appel (pas d’intérêt ici pour l’instant) :

Au niveau de la classe Greeting, on ajoute un compteur d’appel (lignes 2 et 3) et on l’incrémente dans la fonction (ligne 6):

Greeting.java
1
2
3
4
5
6
7
8
public class Greeting {
static final Counter requests = Counter.build()
.name("greeting_count").help("Total greeting requests.").register();
public Greeting(long id, String content) {
requests.inc();
}
}

Le résultat sans surprise :

1
2
3
# HELP greeting_count Total greeting requests.
# TYPE greeting_count counter
greeting_count 3.0

Ajoutons des labels à notre compteur, le premier donne un information si on pense que notre temps de traitement est rapide (<250ms) et le second comptabilise si le temps de traitement est pair :

Greeting.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Greeting {
static final String[] labelNames = new String[]{"speed", "even"};
static final Counter requestsL = Counter.build()
.name("greeting_count_label")
.help("Total greeting requests by speed")
.labelNames(labelNames)
.register();
public Greeting(long id, String content) {
String sEven = "no";
if (t%2 == 0) {
sEven = "yes";
}
if (t<250) {
requestsL.labels(new String[]{"fast", sEven}).inc();
} else {
requestsL.labels(new String[]{"slow", sEven}).inc();
}

Le résultat :

1
2
3
4
5
6
# HELP greeting_count_label Total greeting requests by speed
# TYPE greeting_count_label counter
greeting_count_label{speed="fast",even="yes",} 2.0
greeting_count_label{speed="slow",even="no",} 5.0
greeting_count_label{speed="fast",even="no",} 1.0
greeting_count_label{speed="slow",even="yes",} 7.0

Conclusion

Il reste encore beaucoup d’objet à explorer, je vous laisse avancer avec les gauges, les histogrammes, vous avez les sources sur le git comme pour les autres articles dans le répertoire 04-java.

Nous terminons ici une première série d’articles sur prometheus, son utilisation et l’instrumentation de codes en java et en python, de quoi déjà couvrir quelques usages. Il existe des librairies standard pour le go et le ruby et vous trouverez des librairies tiers pour la plupart des autres langages, dont .net, node.js, php ou encore rust.

Have fun et dites moi si cela vous a servi à quelque chose dans la vraie vie…

Photo Sergey Svechnikov

Alexandre Chauvin Hameau

2018-06-09T13:27:26