Spring Boot Actuator es una librería que proporciona out-of-the-box una amplia colección de funcionalidades de monitorización y administración para aplicaciones desarrolladas con Spring Boot accesibles mediante endpoints vía REST y/o con JMX Beans. En este tutorial aprenderemos a utilizar Spring Boot Actuator con la versión 2.1 de Spring Boot. Téngase en cuenta que hay numerosas e importantes diferencias en el módulo Actuator entre Spring Boot 1.x y Spring Boot 2.x
Proyecto de ejemplo
Vamos a partir de un proyecto web Spring Boot «limpio» que sólo utiliza el starter web y el correspondiente al módulo de actuator.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath /> </parent> <groupId>com.danielme.demo</groupId> <artifactId>spring-boot-actuator</artifactId> <version>0.0.1</version> <name>spring-boot-actuator</name> <description>Demo project for Spring Boot Actuator</description> <url>https://danielme.com/2019/04/09/spring-boot-actuator/</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
package com.danielme.demo.actuator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ActuatorApplication { public static void main(String[] args) { SpringApplication.run(ActuatorApplication.class, args); } }
Se incluye el wrapper de Maven por lo que la aplicación puede ejecutarse con un Tomcat embebido con el comando mvnw spring-boot:run
Los endpoints disponibles pueden consultarse vía REST en el path /actuator en la raíz de la aplicación aunque este valor puede configurarse en el application.properties con la propiedad management.endpoints.web.base-path (por seguridad es recomendable cambiarlo).
Nota: la especificación de los servicios REST (Spring Boot Web API) se encuentra aquí.
Por seguridad todos los servicios de actuator vienen deshabilitados excepto los dos que vemos en el listado y que vamos a explorar a continuación.
health
Este servicio proporciona información relativa al estado de la aplicación. Si hacemos un GET a la url http://localhost:8080/actuator/health obtendremos la siguiente información.
{ "status":"UP" }
Por seguridad el servicio health no ofrece más información a menos que activemos los detalles del mismo con la propiedad management.endpoint.health.show-details indicando el nivel de seguridad requerido al cliente que hace la consulta:
- always: muestra siempre toda la información. Por seguridad no es recomendable.
- when-authorized: muestra todos los datos sólo para los usuarios logados con Spring Security que tengan los roles indicados con la propiedad management.endpoint.health.roles
Más adelante abordaremos la securización de los servicios de Spring Boot Actuator así que de momento usaremos always. Los detalles del proyecto de ejemplo son los siguientes:
{ "status": "UP", "details": { "diskSpace": { "status": "UP", "details": { "total": 230773190656, "free": 11430903808, "threshold": 10485760 } } } }
La información mostrada puede variar en función de la configuración de la aplicación. Por ejemplo, para la aplicación del tutorial Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA se incluye el estado de la base de datos.
{ "status": "UP", "details": { "db": { "status": "UP", "details": { "database": "MySQL", "hello": 1 } }, "diskSpace": { "status": "UP", "details": { "total": 230773190656, "free": 11443109888, "threshold": 10485760 } } } }
La información del servicio health se obtiene a partir de los beans que implementen HealthIndicator, cada implementación indica un valor status (UP, DOWN) y toda la información adicional que se quiera incluir en el JSON. Esta respuesta se modela con la clase Health. Por ejemplo, la siguiente clase comprueba si hay conectividad desde la aplicación a una url que debe devolver un HTTP status 200 indicando el tiempo de respuesta de dicha url en milisegundos.
package com.danielme.demo.actuator.health; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.net.URL; import javax.net.ssl.HttpsURLConnection; 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; @Component public class HttpConnectivityHealthIndicator implements HealthIndicator { private static final String GOOGLE_URL = "https://www.google.com/"; private static final Logger logger = LoggerFactory .getLogger(HttpConnectivityHealthIndicator.class); @Override public Health health() { try { return checkConnectivity(); } catch (IOException ex) { return checkErroneous(ex); } } private Health checkConnectivity() throws IOException, IOException { HttpsURLConnection urlConnection = (HttpsURLConnection) new URL(GOOGLE_URL) .openConnection(); long millisStart = System.currentTimeMillis(); int responseCode = urlConnection.getResponseCode(); long millisDuration = System.currentTimeMillis() - millisStart; if (responseCode == 200) { return Health.up().withDetail("timeMillis", millisDuration).build(); } return Health.down().withDetail("timeMillis", millisDuration).build(); } private Health checkErroneous(IOException ex) { logger.error("cannot perform health check", ex); StringWriter trace = new StringWriter(); ex.printStackTrace(new PrintWriter(trace)); return Health.down().withDetail("IOException", trace.toString()).build(); } }
El resultado se incluye en el informe, el nombre del indicador se deduce del nombre de la clase.
{ "status": "UP", "details": { "httpConnectivity": { "status": "UP", "details": { "timeMillis": 271 } }, "diskSpace": { "status": "UP", "details": { "total": 230773190656, "free": 11437895680, "threshold": 10485760 } } } }
info
Este servicio está pensado para publicar información genérica y pública sobre la aplicación. Por ejemplo podemos mostrar las constantes definidas en el application.properties con el prefijo info
info.app.name=Spring Boot Actuator Demo info.app.web=https://danielme.com/spring/ info.hello=Hi there!!!
{ "app": { "name": "Spring Boot Actuator Demo", "web": "https://danielme.com/spring/" }, "hello": "Hi there!!!" }
De forma automática también se añaden a la información proporcionada por /info las propiedades definidas en los ficheros git.properties y META-INF/build-info.properties. Al igual que en el caso del endpoint /health, se puede añadir información de forma dinámica implementando una interfaz, en este caso InfoContributor.
package com.danielme.demo.actuator.info; import org.springframework.boot.actuate.info.Info.Builder; import org.springframework.boot.actuate.info.InfoContributor; import org.springframework.stereotype.Component; @Component public class OSInfoContributor implements InfoContributor { @Override public void contribute(Builder builder) { builder.withDetail("os", System.getProperty("os.name")); } }
Configuración
Los servicios ofrecidos por Spring Boot Actuator son numerosos y su cantidad se va incrementando con cada nueva versión. Pueden consultarse en la documentación oficial. Se indica además la disponibilidad de cada servicio tanto para acceso mediante REST como por JMX.
Por defecto todos los servicios están habilitados excepto shutdown. Esto se configura con las propiedades del tipo management.endpoint.enabled.
management.endpoint.shutdown.enabled=true
Además de estar habilitado, el endpoint de un servicio debe estar «expuesto» para que pueda ser llamado. Nuevamente recurriremos a la documentación para comprobar los servicios que están expuestos por defecto. La exposición de los endpoints se configura con las propiedades management.endpoints.web.exposure.include/ management.endpoints.web.exposure.exclude para REST y management.endpoints.jmx.exposure.include/ management.endpoints.jmx.exposure.exclude para JMX. Se admite un listado de servicios separados por comas. Por ejemplo, la siguiente configuración habilita el acceso REST a todos los servicios excepto conditions y metrics.
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.exclude=conditions, metrics
Otros servicios
Para Spring Boot 2.1 tenemos los siguientes servicios accesibles vía REST, téngase en cuenta que no todos pueden serlo.
{ "_links": { "self": { "href": "http://localhost:8080/actuator", "templated": false }, "auditevents": { "href": "http://localhost:8080/actuator/auditevents", "templated": false }, "beans": { "href": "http://localhost:8080/actuator/beans", "templated": false }, "caches-cache": { "href": "http://localhost:8080/actuator/caches/{cache}", "templated": true }, "caches": { "href": "http://localhost:8080/actuator/caches", "templated": false }, "health": { "href": "http://localhost:8080/actuator/health", "templated": false }, "health-component": { "href": "http://localhost:8080/actuator/health/{component}", "templated": true }, "health-component-instance": { "href": "http://localhost:8080/actuator/health/{component}/{instance}", "templated": true }, "conditions": { "href": "http://localhost:8080/actuator/conditions", "templated": false }, "shutdown": { "href": "http://localhost:8080/actuator/shutdown", "templated": false }, "configprops": { "href": "http://localhost:8080/actuator/configprops", "templated": false }, "env-toMatch": { "href": "http://localhost:8080/actuator/env/{toMatch}", "templated": true }, "env": { "href": "http://localhost:8080/actuator/env", "templated": false }, "info": { "href": "http://localhost:8080/actuator/info", "templated": false }, "loggers": { "href": "http://localhost:8080/actuator/loggers", "templated": false }, "loggers-name": { "href": "http://localhost:8080/actuator/loggers/{name}", "templated": true }, "heapdump": { "href": "http://localhost:8080/actuator/heapdump", "templated": false }, "threaddump": { "href": "http://localhost:8080/actuator/threaddump", "templated": false }, "metrics": { "href": "http://localhost:8080/actuator/metrics", "templated": false }, "metrics-requiredMetricName": { "href": "http://localhost:8080/actuator/metrics/{requiredMetricName}", "templated": true }, "scheduledtasks": { "href": "http://localhost:8080/actuator/scheduledtasks", "templated": false }, "httptrace": { "href": "http://localhost:8080/actuator/httptrace", "templated": false }, "mappings": { "href": "http://localhost:8080/actuator/mappings", "templated": false } } }
Existen otros servicios que no veremos en la aplicación de ejemplo porque son específicos de módulos de Spring que no tenemos como por ejemplo el servicio session que sólo está disponible si utilizamos Spring Session.
En mi opinión, los más interesantes son
env
Muestras ciertas variables de entorno incluyendo las definidas en la máquina virtual (systemProperties) y en todos los .properties utilizados por la aplicación. Para las variables que contengan la palabra password no se muestra su contenido, por ejemplo
{ "name":"applicationConfig: [classpath:/application.properties]", "properties":{ "spring.datasource.url":{ "value":"jdbc:mysql://localhost:3306/db", "origin":"class path resource [application.properties]:1:23" }, "spring.datasource.username":{ "value":"user", "origin":"class path resource [application.properties]:2:28" }, "spring.datasource.password":{ "value":"******", "origin":"class path resource [application.properties]:3:28" }
mappings
Permite conocer todos los endpoints web con los que cuenta la aplicación incluyendo los expuestos por Actuator.
{ "handler":"Actuator web endpoint 'health'", "predicate":"{GET /actuator/health, produces [application/vnd.spring-boot.actuator.v2+json || application/json]}", "details":{ "handlerMethod":{ "className":"org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler", "name":"handle", "descriptor":"(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;" }, "requestMappingConditions":{ "consumes":[ ], "headers":[ ], "methods":[ "GET" ], "params":[ ], "patterns":[ "/actuator/health" ], "produces":[ { "mediaType":"application/vnd.spring-boot.actuator.v2+json", "negated":false }, { "mediaType":"application/json", "negated":false } ] } } },
metrics
Servicio de monitorización basado en Micrometer, una fachada que permite integrar de forma transparente servicios de monitorización tales como como Prometheus, CloudWatch, Datadog o Netflix Atlas. Spring Boot por defecto ya ofrece un conjunto de métricas consultables en el endpoint /actuator/metrics/, por ejemplo /actuator/metrics/jvm.memory.used
{ "name": "jvm.memory.used", "description": "The amount of used memory", "baseUnit": "bytes", "measurements": [ { "statistic": "VALUE", "value": 96914704 } ], "availableTags": [ { "tag": "area", "values": [ "heap", "nonheap" ] }, { "tag": "id", "values": [ "Compressed Class Space", "PS Survivor Space", "PS Old Gen", "Metaspace", "PS Eden Space", "Code Cache" ] } ] }
Las métricas disponibles se pueden consultar en /actuator/metrics/
{ "names": [ "jvm.memory.max", "jvm.threads.states", "process.files.max", "jvm.gc.memory.promoted", "system.load.average.1m", "jvm.memory.used", "jvm.gc.max.data.size", "jvm.gc.pause", "jvm.memory.committed", "system.cpu.count", "logback.events", "http.server.requests", "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", "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", "jvm.classes.unloaded", "tomcat.global.error", "tomcat.sessions.active.current", "tomcat.sessions.alive.max", "jvm.gc.live.data.size", "tomcat.threads.current", "process.files.open", "jvm.buffer.count", "jvm.buffer.total.capacity", "tomcat.sessions.active.max", "tomcat.threads.busy", "process.start.time" ] }
Estas métricas pueden habilitarse\deshabilitarse individualmente del siguiente modo
management.metrics.enable.jvm=false
siendo jvm el prefijo de todas las métricas que en este ejemplo estamos desactivando. Se puede utilizar root como prefijo para todas.
management.metrics.enable.root=false management.metrics.enable.tomcat=true
La anterior configuración muestra sólo el conjunto de métricas relacionadas con Tomcat.
Nuevos endpoints
Es posible añadir a Spring Boot Actuator nuestros propios endpoints, tanto para REST como JMX, tan sólo con definir un bean anotado con @Endpoint. Esta anotación tiene el atributo enableByDefault cuyo valor por defecto es true, y los endpoints definidos de esta forma se pueden configurar en el application.properties utilizando las mismas propiedades que para los endpoints incluidos de serie en Spring Boot. Los endpoints pueden tener operaciones READ, WRITE y DELETE implementadas en métodos con las anotaciones @ReadOperation, @PostOperation y @DeleteOperation respectivamente.
package com.danielme.demo.actuator.custom; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.stereotype.Component; @Component @Endpoint(id = "custom") public class CustomEndpoint { @ReadOperation public Map<String, Object> infoAll() { Map<String, Object> info = new LinkedHashMap<>(); info.put("param1", 2); info.put("param2", "value 2"); return info; } }
La llamada a /actuator/custom devolvería lo siguiente.
{ "param1": 2, "param2": "value 2" }
También es posible añadir un parámetro en la url.
@ReadOperation public Map<String, Object> info(@Selector String selector) { Map<String, Object> info = new LinkedHashMap<>(); info.put(selector, "value for " + selector); return info; }
La llamada a /actuator/custom/hello devuelve
{ "hello": "value for hello" }
Para que lo anterior funcione en Eclipse es necesario habilitar en Window->Preferences->Java->Compiler la siguiente opción
En los ejemplos se está devolviendo un Map pero cualquier objeto será automáticamente serializado en un JSON con las propiedades que expongan sus getters.
Otra opción para crear nuestros propios endpoints es implementarlos en controladores web de Spring anotados con @ControllerEndpoint o @RestControllerEndpoint en lugar de @Controller o @RestController. La ventaja es que estos endpoints los implementamos como cualquier servicio REST con Spring MVC o incluso Spring WebFlux pero no se pueden exponer mediante JMX. Sirva la siguiente clase a modo de ejemplo, devuelve un status HTTP 204 como respuesta a un POST enviado a /actuator/rest-controller
package com.danielme.demo.actuator.custom; import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseStatus; @Component @RestControllerEndpoint(id = "rest-controller") public class CustomRestControllerEndpoint { @PostMapping @ResponseStatus(HttpStatus.CREATED) public void post() { //nothing here } }
Securización
Los endpoints REST de Spring Boot Actuator pueden ser securizados a nivel de url utilizando Spring Security siguiendo lo explicado en mi tutorial Spring REST: Securización BASIC y JDBC con Spring Security así que no voy a explayarme en los detalles. Por ejemplo, vamos permitir que sólo los usuarios logados puedan acceder al endpoint scheduledtasks.
package com.danielme.demo.actuator; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity public class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .csrf().disable().authorizeRequests() .requestMatchers(EndpointRequest.to(ScheduledTasksEndpoint.class)).authenticated() .antMatchers("/**").permitAll().and() .httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); auth.inMemoryAuthentication().withUser("admin").password(encoder.encode("admin")) .roles("ADMIN").and().withUser("user").password(encoder.encode("user")) .roles("USER"); } }
En lugar de poner la url del endpoint como una cadena se ha utilizado la clase EndpointRequest para obtenerla. De este modo un cambio en la url no afectará a la securización.
Si no se proporcionan las credenciales de un usuario, por ejemplo user/user con BASIC, no se podrán consultar los datos del servicio
{ "timestamp": "2019-04-09T20:22:30.877+0000", "status": 401, "error": "Unauthorized", "message": "Unauthorized", "path": "/actuator/scheduledtasks" }
JMX
Los endpoint de Spring Boot Actuator son en su mayoría accesibles vía JMX, de hecho por defecto casi todos son expuestos mediante este mecanismo. Podemos acceder a ellos con la herramienta de monitorización JConsole incluida en la jdk. Con la aplicación de ejemplo en ejecución, la seleccionamos en el menu de inicio.
Dentro de la pestaña MBeans están disponibles los endpoints.
Código de ejemplo
El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.
Otros tutoriales relacionados con Spring
Testing en Spring Boot con JUnit 45. Mockito, MockMvc, REST Assured, bases de datos embebidas
Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA
Spring JDBC Template: simplificando el uso de SQL
Persistencia en BD con Spring Data JPA
Spring REST: Securización BASIC y JDBC con Spring Security
Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache