Monitorizar una aplicación Java de Spring Boot con Micrometer, Prometheus y Grafana

Escrito por picodotdev el .
java planeta-codigo programacion software
Comentarios

Java
Promehteus
Grafana

Los proyectos de Spring no son tan conservadores como Java EE o ahora Jakata EE y se desarrollan a una velocidad mayor cubriendo de forma más temprana las necesidades de los programadores según evolucionan las tecnologías y se adoptan nuevos modelos de arquitectura.

Con el advenimiento de los microservicios, contenedores, la nube y aplicaciones autocontenidas Spring se ha adaptado con proyectos como Spring Boot y Spring Cloud. En el asunto que ocupa este artículo de métricas con la versión 2 de Spring Boot se ha adoptado Micrometer como librería para proporcionar las métricas.

Micrometer permite exportar a cualquiera de los más populares sistemas de monitorización los datos de las métricas. Usando Micrometer la aplicación se abstrae del sistema de métricas empleado pudiendo cambiar en un futuro si se desea. Uno de los sistemas más populares de monitorización es Prometheus que se encarga de recoger y almacenar los datos de las métricas expuestas por las aplicaciones y ofrece un lenguaje de consulta de los datos con el que otras aplicaciones pueden visualizarlos en gráficas y paneles de control. Grafana es una de estas herramientas que permite visualizar los datos proporcionados por Prometheus. Estos sistemas de monitorización ofrecen un sistema de alertas que se integran entre otros con Slack.

En el artículo Información y métricas de la aplicación con Spring Boot Actuator mostraba como configurar Spring Boot y Spring Boot Actuator para exponer métricas en el endpoint /actuator/metrics, con estas herramientas solo se exponen la clave y valor de cada métrica y solo en un momento dado. Pueden ser métricas del servicio como cantidad de CPU usada, memoria consumida y libre, espacio en almacenamiento, etc… o métricas de aplicación como número de peticiones realizadas al servicio, tiempo de respuesta, etc… Una de las funcionalidades de Prometheus es recolectar cada cierto tiempo los valores de estas métricas que da lugar a una colección de datos que varía en el tiempo y que Grafana puede visualizar en gráficas para una mucha mayor facilidad de comprensión que la enorme cantidad de datos en crudo.

Usando Spring Boot 2 exportar los datos para Prometheus es realmente sencillo, basta con incluir la dependencia io.micrometer:micrometer-registry-prometheus mediante la herramienta de construcción, por ejemplo Gradle, y automáticamente se expone en el endpoint /actuator/prometheus con la información de las métricas en el formato que espera Prometheus para recolectarla.

 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
31
plugins {
    id 'application'
    id 'org.springframework.boot' version '2.0.7.RELEASE'
    id 'io.spring.dependency-management' version '1.0.6.RELEASE'
}

mainClassName = 'io.github.picodotdev.blogbitix.springcloud.service.Main'

dependencyManagement {
    imports {
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Finchley.SR2'
    }
}

