Spring Boot: custom “info” contributors for Actuator
In the previous article Spring Boot: configuring the Actuator “info” endpoint, I explained some fundamental concepts about configuring and using the “info” endpoint provided by Spring Boot Actuator. In this article, it’s time to see how to implement custom info contributors.
The Actuator “info” endpoint exposes information produced by one or more implementations of the InfoContributor
interface. In my previous article, I only talked briefly about this interface, and instead, I presented the five standard predefined implementations provided by Actuator. Now it’s time to see more details about this interface.
The InfoContributor interface
InfoContributor
is a straightforward interface, and, most importantly, it is also a functional interface. It means you can implement this interface using a traditional regular class or the new lambda expressions (Java 8+). I will show an example of both cases later.
InfoContributor
has the following definition:
package org.springframework.boot.actuate.info;
@FunctionalInterface
public interface InfoContributor {
void contribute(Info.Builder builder);
}
The single abstract
method contribute
receives an object of type Info.Builder
. This object works in the classic “builder” style, with methods returning the same Builder
object so that you can eventually chain more invocations.
There are only two practical methods in Info.Builder
:
public Builder withDetail(String key, Object value)
public Builder withDetails(Map<String, Object> details)
The withDetail
method adds a single object under a specific key. This object can be of a simple type (e.g., String
, Integer
, etc.) or a complex type with several properties. In the latter case, you will have a JSON object under the key in the JSON response. The second withDetails
method is similar, but you can pass more associations using a single Map
object.
The choice between the two methods mainly depends on what kind of data you start from. For example, if you already have a single Map
object with appropriate key names, then withDetail
s
is the right choice. On the other hand, if you have a single object to expose or a few objects in different variables, then withDetail
is more suitable.
Keep in mind that this:
builder.withDetail("key1", object1).withDetail("key2", object2);
produces the same result (ignoring key ordering) as this:
Map<String, Object> map = new HashMap<>();
map.put("key1", object1);
map.put("key2", object2);
builder.withDetails(map);
Most of the time, you will use the simple withDetail
method, as you will see in the upcoming examples.
Since the InfoContributor
interface is so simple, I think there is not much more to say about it. The following section will show a concrete example of implementing custom “info” contributors.
Concrete example: a time-zone info contributor
This section provides a concrete and realistic example of a custom contribution to the “info” endpoint. I chose a reasonably straightforward scenario related to the time-zone information. The main objective is to develop a custom info contributor that exposes the following details about the system time-zone (the default time-zone of the Java Virtual Machine):
- The time-zone ID (e.g., “Europe/Berlin”)
- The time-zone display name in English (e.g., “Central European Time”)
- The current offset from UTC (e.g., “+02:00”)
- If daylight savings is currently active or not
Clearly, you could add many other time-zone details. But for this example case, the above information is more than enough.
The Java Standard Edition framework provides two different classes to deal with time-zones:
- The old “legacy”
java.util.TimeZone
class - The new
java.time.ZoneId
class provided by the Date/Time API introduced in JDK 8
Both classes can provide, more or less, the same level of information. However, ZoneId
is more modern and offers better information about offsets thanks to the ZoneOffset
class. Thus, I decided to use ZoneId
for the example case.
When you have to expose several correlated information from an InfoContributor
, I advise you to create a simple “bean” class containing all the properties to expose. Spring Boot Actuator indeed uses this same technique. For example, the predefined OsInfoContributor
described in the previous article creates and exposes an instance of OsInfo
, a simple “bean” class with three properties: name, version, and arch.
The TimeZoneInfo example class
Given the advice provided in the previous paragraph, the following TimeZoneInfo
class is a simple “bean” class that contains the four time-zone details.
package your.pkgname;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.TextStyle;
import java.time.zone.ZoneRules;
import java.util.Locale;
public class TimeZoneInfo {
private final String id;
private final String displayName;
private final String currentOffset;
private final boolean daylightSavings;
public TimeZoneInfo(Clock clock) {
ZoneId zone = clock.getZone();
ZoneRules rules = zone.getRules();
Instant instant = clock.instant();
id = zone.getId();
displayName = zone.getDisplayName(TextStyle.FULL, Locale.ENGLISH);
currentOffset = DateTimeFormatter.ofPattern("xxxxx").format(rules.getOffset(instant));
daylightSavings = rules.isDaylightSavings(instant);
}
public static TimeZoneInfo system() {
return new TimeZoneInfo(Clock.systemDefaultZone());
}
// ... getter methods omitted for brevity ...
}
I omitted the getter methods for brevity, but you must write them to make the class usable. Any modern IDE is capable of generating those getters for you.
There are two important things to notice in the code above. The first is that I used Clock
as starting point. The Clock
class is an abstraction useful for making the code unit-testable since you can create a Clock
object with a fixed instant. It means that the TimeZoneInfo
class is also unit-testable. You are not strictly required to reach this level of abstraction. Either way, it’s a good approach.
The second thing to notice is the formatting of the current offset. The getOffset
method of ZoneRules
returns a ZoneOffset
object. Its toString
method can return a string like "+05:00"
but can also return "Z"
for UTC. My main goal was to produce a string that is always formatted with hours/minutes and optional seconds, and for this reason, I used DateTimeFormatter
. The pattern "xxxxx"
may not be widely known, but it is documented in the DateTimeFormatter
Javadoc documentation:
Offset X and x: This formats the offset based on the number of pattern letters. […] Five letters outputs the hour and minute and optional second, with a colon, such as ‘+01:30:15’ […]
The InfoContributor example implementation
Now it’s time to see how to implement the InfoContributor
interface concretely. As said before, since the InfoContributor
is a functional interface, it can be implemented using a regular class or the new lambda expressions. So I am going to show both approaches.
Implementation with a regular class
The following is a simple implementation using a regular class.
package your.pkgname;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
@Component
public class TimeZoneInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("time-zone", TimeZoneInfo.system());
}
}
Notice that the class is annotated with @Component
because the Spring Boot’s component scanning must pick it up to work. Furthermore, you can notice that the contribute
method implementation is concise and effortless, thanks to the TimeZoneInfo
class, which encapsulates the time-zone details extraction.
Implementation with a lambda expression
If you want to use a lambda expression, things are slightly different because you must write a @Bean
method in a @Configuration
class.
package your.pkgname;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ActuatorInfoConfiguration {
@Bean
public InfoContributor timeZoneInfoContributor() {
return builder -> builder.withDetail("time-zone", TimeZoneInfo.system());
}
}
Even in this case, the code is self-explanatory and concise.
Checking the resulting JSON
Suppose everything is correct, and Spring Boot picks up the TimeZoneInfoContributor
class or the ActuatorInfoConfiguration
class (clearly, only one of them, not both!). In that case, you can check the response of a GET to the URL: http://localhost:8080/actuator/info. You should obtain a JSON document with a structure similar to the following:
{
"time-zone": {
"id": "Europe/Berlin",
"displayName": "Central European Time",
"currentOffset": "+02:00",
"daylightSavings": true
}
}
In my case, since I live in Italy, my machine uses the Europe/Berlin
time-zone. We currently have daylight savings active, so we are at +2 hours from UTC (normally we are at +1 hour from UTC when daylight savings is inactive).
Conclusions
In this article, you have seen that implementing custom “info” contributors for Actuator is a straightforward task, especially if you take care to encapsulate the information details in a separate and appropriate class.
The choice between a regular class and a lambda expression mainly depends on the length of the implementation code. A lambda expression is appropriate if the code is reasonably concise and has one or few lines. However, a regular class is more appropriate if the code is longer.
Spring Boot Actuator also provides two “support” classes named SimpleInfoContributor
and MapInfoContributor
. These classes are simple encapsulations of a single object or a single Map
. As a final “bonus”, the following is the ActuatorInfoConfiguration
class updated to use SimpleInfoContributor
.
package your.pkgname;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.boot.actuate.info.SimpleInfoContributor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ActuatorInfoConfiguration {
@Bean
public InfoContributor timeZoneInfoContributor() {
return new SimpleInfoContributor("time-zone", TimeZoneInfo.system());
}
}
Technically, you can also define a class that extends SimpleInfoContributor
, with a no-arg constructor that calls the “super” constructor with prefix
and detail
.
package your.pkgname;
import org.springframework.boot.actuate.info.SimpleInfoContributor;
import org.springframework.stereotype.Component;
@Component
public class TimeZoneInfoContributor extends SimpleInfoContributor {
public TimeZoneInfoContributor() {
super("time-zone", TimeZoneInfo.system());
}
}
The latter code is even slightly shorter than the @Configuration
class version.