I am going to quickly show some cool things, which could be done almost out of the box by means of Spring Actuator module. I selected just 4 cool features, but there are of course more.

Before getting started with cool things, I must warn you that big power brings also high responsibility. Don’t forget to check the security part of actuator documentation before delivering a solution to production.

Setup project

To show something we need a Spring project, I would need Spring MVC, Actuator, and Lombok to make life easier.

I showed already the Spring CLI in my previous article, I am going to use it this time also:

1
2
3
4
5
6
    spring init \
        -d=web,actuator,lombok \
        -a=actuator-test \
        -g=com.relaximus \
        --build=gradle \
        actuator-test

Quite self-explainable, isn’t it? It’s a java project on Gradle.

As usual the projects with examples is hosted in GitHub repository.

Loggers

The first example is Loggers. Every modern application is using some kind of logging system. Usually, it’s configured in some files stored on the classpath. But look at what you can achieve with the Actuator loggers section.

Let’s define the level of logging for our application in the application.properties file:

1
logging.level.com.relaximus.actuatortest=INFO

At the moment you won’t see the loggers section in the actuator endpoint, as it’s not exposed via HTTP by default. Let’s open it:

1
management.endpoints.web.exposure.include=info,health,loggers

To the default info and health sections, I added loggers. Now the root actuator endpoint /actuator shows the loggers section also.

1
GET http://127.0.0.1:8080/actuator/loggers/com.relaximus.actuatortest

response:

1
2
3
4
{
  "configuredLevel": "INFO",
  "effectiveLevel": "INFO"
}

Nice. But this is only part of the story. Let’s add some controller to the project, which only makes some logging on request.

DemoController:

1
2
3
4
5
6
7
8
9
@Slf4j
@RestController
public class DemoController {
    @GetMapping("/push-log")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void pushLog() {
        log.debug("This is a debug log from DemoController");
    }
}

As you noticed there is a DEBUG level for the logging, so calling the /push-log does nothing. But what we can do is actually to change the level of the particular logger without stopping our application, on the fly. We need to send a POST request to the same logger URL:

1
2
3
4
5
6
POST http://127.0.0.1:8080/actuator/loggers/com.relaximus.actuatortest
Content-Type: application/vnd.spring-boot.actuator.v3+json

{
  "configuredLevel": "DEBUG"
}

Now let’s try the call the logging controller:

1
GET http://127.0.0.1:8080/push-log

The next line appears in the console:

1
2020-10-30 23:01:10.473 DEBUG 32644 --- [nio-8080-exec-8] c.relaximus.actuatortest.DemoController  : This is a debug log from DemoController

Isn’t it what you were dreaming all the time, trying to find the cause of the bug, which appears only on production environments, but all the logging there is disabled.

Info

That was fun. But the fun is not finished. The next section is info. This is a user arbitrary section, which can be used for posting some custom information regarding the app. Well, you might think, what could be interesting the fully custom section, where I can put everything? The cool thing what tools coming out of the box with Actuator. In general, the whole framework trying to be as less config requiring as possible. And the next things prove this.

First, you can add the build info into the info-section. Add this in build.gradle:

1
2
3
springBoot {
	buildInfo()
}

If you want to pass some custom data from the Gradle project. The next code should be added to the same build.gradle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
plugins {
...
    id "com.gorylenko.gradle-git-properties" version "2.2.2"
...
}

processResources {
	println "project.properties = ${project.properties}"
	expand(project.properties)
}

The last section searches in the properties file of the application the placeholders and put there the appropriate data from the project. Having that, we can add the next lines to our application.properties:

1
2
info.app.name=${name}
info.app.description=${description}

And last but not least, let’s define the Spring component implementing InfoContributor interface:

1
2
3
4
5
6
7
8
@Component
public class DemoInfoContributor implements InfoContributor {
    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("demo",
            Collections.singletonMap("info-timestamp", LocalDateTime.now().toString()));
    }
}

This is the place where we can add any custom trash we want. Well, let’s check what we have in the info section of the actuator endpoint:

1
GET http://127.0.0.1:8080/actuator/info

result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "app": {
    "name": "actuator-test",
    "description": "null"
  },
  "git": {
    "branch": "master",
    "commit": {
      "id": "ff889ec",
      "time": "2020-10-29T22:36:04Z"
    }
  },
  "build": {
    "artifact": "actuator-test",
    "name": "actuator-test",
    "time": "2020-10-30T21:53:59.493Z",
    "version": "0.0.1-SNAPSHOT",
    "group": "com.relaximus"
  },
  "demo": {
    "info-timestamp": "2020-10-30T23:23:07.241981"
  }
}

Notice how effortless was that. But don’t push too much data into this section, it can become a heap of poorly structured data very quickly.

Health and custom levels

Saying about Actuator it’s hard to avoid a word about the health endpoint, which allows checking the status of the application in general. There are two things which I wanted to mention here. First, Spring Actuator aggregates statuses from different subsystems of the application in one final status. If you added the database support to the project and DataSource bean was created, spring boot will add the health indicator for it. There are many such auto-indicators.

Second, if you didn’t find already existing health indicator, you can create your own pretty simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Component
public class DemoHealthIndicator implements HealthIndicator {
    private static int count = 0;
    private static Status WARMING = new Status("WARMING");

    @Override
    public Health health() {
        return ++count > 2
            ? Health.up().build()
            : Health.status(WARMING).build();
    }
}

Here I am returning status WARMING on the first check of health, and then it’s again UP. That’s so easy now my indicator is participating in the aggregation of all system indicators in one general status. The only thing, which is left is to add my new WARMING status to the order in which, statuses being aggregated by default.

1
management.endpoint.health.status.order=down,out-of-service,warming,unknown,up

This means that if there are UP and WARMING the final status will be “WARMING”. And in the case of DOWN and WARMING, the last one is the total status of the application.

1
GET http://127.0.0.1:8080/actuator/health

result:

1
2
3
{
  "status": "WARMING"
}

and on the second request, status is:

1
2
3
{
  "status": "UP"
}

Metrics

The last feature is the metrics. It allows you in just a few minutes to configure almost all crucial metrics collection for the app. Out of the box the long list of metrics frameworks and services supported. I’ll show the simplest example of in-memory metrics gathering coming by default with the core Micrometer package.

Ok, so what we need to do is first add the needed dependency in our build and expose the metrics endpoint. In build.gradle:

1
2
3
4
5
dependencies {
    ...
	implementation 'io.micrometer:micrometer-core:latest.release'
	...
}

and in application.properties:

1
management.endpoints.web.exposure.include=info,health,loggers,metrics

Now we are able to see the /actuator/metrics endpoint which returns the list of all available metrics to query. By default, out of the box come to a lot of JVM specific metrics, for example:

Let’s try something a little bit more sophisticated. You can add your own tags to the metrics and query metrics by tag or other parameters for the metrics as filters. Let’s do both: DemoController:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import io.micrometer.core.annotation.Timed;

@Slf4j
@RestController
public class DemoController {
    @GetMapping("/push-log")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    @Timed(extraTags = { "demo-tag", "test-val" }) // this one is added
    public void pushLog() {
        log.debug("This is a debug log from DemoController");
    }
}

Spring Actuator provides nice annotation @Timed to add custom info to the metrics. Here I have added the tag demo-tag to the push-log controller created earlier. Let’s see what we have in the all request metric:

Let’s query metric for the specific request, for the /push-log:

This is just an example of how metrics data being collected, for the production-ready solution you’d probably use some more advanced service, like Prometheus or datadog, but, in general, notice how less effort was applied here to start logging request metrics, for example.

Conclusion

Spring Actuator is really cool framework, but use it carefully.