What is Spring Boot Actuator ?

Spring Boot Actuator is a spring feature which allows any web-app developer to add features to their web-services and applications to make them production-ready, such as monitoring and administration. The actuator is available as a library which attaches on-the-fly and provides tools to manage a web app by monitoring its performance and state e.g., database metrics, traffic metrics etc.,.

We can use HTTP endpoints or JMX beans to interact with Actuators. The main purpose of an Actuator endpoint is to expose a bit of information like health, info, metrics, env, config etc. about the running application.

Several endpoints are made available and can be configured and extended once the actuator module is on the classpath.

Spring Boot Actuator Example

Adding actuator to an existing rest-service

In this post, we are going to add spring boot actuator to an existing webservice which we created in this article.

That article outlined how to create a web-service that provides a REST API to manage a collection of films. The client can retrieve the list of films in the collection along with their year of release and other details. We can add films to it, modify individual films’ attributes as well as delete films from it. You can get the source-code here or use your own project for this post. Specifically, the rest of the post assumes a Jersey / Spring MVC web application or webservice that is built using Spring 2.x and setup as a Maven project.

In order to add the actuator endpoints, we only need to edit the pom.xml file. This involves one simple step of adding the spring-boot-actuator dependency to our pom.xml. Open pom.xml and add the below dependency (irrespective of the Boot version):

pom.xml entry for spring-boot-actuator dependency <dependency> <groupId>org.springframework.boot</groupId> <articraftId>spring-boot-starter-actuator</articraftId> </dependency> 1 2 3 4 <dependency> <groupId> org.springframework.boot </groupId> <articraftId> spring-boot-starter-actuator </articraftId> </dependency>

When you save pom.xml file, you will notice Maven downloading the necessary dependencies in the background (check progress tab in STS). Now that our webservice is ready after adding the Spring-boot-Actuator dependency, we can run our application by right-clicking on our project and selecting “Run As…” -> “Spring Boot App”. The embedded Tomcat server will be launched and the webservice will be deployed.

Now we can access the web-service by pointing our browser at http://localhost:8080/films or by using Postman.

Try accessing http://localhost:8080/actuator/health.

This is one of the several endpoints automatically provided by Spring boot Actuator. All the HTTP end-points added by spring boot actuator are available under the base URI /actuator by default. There are more endpoints with varied functionality and each can be customised and extended. We will walk-through these details in the rest of the post.

History of Spring boot Actuator

Since our project uses JDK 8 and Spring Boot 2, we are using the 2.X version of the Spring boot Actuator which is the 2nd generation.

The 1.X version of Spring boot actuator, available since the first Spring boot release (Apr 2014), follows a Read/Write model, where we can get the health of our app or change our logging configuration, gracefully turn off our app etc. by either reading from it or writing to it. It requires Spring MVC to expose the end points through HTTP regardless of how the rest of the app is designed. The 1.X actuator has its own model to provide security by taking advantage of spring security constructs which should be configured independently.

The 2.X version released with Spring Boot 2 (early 2018) differs from the 1.X actuator by not being tied to Spring MVC. The 2.X actuator also has better defaults and simpler security configuration. It requires Java 8 or later.

In 2.X Actuator, most endpoints are not public and are disabled over HTTP. Only two endpoints, namely /info and /health, are enabled by default and publicly available. Other endpoints are senstive and require a username/password to access over HTTP if web security is enabled, or not exposed over HTTP as sensitive endpoints are not exposed publicly.

Spring Boot Actuator endpoints

A few commonly used endpoints provided by Boot 2.X Actuator are enlisted below:

Endpoint name Purpose /auditevents Exposes audit events like user login/logout /beans Returns all beans configured in the Bean factory /conditions Creates a report of conditions built around auto-configuration /configprops Displays a list of all configuration properties /env Returns the present environment properties /health Returns health of the current application /threaddump Performs a thread dump of the JVM /info Displays info about the application /logfile Returns logs of the application /loggers Shows the logger configuration logged in the application /metrics Displays metrics information(generic and custom) of the application /shutdown Allows gracefull shutdown of the application /sessions Displays last few HTTP sessions of the Spring session /flyway Shows details about Flyway DB migration

For the complete list of Actuator endpoints, have a look here.

Configuring the endpoint properties

Spring Boot Actuator is richly customizable to fit every application / user need. The URL and endpoint properties are configured or customised by modifying application.properties in (src/main/resources). Below is the modified application.properties for our project.

