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 withDetails 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:

  1. The old “legacy” java.util.TimeZone class
  2. 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.

Similar Posts