dependencies {
    // Spring
 def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
    compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
    compile("org.springframework.boot:spring-boot-starter-web", excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
    compile("org.springframework.boot:spring-boot-starter-actuator", excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-config', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-bus-amqp', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client', excludeSpringBootStarterLogging)
    compile('io.micrometer:micrometer-registry-prometheus:1.0.8')
    
    runtime('com.google.code.gson:gson:2.8.5')
    runtime('com.fasterxml.jackson.core:jackson-databind:2.9.6')
    runtime('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')
}

Micrometer y Prometheus ofrecen varios tipos de métricas:

  • Counter: representa un valor que se va incrementando a lo largo del tiempo. Puede ser el número de invocaciones recibidas por servicio.
  • Gauge: representa un valor que arbitrariamente puede subir o bajar. Puede ser la cantidad de memoria usada.
  • Timer: mide periodos de tiempo. Puede ser el tiempo de respuesta empleado para atender una petición de un servicio.
  • Distribution summaries: recolecta la distribución de una serie de datos con los que se pueden obtener percentiles.

Utilizando el ejemplo que hice para la serie de artículos sobre Spring Cloud he añadido al micro servicio service un contador con el número de invocaciones que se le ha realizado. Este dato se expone en el endpoint con la clave service.invocations como se ha definido al registrar el contador en Micrometer con la clase MeterRegistry. Además de esta métrica propia del servicio Spring Boot Actuator añade otras muchas más del uso de la CPU, memoria, …

Una clase de una aplicación de Spring Boot que utiliza un Counter.

 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
31
32
33
34
35
36
package io.github.picodotdev.blogbitix.springcloud.service;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.Random;

@RestController
public class DefaultController {

    @Autowired
    private DefaultConfiguration configuration;

    private Random random;
    private Counter counter;

    public DefaultController(MeterRegistry registry) {
        this.random = new Random();
        this.counter = Counter.builder("service.invocations").description("Total service invocations").register(registry);
    }

    @RequestMapping("/")
    public String home(HttpServletRequest request) throws Exception {
        counter.increment();

        // Timeout simulation
     //Thread.sleep(random.nextInt(2000));

        return String.format("Hello world (%s, %s)", request.getRequestURL(), configuration.getKey());
    }
}

Las claves de las métricas por defecto exportadas por Spring Boot Actuator.

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{
   "names":[
      "jvm.memory.max",
      "jvm.threads.states",
      "http.server.requests",
      "process.files.max",
      "jvm.gc.memory.promoted",
      "tomcat.cache.hit",
      "rabbitmq.channels",
      "system.load.average.1m",
      "tomcat.cache.access",
      "jvm.memory.used",
      "jvm.gc.max.data.size",
      "jvm.gc.pause",
      "jvm.memory.committed",
      "system.cpu.count",
      "spring.integration.channels",
      "rabbitmq.connections",
      "tomcat.global.sent",
      "jvm.buffer.memory.used",
      "tomcat.sessions.created",
      "jvm.threads.daemon",
      "system.cpu.usage",
      "jvm.gc.memory.allocated",
      "tomcat.global.request.max",
      "tomcat.global.request",
      "rabbitmq.rejected",
      "tomcat.sessions.expired",
      "jvm.threads.live",
      "jvm.threads.peak",
      "tomcat.global.received",
      "process.uptime",
      "tomcat.sessions.rejected",
      "process.cpu.usage",
      "tomcat.threads.config.max",
      "jvm.classes.loaded",
      "rabbitmq.consumed",
      "jvm.classes.unloaded",
      "tomcat.global.error",
      "tomcat.sessions.active.current",
      "service.invocations",
      "tomcat.sessions.alive.max",
      "jvm.gc.live.data.size",
      "tomcat.servlet.request.max",
      "tomcat.threads.current",
      "tomcat.servlet.request",
      "process.files.open",
      "jvm.buffer.count",
      "jvm.buffer.total.capacity",
      "tomcat.sessions.active.max",
      "spring.integration.handlers",
      "tomcat.threads.busy",
      "rabbitmq.published",
      "process.start.time",
      "tomcat.servlet.error",
      "spring.integration.sources",
      "rabbitmq.acknowledged"
   ]
}

Los datos de una métrica en el enpoint /actuator/metrics/service.invocations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
   "name":"service.invocations",
   "description":"Total service invocations",
   "baseUnit":null,
   "measurements":[
      {
         "statistic":"COUNT",
         "value":1.0
      }
   ],
   "availableTags":[

   ]
}

Y las mismas métricas en el formato que espera Prometheus.

  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
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# HELP jvm_buffer_total_capacity_bytes An estimate of the total capacity of the buffers in this pool
# TYPE jvm_buffer_total_capacity_bytes gauge
jvm_buffer_total_capacity_bytes{id="direct",} 40960.0
jvm_buffer_total_capacity_bytes{id="mapped",} 0.0
# HELP process_files_max_files The maximum file descriptor count
# TYPE process_files_max_files gauge
process_files_max_files 4096.0
# HELP system_cpu_count The number of processors available to the Java virtual machine
# TYPE system_cpu_count gauge
system_cpu_count 8.0
# HELP tomcat_global_request_seconds  
# TYPE tomcat_global_request_seconds summary
tomcat_global_request_seconds_count{name="http-nio-8080",} 2.0
tomcat_global_request_seconds_sum{name="http-nio-8080",} 0.111
# HELP jvm_threads_states_threads The current number of threads having NEW state
# TYPE jvm_threads_states_threads gauge
jvm_threads_states_threads{state="runnable",} 7.0
jvm_threads_states_threads{state="blocked",} 0.0
jvm_threads_states_threads{state="waiting",} 16.0
jvm_threads_states_threads{state="timed-waiting",} 9.0
jvm_threads_states_threads{state="new",} 0.0
jvm_threads_states_threads{state="terminated",} 0.0
# HELP tomcat_cache_access_total  
# TYPE tomcat_cache_access_total counter
tomcat_cache_access_total 0.0
# HELP rabbitmq_acknowledged_total  
# TYPE rabbitmq_acknowledged_total counter
rabbitmq_acknowledged_total{name="rabbit",} 0.0
# HELP process_uptime_seconds The uptime of the Java virtual machine
# TYPE process_uptime_seconds gauge
process_uptime_seconds 46.087
# HELP jvm_gc_memory_allocated_bytes_total Incremented for an increase in the size of the young generation memory pool after one GC to before the next
# TYPE jvm_gc_memory_allocated_bytes_total counter
jvm_gc_memory_allocated_bytes_total 8.47628712E8
# HELP rabbitmq_consumed_total  
# TYPE rabbitmq_consumed_total counter
rabbitmq_consumed_total{name="rabbit",} 0.0
# HELP tomcat_global_error_total  
# TYPE tomcat_global_error_total counter
tomcat_global_error_total{name="http-nio-8080",} 0.0
# HELP jvm_threads_peak_threads The peak live thread count since the Java virtual machine started or peak was reset
# TYPE jvm_threads_peak_threads gauge
jvm_threads_peak_threads 34.0
# HELP jvm_gc_pause_seconds Time spent in GC pause
# TYPE jvm_gc_pause_seconds summary
jvm_gc_pause_seconds_count{action="end of major GC",cause="Metadata GC Threshold",} 1.0
jvm_gc_pause_seconds_sum{action="end of major GC",cause="Metadata GC Threshold",} 0.137
jvm_gc_pause_seconds_count{action="end of minor GC",cause="Metadata GC Threshold",} 1.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Metadata GC Threshold",} 0.014
jvm_gc_pause_seconds_count{action="end of minor GC",cause="Allocation Failure",} 2.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Allocation Failure",} 0.052
# HELP jvm_gc_pause_seconds_max Time spent in GC pause
# TYPE jvm_gc_pause_seconds_max gauge
jvm_gc_pause_seconds_max{action="end of major GC",cause="Metadata GC Threshold",} 0.137
jvm_gc_pause_seconds_max{action="end of minor GC",cause="Metadata GC Threshold",} 0.014
jvm_gc_pause_seconds_max{action="end of minor GC",cause="Allocation Failure",} 0.033
# HELP http_server_requests_seconds  
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{exception="None",method="GET",status="200",uri="/",} 1.0
http_server_requests_seconds_sum{exception="None",method="GET",status="200",uri="/",} 0.038397291
http_server_requests_seconds_count{exception="None",method="GET",status="200",uri="/**/favicon.ico",} 1.0
http_server_requests_seconds_sum{exception="None",method="GET",status="200",uri="/**/favicon.ico",} 0.010500544
# HELP http_server_requests_seconds_max  
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{exception="None",method="GET",status="200",uri="/",} 0.038397291
http_server_requests_seconds_max{exception="None",method="GET",status="200",uri="/**/favicon.ico",} 0.010500544
# HELP tomcat_sessions_created_sessions_total  
# TYPE tomcat_sessions_created_sessions_total counter
tomcat_sessions_created_sessions_total 0.0
# HELP rabbitmq_published_total  
# TYPE rabbitmq_published_total counter
rabbitmq_published_total{name="rabbit",} 0.0
# HELP spring_integration_handlers The number of message handlers
# TYPE spring_integration_handlers gauge
spring_integration_handlers 2.0
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="PS Survivor Space",} 0.0
jvm_memory_used_bytes{area="heap",id="PS Old Gen",} 4.2640064E7
jvm_memory_used_bytes{area="heap",id="PS Eden Space",} 2.21190608E8
jvm_memory_used_bytes{area="nonheap",id="Metaspace",} 6.2586488E7
jvm_memory_used_bytes{area="nonheap",id="Code Cache",} 1.9553024E7
jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space",} 8220544.0
# HELP tomcat_servlet_error_total  
# TYPE tomcat_servlet_error_total counter
tomcat_servlet_error_total{name="default",} 0.0
# HELP rabbitmq_connections  
# TYPE rabbitmq_connections gauge
rabbitmq_connections{name="rabbit",} 0.0
# HELP spring_integration_sources The number of message sources
# TYPE spring_integration_sources gauge
spring_integration_sources 0.0
# HELP tomcat_sessions_active_max_sessions  
# TYPE tomcat_sessions_active_max_sessions gauge
tomcat_sessions_active_max_sessions 0.0
# HELP tomcat_global_received_bytes_total  
# TYPE tomcat_global_received_bytes_total counter
tomcat_global_received_bytes_total{name="http-nio-8080",} 0.0
# HELP tomcat_threads_current_threads  
# TYPE tomcat_threads_current_threads gauge
tomcat_threads_current_threads{name="http-nio-8080",} 10.0
# HELP tomcat_sessions_active_current_sessions  
# TYPE tomcat_sessions_active_current_sessions gauge
tomcat_sessions_active_current_sessions 0.0
# HELP tomcat_global_request_max_seconds  
# TYPE tomcat_global_request_max_seconds gauge
tomcat_global_request_max_seconds{name="http-nio-8080",} 0.1
# HELP tomcat_sessions_expired_sessions_total  
# TYPE tomcat_sessions_expired_sessions_total counter
tomcat_sessions_expired_sessions_total 0.0
# HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use
# TYPE jvm_memory_committed_bytes gauge
jvm_memory_committed_bytes{area="heap",id="PS Survivor Space",} 3.2505856E7
jvm_memory_committed_bytes{area="heap",id="PS Old Gen",} 3.00941312E8
jvm_memory_committed_bytes{area="heap",id="PS Eden Space",} 6.17086976E8
jvm_memory_committed_bytes{area="nonheap",id="Metaspace",} 6.6060288E7
jvm_memory_committed_bytes{area="nonheap",id="Code Cache",} 2.0512768E7
jvm_memory_committed_bytes{area="nonheap",id="Compressed Class Space",} 8912896.0
# HELP jvm_gc_memory_promoted_bytes_total Count of positive increases in the size of the old generation memory pool before GC to after GC
# TYPE jvm_gc_memory_promoted_bytes_total counter
jvm_gc_memory_promoted_bytes_total 2.5852976E7
# HELP system_load_average_1m The sum of the number of runnable entities queued to available processors and the number of runnable entities running on the available processors averaged over a period of time
# TYPE system_load_average_1m gauge
system_load_average_1m 1.23
# HELP system_cpu_usage The "recent cpu usage" for the whole system
# TYPE system_cpu_usage gauge
system_cpu_usage 0.09841353781068218
# HELP jvm_buffer_count_buffers An estimate of the number of buffers in the pool
# TYPE jvm_buffer_count_buffers gauge
jvm_buffer_count_buffers{id="direct",} 5.0
jvm_buffer_count_buffers{id="mapped",} 0.0
# HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management
# TYPE jvm_memory_max_bytes gauge
jvm_memory_max_bytes{area="heap",id="PS Survivor Space",} 3.2505856E7
jvm_memory_max_bytes{area="heap",id="PS Old Gen",} 5.59939584E9
jvm_memory_max_bytes{area="heap",id="PS Eden Space",} 2.735210496E9
jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
jvm_memory_max_bytes{area="nonheap",id="Code Cache",} 2.5165824E8
jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
# HELP jvm_threads_live_threads The current number of live threads including both daemon and non-daemon threads
# TYPE jvm_threads_live_threads gauge
jvm_threads_live_threads 32.0
# HELP service_invocations_total  
# TYPE service_invocations_total counter
service_invocations_total 1.0
# HELP tomcat_threads_config_max_threads  
# TYPE tomcat_threads_config_max_threads gauge
tomcat_threads_config_max_threads{name="http-nio-8080",} 200.0
# HELP jvm_threads_daemon_threads The current number of live daemon threads
# TYPE jvm_threads_daemon_threads gauge
jvm_threads_daemon_threads 29.0
# HELP process_start_time_seconds Start time of the process since unix epoch.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.544815302414E9
# HELP rabbitmq_rejected_total  
# TYPE rabbitmq_rejected_total counter
rabbitmq_rejected_total{name="rabbit",} 0.0
# HELP tomcat_servlet_request_max_seconds  
# TYPE tomcat_servlet_request_max_seconds gauge
tomcat_servlet_request_max_seconds{name="default",} 0.0
# HELP process_files_open_files The open file descriptor count
# TYPE process_files_open_files gauge
process_files_open_files 189.0
# HELP jvm_buffer_memory_used_bytes An estimate of the memory that the Java virtual machine is using for this buffer pool
# TYPE jvm_buffer_memory_used_bytes gauge
jvm_buffer_memory_used_bytes{id="direct",} 40960.0
jvm_buffer_memory_used_bytes{id="mapped",} 0.0
# HELP tomcat_threads_busy_threads  
# TYPE tomcat_threads_busy_threads gauge
tomcat_threads_busy_threads{name="http-nio-8080",} 1.0
# HELP jvm_gc_live_data_size_bytes Size of old generation memory pool after a full GC
# TYPE jvm_gc_live_data_size_bytes gauge
jvm_gc_live_data_size_bytes 4.2640064E7
# HELP tomcat_servlet_request_seconds  
# TYPE tomcat_servlet_request_seconds summary
tomcat_servlet_request_seconds_count{name="default",} 0.0
tomcat_servlet_request_seconds_sum{name="default",} 0.0
# HELP jvm_gc_max_data_size_bytes Max size of old generation memory pool
# TYPE jvm_gc_max_data_size_bytes gauge
jvm_gc_max_data_size_bytes 5.59939584E9
# HELP tomcat_sessions_alive_max_seconds  
# TYPE tomcat_sessions_alive_max_seconds gauge
tomcat_sessions_alive_max_seconds 0.0
# HELP tomcat_global_sent_bytes_total  
# TYPE tomcat_global_sent_bytes_total counter
tomcat_global_sent_bytes_total{name="http-nio-8080",} 1004.0
# HELP rabbitmq_channels  
# TYPE rabbitmq_channels gauge
rabbitmq_channels{name="rabbit",} 0.0
# HELP tomcat_sessions_rejected_sessions_total  
# TYPE tomcat_sessions_rejected_sessions_total counter
tomcat_sessions_rejected_sessions_total 0.0
# HELP spring_integration_channels The number of message channels
# TYPE spring_integration_channels gauge
spring_integration_channels 4.0
# HELP jvm_classes_unloaded_classes_total The total number of classes unloaded since the Java virtual machine has started execution
# TYPE jvm_classes_unloaded_classes_total counter
jvm_classes_unloaded_classes_total 0.0
# HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process
# TYPE process_cpu_usage gauge
process_cpu_usage 0.0012776292616022062
# HELP tomcat_cache_hit_total  
# TYPE tomcat_cache_hit_total counter
tomcat_cache_hit_total 0.0
# HELP jvm_classes_loaded_classes The number of classes that are currently loaded in the Java virtual machine
# TYPE jvm_classes_loaded_classes gauge
jvm_classes_loaded_classes 12793.0