application.properties management.endpoints.web.exposure.include=* management.endpoints.enabled-by-default=true management.endpoint.env.enabled=false management.endpoints.web.base-path=/admin management.endpoints.web.path-mapping.health=status management.endpoint.health.enabled=true management.endpoint.health.show-details=ALWAYS 1 2 3 4 5 6 7 8 9 management . endpoints . web . exposure . include =* management . endpoints . enabled - by - default = true management . endpoint . env . enabled = false management . endpoints . web . base - path =/ admin management . endpoints . web . path - mapping . health = status management . endpoint . health . enabled = true management . endpoint . health . show - details = ALWAYS

The first line selects all end-points for making available through HTTP. Although in this exercise we are only interested in HTTP end-points, actuator also provides access over JMX. By default, most endpoints are enabled (meaning active) but only the health and info end-points are exposed over HTTP – this line makes all enabled end-points accessible over HTTP.

The 2nd line sets all end-points to be enabled by default and the 3rd line disables the env endpoint – this is an example to illustrate how individual end-points can be disabled. Also an application can adopt a more conservative strategy of setting enabled-by-default to false and use one-liners like line 8 for every end-point that it would like to deliberately enable. The correct choice for the approach would depend on the application context.

The next 2 lines change the base-path – /actuator is the default and if there is context mapping, e.g., in a Spring-MVC app then base-path is changed to the context path, but it can also be set in application.properties as shown here. The last line sets the verbosity of the /health end-point. This is a parameter that is specific to health . End-points can provide such attributes for customising their behaviour.

In case spring-security is enabled in the app, then all the endpoints are sensitive by default (except health). We would need to add custom security configuration that allows unauthenticated access to the endpoints as shown below or provide an alternate security configuration use RequestMatcher objects that select specific users or roles. For more on this see this section of actuator docs.

Security Config to permit all actuator endpoints without need for authentication @Configuration public class ActuatorSecurity extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatcher(EndpointRequest.toAnyEndpoint()).authorizeRequests() .anyRequest().permitAll(); } } 1 2 3 4 5 6 7 8 9 @Configuration public class ActuatorSecurity extends WebSecurityConfigurerAdapter { @Override protected void configure ( HttpSecurity http ) throws Exception { http . requestMatcher ( EndpointRequest . toAnyEndpoint ( ) ) . authorizeRequests ( ) . anyRequest ( ) . permitAll ( ) ; } }

Additionally management.server.port can be set in application.properties to set the HTTP port for actuator end-points. These settings are not needed in our app as of now but head here to learn how to secure a webservice.

Customizing endpoints

In general each endpoint can be customized using three properties, namely:

id – name of this endpoint when accessed over HTTP (URI) enabled -if true, endpoint can be accessed, else cannot be accessed sensitive – if true, needs authorisation for access

Now, each endpoint can be configured using the below format:

endpoints.[name of the endpoint].[property that needs to be customised]=[value]

In addition to above three properties each end-point may expose custom properties which are documented here.

Extending endpoint functionality

It’s easy to extend existing end-points to add information specific to our application. For example, Spring Boot Actuator provides auto-configuration for Micrometer, so we can easily add counters and gauges. In our example, we will add 4 new end-points within the /metrics to display the current size of the films database (id=filmsdb.size) and count the number of POST (id=filmsdb.adds), PUT (id=filmsdb.mods) and DELETE (id=filmsdb.deletes) API requests made. Of course in a real-world application the metrics names may be prefixed by an FQDN or other namespace qualifier to prevent collision.

In order to achieve this extension, we modify the FilmService class by injecting a MeterRegistry object and registering a gauge for the size of the films database and 3 counters – one each for adds, mods and deletes. The source code of the modified file is below.

