Metriche con Spring Boot Actuator
Spring Boot permette di utilizzare una serie di feature per monitorare le nostre applicazioni che vanno sotto il nome di Spring Boot Actuator
In questo articolo vediamo come abilitare ed utilizzare Actuator all’interno della nostra applicazione Spring Boot. Stiamo parlando di informazioni specifiche della nostra applicazione che vengono rese disponibili attraverso degli endpoint (url) che pubblicano dati in formato JSON. Sicuramente gli sviluppatori che incominciano ad avere qualche capello bianco vedranno delle similitudini tra questo sistema e il vecchio ma ancora vivo JMX (Java Management Extensions), che infatti è uno dei modi attraverso il quale Spring Boot Actuator può pubblicare le informazioni.
La prima cosa che dobbiamo fare è aggiungere la dipendenza al nostro progetto
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
Così facendo abbiamo incluso nel nostro progetto gli endpoint Actuator, ora dobbiamo abilitarli perchè per default sono visibili solo a chi è autenticato nella nostra applicazione. Per fare ciò dobbiamo aggiungere una proprietà nel nostro progetto, nel file application.yml
management: security: enabled: false
Ora possiamo visualizzare tutte le informazioni che sono disponibili attraverso i diversi endpoint pubblicati da Actuator. Per avere una lista completa potete vedere la documentazione ufficiale, ma per incominciare subito a visualizzarli nella nostra applicazione d’esempio potete richiamare l’url http://localhost:8080/actuator . Infatti noi abbiamo incluso anche la dipendenza a Spring Data REST HAL Browser, che ci permette di visualizzare le informazioni JSON che vengono ritornate in maniera molto veloce.
Andando ad esempio sull’endpoint /health riceveremo le seguenti informazioni sulle condizioni della nostra applicazione
{ "status": "UP", "diskSpace": { "status": "UP", "total": 482730139648, "free": 405372182528, "threshold": 10485760 }, "db": { "status": "UP", "database": "H2", "hello": 1 } }
Tutti gli endpoint sono configurabili attraverso i properties della nostra applicazione, per abilitarli o per customizzare qualche comportamento. Esiste anche l’endpoint shutdown, disabilitato per default, che permette addirittura di spegnere la nostra applicazione. Se volessimo abilitarlo dovremmo andare ad operare sulla configurazione richiamando lo specifico endpoint
endpoints: shutdown: enabled: true
e poi richiamando il path /shutdown con il metodo HTTP POST potremo spegnere la nostra applicazione. Magari questa funzionalità potrebbe tornare utile in alcuni casi, ovviamente riabilitando quantomeno l’autenticazione!!
Health
All’interno della nostra applicazione possono esserci diversi servizi esterni di cui vogliamo avere informazioni sullo stato di “salute”. Avviando semplicemente la nostra applicazione vediamo che avendo configurato anche il database H2 tra le nostre dipendenze, quando andiamo a richiamare il path /health riceviamo le informazioni sullo status anche del db. Può essere molto utile agganciarsi a questo sistema per pubblicare le informazioni sullo stato di salute di un servizio che viene gestito dalla nostra applicazione. Per fare ciò dobbiamo creare un’implementazione dell’interfaccia HealthIndicator
package com.javastaff.spring.boot.actuator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisShardInfo; import redis.clients.jedis.Protocol; @Component public class MyCustomServiceHealthIndicator implements HealthIndicator { private static final Logger LOG = LoggerFactory.getLogger(MyCustomServiceHealthIndicator.class); @Override public Health health() { int status = redisCheck(); if (status == 0) { return Health.down().build(); } else { String lastItem=redisLastItem(); return Health.up().withDetail("lastItem", lastItem).build(); } } private String redisLastItem() { JedisShardInfo shardInfo = new JedisShardInfo("localhost", Protocol.DEFAULT_PORT); Jedis jedis = new Jedis(shardInfo); String lastItem=jedis.randomKey(); jedis.close(); return lastItem; } public int redisCheck() { LOG.info("Checking REDIS..."); int status=0; try { JedisShardInfo shardInfo = new JedisShardInfo("localhost", Protocol.DEFAULT_PORT); Jedis jedis = new Jedis(shardInfo); String pong=jedis.ping(); if (pong.equals("PONG")) status=1; jedis.close(); } catch (Exception ex) { LOG.error(ex.getMessage()); } return status; } }
Il servizio che vogliamo pubblicare sotto /health è un nostro server Redis. Il metodo che viene richiamato è health() che va a verificare la connessione a Redis con un classico PING/PONG e poi, se il servizio è online, recupera una chiave random. Ora se andiamo a richiamare l’endpoint health troveremo anche il nostro servizio
{ "status": "UP", "myCustomService": { "status": "UP", "lastItem": "36f4d627-a65f-df27-d1c6-488042afd9bd" }, "diskSpace": { "status": "UP", "total": 482730139648, "free": 405346942976, "threshold": 10485760 }, "db": { "status": "UP", "database": "H2", "hello": 1 } }
Metriche
Altro endpoint che sicuramente può essere utile nelle nostre applicazioni è quello relativo alle metriche, /metrics. Per default vengono visualizzate varie metriche standard come quelle riportate di seguito
"mem": 388887, "mem.free": 248732, "processors": 4, "instance.uptime": 533884, "uptime": 543483, "systemload.average": 2, "heap.committed": 319488, "heap.init": 126976, "heap.used": 70755, "heap": 1797120, "nonheap.committed": 70720, "nonheap.init": 2496, "nonheap.used": 69407, "nonheap": 0, "threads.peak": 24, "threads.daemon": 21, "threads.totalStarted": 30, "threads": 24, "classes": 9402, "classes.loaded": 9402, "classes.unloaded": 0,
Oltre alle metriche standard possiamo aggiungere ovviamente dei nostri valori utilizzando delle variabili che possono essere gestite semplicemente incrementandole/decrementandole (counter) o settandone direttamente il valore (gauge). Nella seguente classe andiamo a popolare due di queste variabili utilizzando i servizi di default messi a disposizione da Spring Boot Actuator
@Service public class ServiceWithMetrics { @Autowired private CounterService counterService; @Autowired private GaugeService gaugeService; @PostConstruct public void faccioCose() { counterService.increment("faccio.cose"); gaugeService.submit("faccio.grandi.cose", 123); } }
Richiamando adesso l’endpoint /metrics troveremo ora anche le nostre nuove metriche. Come in tutti gli altri casi quando parliamo di Spring Boot, non stiamo parlando di feature spettacolari, ma comunque sono quelle classiche cose che devono essere implementate nelle nostre applicazioni e averle già pronte per l’utilizzo è sicuramente un ottimo punto di partenza.
I nostri servizi possono produrre delle metriche che rendiamo disponibili tramite questi endpoint REST, ma potrebbe essere utile inviare queste informazioni anche verso altri sistemi. Proprio per questo motivo è stato implementato un sistema per esportare ed aggregare le metriche che stiamo producendo. Qui di seguito vedete un semplice esempio di configurazione che esporta le metriche su un server Redis
package com.javastaff.spring.boot.actuator; import org.springframework.boot.actuate.autoconfigure.ExportMetricWriter; import org.springframework.boot.actuate.metrics.export.MetricExportProperties; import org.springframework.boot.actuate.metrics.repository.redis.RedisMetricRepository; import org.springframework.boot.actuate.metrics.writer.MetricWriter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import redis.clients.jedis.JedisShardInfo; import redis.clients.jedis.Protocol; @Configuration public class CustomExport { @Bean @ExportMetricWriter MetricWriter metricWriter(MetricExportProperties export) { JedisShardInfo shardInfo = new JedisShardInfo("localhost", Protocol.DEFAULT_PORT); JedisConnectionFactory factory= new JedisConnectionFactory(shardInfo); return new RedisMetricRepository(factory, export.getRedis().getPrefix(), export.getRedis().getKey()); } }
Attraverso l’annotation ExportMetricWriter andiamo a dire a Spring dove vogliamo scrivere le nostre metriche. Per far funzionare il precedente esempio dobbiamo inserire nel nostro application.yaml la chiave e il prefisso che utilizzeremo per le informazioni su Redis
spring: metrics: export: redis: prefix: javastaff.actuator.test.${random.value:0000} key: keys.javastaff.actuator
Se proviamo ad andare su Redis vedremo quindi le informazioni delle metriche che vengono salvate
federico@work:~$ redis-cli 127.0.0.1:6379> zrange keys.javastaff.actuator 0 -1 WITHSCORES 1) "javastaff.actuator.test.89816aa5c1b8af7330215bd28e903cf9.counter.status.200.actuator" 2) "1" 3) "javastaff.actuator.test.89816aa5c1b8af7330215bd28e903cf9.counter.status.200.health" 4) "1" 5) "javastaff.actuator.test.89816aa5c1b8af7330215bd28e903cf9.counter.status.200.metrics" 6) "1" 7) "javastaff.actuator.test.89816aa5c1b8af7330215bd28e903cf9.gauge.response.actuator.star-star" 8) "11" 9) "javastaff.actuator.test.89816aa5c1b8af7330215bd28e903cf9.counter.status.200.actuator.star-star" 10) "35" 11) "javastaff.actuator.test.89816aa5c1b8af7330215bd28e903cf9.gauge.response.health" 12) "55" 13) "javastaff.actuator.test.89816aa5c1b8af7330215bd28e903cf9.gauge.response.metrics" 14) "144" 15) "javastaff.actuator.test.89816aa5c1b8af7330215bd28e903cf9.gauge.response.actuator" 16) "196" 127.0.0.1:6379>
Per maggiori informazioni su questo argomento vi rimando alla documentazione ufficiale.
Custom Endpoint
Con tutti questi endpoint a disposizione viene ovviamente la voglia di farne uno nostro, che possa ritornare informazioni specifiche per la nostra applicazione. Ecco quindi come fare, implementando l’interfaccia Endpoint
package com.javastaff.spring.boot.actuator; import java.util.Arrays; import java.util.HashMap; import java.util.List; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.stereotype.Component; @Component public class CustomEndpoint implements Endpoint<HashMap<String, List<String>>> { public String getId() { return "customEndpoint"; } public boolean isEnabled() { return true; } public boolean isSensitive() { return true; } public HashMap<String, List<String>> invoke() { HashMap<String, List<String>> map = new HashMap<>(); map.put("enabled", Arrays.asList("feature1","feature2")); map.put("notAllowed", Arrays.asList("command1","command2")); return map; } }
in questo caso ritorniamo dei valori d’esempio all’interno di un HashMap ma avremmo potuto utilizzare anche altre strutture dati. Recuperando la lista degli endpoint da /actuator troveremo il nostro che è stato pubblicato sotto l’url /customEndPoint . Effettuando la chiamata riceveremo l’HashMap in formato JSON
{ "notAllowed": [ "command1", "command2" ], "enabled": [ "feature1", "feature2" ] }

Looking for a right “about me”…
Una risposta
[…] I liked this Italian-language post by Federico Paparoni on using the Spring Boot Actuator for metrics […]