Para iniciar el ejemplo de Spring Cloud que consta de un servicio de registro y descubrimiento, un servicio de configuración, un servicio del que se pueden iniciar varias instancias y un cliente que hace peticiones hay que utilizar la siguiente serie de comandos.

1
2
3
4
$ ./gradlew discoveryserver:run --args="--port=8761"
$ ./gradlew configserver:run --args="--port=8090"
$ ./gradlew service:run --args="--port=8080"
$ ./gradlew client:run

Una vez expuestas las métricas en el formato que espera Prometheus este ya puede recolectarlas. Para usar Prometheus y posteriormente Grafana de forma fácil evitando tener que instalar y configurar nada se puede usar Docker, en este caso con Docker Compose. En la serie de artículos sobre Docker explico que proporciona Docker y como usar las varias herramientas que ofrece.

El archivo de Docker Compose contiene dos contenedores uno para Prometheus y otro para Grafana, con sus archivos de configuración. En la configuración de Prometheus se crean un job que recolecta las métricas cada pocos segundos del servicio a través del endpoint de métricas. En la configuración de Grafana se añade como una fuente de datos Prometheus, se puede añadir otras varias.

1
2
$ docker-compose up
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
version: '3'
services:
  prometheus:
    image: prom/prometheus
    ports:
    - "9090:9090"
    volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
    network_mode: "host"
  grafana:
    image: grafana/grafana
    ports:
    - "3000:3000"
    volumes:
    - ./grafana-datasources.yml:/etc/grafana/provisioning/datasources/grafana-datasources.yml
    network_mode: "host"
    depends_on:
    - prometheus