FilmService.java package com.javagists.jerseyfilms.service; import java.util.Collection; import java.util.Collections; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.springframework.stereotype.Service; import com.javagists.jerseyfilms.model.Film; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; @Service public class FilmService { private final ConcurrentMap<String, Film> db; private Counter adds, mods, deletes; public FilmService(MeterRegistry registry) { this.db = new ConcurrentHashMap<>(); registry.gaugeMapSize("filmsdb.size", Tags.empty(), this.db); this.adds = registry.counter("filmsdb.adds"); this.mods = registry.counter("filmsdb.mods"); this.deletes = registry.counter("filmsdb.deletes"); } // Get all the films stored in the database public Collection<Film> getAllFilms() { Collection<Film> all = this.db.values(); if (all.isEmpty()) { return Collections.emptyList(); } else { return all; } } // Add a film to the database public void addFilm(Film f) { this.adds.increment(); if(f.getId() == null) { f.setId(String.valueOf(this.db.size()+1)); } this.db.put(f.getId(), f); } // Get a film by id public Film getFilm(String id) { return this.db.get(id); } // Modify a film's attributes public Film updateFilm(String id, Film f) { this.mods.increment(); if(!this.db.containsKey(id)) { throw new IllegalArgumentException("Invalid Film or Film does not exist!"); } if((f.getId() == null) || (id != f.getId())) { f.setId(id); } return this.db.put(id, f); } // Delete a film from database public void removeFilm(String id) { this.deletes.increment(); if(!this.db.containsKey(id)) { throw new IllegalArgumentException("Invalid Film or Film does not exist!"); } this.db.remove(id); } } 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 package com . javagists . jerseyfilms . service ; import java . util . Collection ; import java . util . Collections ; import java . util . concurrent . ConcurrentHashMap ; import java . util . concurrent . ConcurrentMap ; import org . springframework . stereotype . Service ; import com . javagists . jerseyfilms . model . Film ; import io . micrometer . core . instrument . Counter ; import io . micrometer . core . instrument . MeterRegistry ; import io . micrometer . core . instrument . Tags ; @Service public class FilmService { private final ConcurrentMap < String , Film > db ; private Counter adds , mods , deletes ; public FilmService ( MeterRegistry registry ) { this . db = new ConcurrentHashMap < > ( ) ; registry . gaugeMapSize ( "filmsdb.size" , Tags . empty ( ) , this . db ) ; this . adds = registry . counter ( "filmsdb.adds" ) ; this . mods = registry . counter ( "filmsdb.mods" ) ; this . deletes = registry . counter ( "filmsdb.deletes" ) ; } // Get all the films stored in the database public Collection <Film> getAllFilms ( ) { Collection <Film> all = this . db . values ( ) ; if ( all . isEmpty ( ) ) { return Collections . emptyList ( ) ; } else { return all ; } } // Add a film to the database public void addFilm ( Film f ) { this . adds . increment ( ) ; if ( f . getId ( ) == null ) { f . setId ( String . valueOf ( this . db . size ( ) + 1 ) ) ; } this . db . put ( f . getId ( ) , f ) ; } // Get a film by id public Film getFilm ( String id ) { return this . db . get ( id ) ; } // Modify a film's attributes public Film updateFilm ( String id , Film f ) { this . mods . increment ( ) ; if ( ! this . db . containsKey ( id ) ) { throw new IllegalArgumentException ( "Invalid Film or Film does not exist!" ) ; } if ( ( f . getId ( ) == null ) | | ( id ! = f . getId ( ) ) ) { f . setId ( id ) ; } return this . db . put ( id , f ) ; } // Delete a film from database public void removeFilm ( String id ) { this . deletes . increment ( ) ; if ( ! this . db . containsKey ( id ) ) { throw new IllegalArgumentException ( "Invalid Film or Film does not exist!" ) ; } this . db . remove ( id ) ; } }

Notice how spring boot takes care of dependency injection for us so we don’t have to worry about the life-cycle of the MeterRegistry object. Run the application we can now see the new data being reported under metrics (remember the actuator endpoints are accessible at localhost:8080/admin ). Below is a screenshot of the API being accessed using curl – curl or wget provide simple yet flexible & powerful command-line based access to REST APIs that can come handy in automating tests or quickly accessing an API even on remote systems that don’t have browsers etc., installed. Using grep we highlight the newly added metrics endpoints in the curl output.

Norice how the counter values for adds, mods and deletes correlate with the current size of the database. We had added 4 movies and deleted 1 so the database size is now 3.

Next we extend the health indicator with a custom indicator class that reports the Genres which are active in the database. For this, we create a new class called ActiveGenreCollector in the com.javagists.jerseyfilms.controller package. Source code is below. This class implements the HealthIndicator interface and is decorated with the @Component tag. Thus, spring-boot automatically adds this to the health indicator end-point (configured at localhost:8080/admin/status ).

ActiveGenreCollector.java package com.javagists.jerseyfilms.controller; import java.util.HashSet; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; import com.javagists.jerseyfilms.model.Film; import com.javagists.jerseyfilms.service.FilmService; @Component public class ActiveGenreCollector implements HealthIndicator { @Autowired FilmService fs; @Override public Health health() { HashSet<String> activeGenres = new HashSet<String>(); String status = "None"; for (Film f : fs.getAllFilms()) { activeGenres.add(f.getGenre().toString()); } if(!activeGenres.isEmpty()) { status = activeGenres.toString(); } return new Health.Builder().up().withDetail("Active Genres", status).build(); } } 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 package com . javagists . jerseyfilms . controller ; import java . util . HashSet ; import org . springframework . beans . factory . annotation . Autowired ; import org . springframework . boot . actuate . health . Health ; import org . springframework . boot . actuate . health . HealthIndicator ; import org . springframework . stereotype . Component ; import com . javagists . jerseyfilms . model . Film ; import com . javagists . jerseyfilms . service . FilmService ; @Component public class ActiveGenreCollector implements HealthIndicator { @Autowired FilmService fs ; @Override public Health health ( ) { HashSet <String> activeGenres = new HashSet <String> ( ) ; String status = "None" ; for ( Film f : fs . getAllFilms ( ) ) { activeGenres . add ( f . getGenre ( ) . toString ( ) ) ; } if ( ! activeGenres . isEmpty ( ) ) { status = activeGenres . toString ( ) ; } return new Health . Builder ( ) . up ( ) . withDetail ( "Active Genres" , status ) . build ( ) ; } }

Now we run the app and notice that the output from our newly added end-point is part of the response.

Also see how diskspaceusage is reported automatically. This is one of the auto-configured health indicators that gets picked up. There are several such indicators that are auto-configured based on application configuration (e.g., if you have MongoDB dependency in your app, a MongoHealthIndicator is added to the end-point automatically). More details here.

Adding a new actuator end-point

Finally we add a brand-new actuator end-point that outputs the genre-wise distribution of movies in our database. For this we add a new class with @WebEndpoint decorator and implement the necessary methods in it. The source for this GenreDistribution.java is below.

GenreDistribution.java package com.javagists.jerseyfilms.controller; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; import org.springframework.stereotype.Component; import com.javagists.jerseyfilms.model.Film; import com.javagists.jerseyfilms.service.FilmService; @Component @WebEndpoint(id = "genre") public class GenreDistribution { @Autowired FilmService fs; @ReadOperation public ConcurrentMap<String, Integer> getGenreDistribution() { ConcurrentMap<String, Integer> gd = new ConcurrentHashMap<>(); for (Film f : this.fs.getAllFilms()) { String key = f.getGenre().toString(); gd.put(key, gd.getOrDefault(key, 0)+1); } return gd; } } 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 package com . javagists . jerseyfilms . controller ; import java . util . concurrent . ConcurrentHashMap ; import java . util . concurrent . ConcurrentMap ; import org . springframework . beans . factory . annotation . Autowired ; import org . springframework . boot . actuate . endpoint . annotation . ReadOperation ; import org . springframework . boot . actuate . endpoint . web . annotation . WebEndpoint ; import org . springframework . stereotype . Component ; import com . javagists . jerseyfilms . model . Film ; import com . javagists . jerseyfilms . service . FilmService ; @Component @WebEndpoint ( id = "genre" ) public class GenreDistribution { @Autowired FilmService fs ; @ReadOperation public ConcurrentMap < String , Integer > getGenreDistribution ( ) { ConcurrentMap < String , Integer > gd = new ConcurrentHashMap < > ( ) ; for ( Film f : this . fs . getAllFilms ( ) ) { String key = f . getGenre ( ) . toString ( ) ; gd . put ( key , gd . getOrDefault ( key , 0 ) + 1 ) ; } return gd ; } }

Now running our application exposes a new end-point at http://localhost:8080/admin/genre (the suffix as specified in id parameter of the @WebEndpoint decorator. The HTTP GET method on this endpoint is mapped to the Java method having the @ReadOperation decorator, i.e., the getGenreDistribution() method which simply iterates through the films list, counts the occurrence of each genre and returns a HashMap with genre-wise count. Spring boot takes care of auto-converting this into JSON, which is then returned as response to the request. Below is a session showing its output.

Summary

In this post we looked at adding Spring boot actuator to a spring boot project. We looked at the actuator end-points functionality and how they can be customized and extended including adding new end-points. These administrative end-points are immensely valuable in monitoring and maintaining production applications and spring-boot-actuator provides them essentially for free with rich customization possibilities. We hope you are able to leverage these advanced features to make your web applications and services robust and maintainable.

The source code for this project can be downloaded here. Also a Postman collection with a collection of requests to exercise the JerseyFilms API and the actuator endpoints is available here. Go ahead and give it a spin!

Share this: Facebook

LinkedIn

Twitter

Tumblr

Pinterest



Like this: Like Loading...