1
2
3
4
5
6
7
8
scrape_configs:
  - job_name: 'spring-actuator'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
    - targets: ['service.127.0.0.1.xip.io:8080']
    tls_config:
      insecure_skip_verify: true
1
2
3
4
5
6
7
8
9
datasources:
- name: Prometheus
  type: prometheus
  access: direct
  url: http://prometheus.127.0.0.1.xip.io:9090
  basic_auth: false
  with_credentials: false
  is_default: true
  version: 1

Prometheus posee la funcionalidad básica de crear gŕaficas con las métricas recogidas pero no tiene la habilidad de crear paneles que recogen una colección de gráficas relacionadas o un editor de consultas más avanzado como tiene Grafana.

Métrica de la aplicación en Prometheus y Grafana

Una vez que Prometheus recolecta los datos de las métricas al introducir las expresiones se proporciona asistencia de código. Por otro lado, en la sección Status > Targets de Prometheus se puede ver el estado de los servicios de los que recolecta métricas.

Estado de los servicios rastreados por Prometheus

Como Spring Boot Actuator exporta muchas métricas del funcionamiento del servicio Grafana puede crear gráficas de todas ellas. No hace falta crear un dashboard desde cero, se pueden descargar e importar dashboards. Este ejemplo para Micrometer recoge la memoria de la JVM (heap y no heap), uso de CPU, carga, hilos, estado de hilos, descriptores de archivos, recolector de basura, classloader y entrada/salida básica.

Dashboard de una aplicación Spring Boot en Grafana

Grafana tiene plugins para añadir como fuentes de datos bases de datos relacionales para extraer mediante sentencias SQL y visualizar datos almacenados en MySQL, PostgreSQL u Oracle.

Con la información de las métricas se conoce más en detalle cual es el comportamiento normal de una aplicación y observar de forma rápida cuando se introducen cambios como afectan al comportamiento de la misma tanto de forma negativa como de forma positiva. En cualquier aplicación que ofrece un servicio es importante conocer su estado y actuar incluso antes de que ofrezca un mal comportamiento e incluso deje de prestar su servicio. La monitorización no sustituye sino que complementa un sistema de trazas que en una aplicación Java es común que se realice con SLF4J o Log4j.

Aún quedan algunas preguntas por responder ¿como agregar los datos de múltiples instancias? ¿si se crean nuevas instancias del servicio como puede conocer Prometheus los nuevos targets que se han creado? En el ejemplo solo hay una instancia del servicio y la configuración de Prometheus es proporcionada por un archivo estático. Aún desconozco las respuestas, no lo he investigado en detalle, hay alguna pregunta sobre este tema en StackOverflow y por la respuesta Prometheus no tiene un adaptador para Eureka entre los varios servicios de descubrimiento que sí soporta en su configuración.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew-run.sh, docker-compose up.