Managing and Monitoring Your Java Applications

Managing and monitoring you Java applications
Photo by Chris Liverani on Unsplash

The Problem

So your team has developed and implemented the five principles of object-oriented design. You’re using Spring Framework as your dependency injection mechanism in your Java applications. You’ve tested the app, it’s been in production for some time now. But how is it doing? Is it healthy? Is it up and running? Oh sure, you can wait for a service desk call to come in, but wouldn’t it be much better if that call came in and you could say “yes, we’re aware of a problem and we’re working on it”? Some form of managing and monitoring your Java applications is moving from “nice to have” toward “essential”.

And when that ticket come in, your first thought. is to see if the app is actually running. You could check log files. You could log in to the app itself, oh wait, your credentials don’t allow you to log in to the production app. Is there a set of support credentials you can use? Where are they?

There has to be a better way.

There is. It’s called Spring Boot Actuator.

Spring Boot Actuator

Spring Boot came out in April 2014. VMware has evolved it regularly since then as an opinionated configuration of your Spring Framework-based application. One component of Spring Boot is Spring Boot Actuator, a web-based management and monitoring framework that is trivial to implement. Basically it means you now have web endpoints such as /health to report if everything is OK, /info to see what version of the software you are using, /metrics to report usage statistics, the list goes on.

Here is how simple it is to add management and monitoring (also known as “observability”) to your Spring application. Follow along using the source code.

First, let’s get some assumptions out of the way. I’m assuming you’re using Maven as your build tool. I assume you know your way around Git. This is not the first Maven-based project you’ve built, so you are intimately familiar with its standard file and directory structure. You know how to poke at REST-ful web services using cURL, HTTPie, Postman, whatever your favourite tool is.

This post is based on using the most recently available GA versions of Maven (3.6.3), Java 11, Spring Boot (2.3.8.RELEASE), Spring Framework (5.2.12.RELEASE), etc. Older versions of Spring Boot (1.5.x) and Spring Framework (4.3.x) are similar, but there are enough differences to make it worth your while to have the documentation handy.

Unlike many other articles on this subject, I’m also assuming you have your own company parent POM that you need to inherit from. I’ll show how you can still use Spring Boot Actuator to manage and monitor your applications.

First, your POM probably looks something like this. Your company POM as a parent, along with the usual Spring Framework dependencies:

<?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>ca.airspeed.company</groupId>
        <artifactId>company-pom</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <groupId>ca.airspeed.demo</groupId>
    <artifactId>management-monitoring</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>Managing and Monitoring Spring Apps</name>
    <url>http://maven.apache.org</url>

    <properties>
        <java.version>11</java.version>
        <jaxb.version>2.3.3</jaxb.version>
        <spring.version>5.2.12.RELEASE</spring.version>
    </properties>

    <dependencies>
        <!-- Spring core & mvc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>${spring.version}</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <type>jar</type>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>

        <!-- Servlet Spec -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.3.3</version>
            <scope>provided</scope>
        </dependency>

        <!--  JAXB goodies: -->
        <dependency>
          <groupId>jakarta.xml.bind</groupId>
          <artifactId>jakarta.xml.bind-api</artifactId>
          <version>${jaxb.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jaxb</groupId>
          <artifactId>jaxb-runtime</artifactId>
          <version>${jaxb.version}</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.200</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>management-monitoring</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Plus you probably have the customary web.xml file.

Checkout commit-id 0eed12a from the Github repo (everything is on master), build it, add it to your Tomcat servlet container and run it. Browse to http://localhost:8080/ and you’ll get a generic welcome page.

Add Management and Monitoring

Here comes the fun part. Change your POM to use some Spring Boot Starter dependencies and a couple useful plugins like this:

<?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>

    <groupId>ca.airspeed.demo</groupId>
    <artifactId>management-monitoring</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>Managing and Monitoring Spring Apps</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
        <jaxb.version>2.3.3</jaxb.version>
        <git-commit-id-plugin.version>4.0.0</git-commit-id-plugin.version>
        <spring-boot.version>2.3.8.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring-boot.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <version>${spring-boot.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>${spring-boot.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- Servlet Spec -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.3.3</version>
            <scope>provided</scope>
        </dependency>

        <!--  JAXB goodies: -->
        <dependency>
          <groupId>jakarta.xml.bind</groupId>
          <artifactId>jakarta.xml.bind-api</artifactId>
          <version>${jaxb.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jaxb</groupId>
          <artifactId>jaxb-runtime</artifactId>
          <version>${jaxb.version}</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.200</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>management-monitoring</finalName>
        <resources>
            <resource>
                <directory>${basedir}/src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/application*.yml</include>
                    <include>**/application*.yaml</include>
                    <include>**/application*.properties</include>
                </includes>
            </resource>
            <resource>
                <directory>${basedir}/src/main/resources</directory>
                <excludes>
                    <exclude>**/application*.yml</exclude>
                    <exclude>**/application*.yaml</exclude>
                    <exclude>**/application*.properties</exclude>
                </excludes>
            </resource>
        </resources>

        <plugins>
            <plugin>
                <groupId>pl.project13.maven</groupId>
                <artifactId>git-commit-id-plugin</artifactId>
                <version>${git-commit-id-plugin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>revision</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <verbose>true</verbose>
                    <dateFormat>yyyy-MM-dd'T'HH:mm:ssZ</dateFormat>
                    <generateGitPropertiesFile>true</generateGitPropertiesFile>
                    <generateGitPropertiesFilename>
                        ${project.build.outputDirectory}/git.properties
                    </generateGitPropertiesFilename>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.2.3</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>build-info</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Your configuration class is now a bit smaller. You can delete your web.xml file because SpringBootServletInitializer takes care of most the boilerplate stuff:

package ca.airspeed.demo.observability;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@SpringBootApplication
public class AppConfig extends SpringBootServletInitializer {

@Bean
public ViewResolver getViewResolver(){
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}

Add an application.yml file in src/main/resources to round out what is no longer in web.xml, plus a few other useful settings we’ll talk about later:

management:
endpoints:
web:
base-path: /manage

server:
servlet:
contextPath: /management-monitoring

spring:
jackson:
date-format: yyyy-MM-dd'T'HH:mm:ssXXX
time-zone: America/Winnipeg
serialization:
write-dates-as-timestamps: false
jpa:
open-in-view: false
mvc:
static-path-pattern: /resources/**

The default management web endpoint is /actuator. If that works for you, fine. My clients have found /manage to be more intuitive.

Change spring.jackson.time-zone to your local TZ database name.

Checkout commit-id 973c292, build it the app, deploy it to your Tomcat container, and browse to http://localhost:8080/. You should get the same welcome Hello World page.

But now, do a GET on http://localhost:8080/management-monitoring/manage/health. If you used HTTPie you would get this:

HTTP/1.1 200 
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Mon, 22 Jun 2020 20:23:26 GMT
Keep-Alive: timeout=20
Transfer-Encoding: chunked

{
"status": "UP"
}

Notice you get an HTTP status code 200 OK, followed by a simple JSON object that reports all is fine. If anything went wrong (such as a failure to obtain a connection to the database), you’d get a 503 Service Unavailable, and the “status” would be “DOWN”.

Are you thinking how easy it would be for your monitoring tool to do a simple GET on this web endpoint, and raise an alert if you got anything other than 200 OK? All you did was add a few dependencies, a tweaked application.yml, and that’s it. You didn’t have to code any Controller classes. Thanks to Spring Boot’s auto discovery and configuration, it’s that simple. Of course, you can customize to your heart’s content; check the documentation.

Quick, what version of the app are we running? Just to a GET on /info (http://localhost:8080/management-monitoring/manage/info):

HTTP/1.1 200 
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Mon, 18 Jan 2021 23:38:20 GMT
Expires: 0
Keep-Alive: timeout=20
Pragma: no-cache
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

{
    "build": {
        "artifact": "management-monitoring",
        "group": "ca.airspeed.demo",
        "name": "Managing and Monitoring Spring Apps",
        "time": "2021-01-18T23:37:04.318Z",
        "version": "1.0.0-SNAPSHOT"
    },
    "git": {
        "branch": "master",
        "commit": {
            "id": "973c292",
            "time": "2021-01-18T23:34:25Z"
        }
    }
}

There you go. Version number, build timestamp, and the branch and commit id used to build it. The Git information comes from the git-commit-id-plugin we used.

There’s More

The /health and /info endpoints are nice for basic monitoring, but what about all the other endpoints? The ones marked as “sensitive” require you to be properly authenticated.

Here we’ll use Spring Security’s simplistic default to achieve authentication. In the real world you would use Spring Security to authenticate against your company’s directory service, such as Microsoft Active Directory.

To begin with, add the the spring-boot-starter-security and micrometer-registry-prometheus dependencies to your POM:

        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.5.1</version>
</dependency>

With Spring Boot 2.x, the sensitive endpoints are not exposed by default, so you need to enable them. We’ll do that next, and we’ll require people to be authenticated and authorized to view sensitive information. By default, “authorization” simply means successfully authenticated. There are configuration settings that let you restrict these sensitive endpoints to members of specific directory groups.

Next, we add a few lies to the top of our application.yml file:

management:
endpoint:
health:
show-details: when-authorized
endpoints:
web:
base-path: /manage
exposure:
include:
- health
- info
- prometheus
- metrics

server:
servlet:
contextPath: /management-monitoring

spring:
jackson:
date-format: yyyy-MM-dd'T'HH:mm:ssXXX
time-zone: America/Winnipeg
serialization:
write-dates-as-timestamps: false
jpa:
open-in-view: false
mvc:
static-path-pattern: /resources/**

The default configuration of Spring Security is an in-memory store that has but one userid: “user”. (Yeah, who knew?). The password is displayed to stdout on Tomcat’s startup, so edit logback.xml to set the logging level of UserDetailsServiceAutoConfiguration to INFO:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<property name="CONSOLE_LOG_PATTERN"
value="%d{HH:mm:ss.SSSXXX} [%thread] %-5level %logger{36} - %msg%n" />
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />

<logger name="ca.airspeed.demo.observability" level="INFO"
additivity="false">
<appender-ref ref="CONSOLE" />
</logger>
<logger
name="org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration"
level="INFO" additivity="false">
<appender-ref ref="CONSOLE" />
</logger>
<logger name="org.springframework" level="WARN"
additivity="false">
<appender-ref ref="CONSOLE" />
</logger>

<root level="WARN">
<appender-ref ref="CONSOLE" />
</root>

</configuration>

Checkout commit id 4280c1c from my Github repo, build and deploy to your Tomcat container. Then start Tomcat and watch the stdout for the phrase “Using generated security password”. That’s the password for userid “user; it will be a randomly-generated UUID.

More Health Information

Now when you supply login credentials, you get much more information at /health. For example, using HTTPie:

$ http --session=test1 --auth=user http://localhost:8080/management-monitoring/manage/health
http: password for user@localhost:8080: <Enter the generated security password from stdout>


HTTP/1.1 200 
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Mon, 18 Jan 2021 23:42:09 GMT
Expires: 0
Keep-Alive: timeout=20
Pragma: no-cache
Set-Cookie: JSESSIONID=318B140944AFEECC3C64CB6EAD85612D; Path=/management-monitoring; HttpOnly
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

{
    "components": {
        "db": {
            "details": {
                "database": "H2",
                "validationQuery": "isValid()"
            },
            "status": "UP"
        },
        "diskSpace": {
            "details": {
                "exists": true,
                "free": 43809165312,
                "threshold": 10485760,
                "total": 250790436864
            },
            "status": "UP"
        },
        "ping": {
            "status": "UP"
        }
    },
    "status": "UP"
}

Check it out: the “db” component just fetched a connection from the connection pool and performed the validation query to ensure the database is up, alive, and accessible. “diskSpace” is thrown in there by default. Spring Boot did this because it saw you have a database JDBC driver on your classpath, so it went ahead and configured both the datasource and connection pool, as well as a HealthIndicator specific to H2.

It gets better: If you are using JMS to publish or subscribe to a message broker, Spring Boot will see you have spring-boot-starter-jms on your classpath, and will automatically configure a HealthIndicator for JMS. All without you asking for it! Same thing if you are using an SMTP server, and LDAP server, Redis, whatever. All the automatically configured HealthIndicators are here.

Prometheus

If you are using Prometheus as a log aggregator, the micrometer-registry-prometheus dependency exposes a default Prometheus scrape at /prometheus (http://localhost:8080/management-monitoring/manage/prometheus). If you used HTTPie for the GET on /health above, you’re already signed in. Just go:

http --session=test1 http://localhost:8080/management-monitoring/manage/prometheus

Metrics

Do a GET on /metrics and you’ll get a huge list of the available metrics. Append any one of them and you get the statistics for it. For example, The number of active database connections:

$ http --session=test1  http://localhost:8080/management-monitoring/manage/metrics/jdbc.connections.active

HTTP/1.1 200
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Disposition: inline;filename=f.txt
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Mon, 22 Jun 2020 21:58:08 GMT
Expires: 0
Keep-Alive: timeout=20
Pragma: no-cache
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

{
"availableTags": [
{
"tag": "name",
"values": [
"dataSource"
]
}
],
"baseUnit": null,
"description": "Current number of active connections that have been allocated from the data source.",
"measurements": [
{
"statistic": "VALUE",
"value": 0.0
}
],
"name": "jdbc.connections.active"
}

Note that if you’re still using Spring Boot 1.5.x (it was end-of-lifed Aug 1, 2019), /metrics shows less detail about these metrics.

Spring Boot Actuator meters every MVC endpoint under the metric /http.server.requests. So to see statistics on the home endpoint (/”) using HTTPie, just go:

$ http --session=test1  http://localhost:8080/management-monitoring/manage/metrics/http.server.requests?tag=uri:/

HTTP/1.1 200
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Disposition: inline;filename=f.txt
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Mon, 22 Jun 2020 22:11:04 GMT
Expires: 0
Keep-Alive: timeout=20
Pragma: no-cache
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

{
"availableTags": [
{
"tag": "exception",
"values": [
"None"
]
},
{
"tag": "method",
"values": [
"GET"
]
},
{
"tag": "outcome",
"values": [
"SUCCESS"
]
},
{
"tag": "status",
"values": [
"200"
]
}
],
"baseUnit": "seconds",
"description": null,
"measurements": [
{
"statistic": "COUNT",
"value": 1.0
},
{
"statistic": "TOTAL_TIME",
"value": 0.091097208
},
{
"statistic": "MAX",
"value": 0.091097208
}
],
"name": "http.server.requests"
}

You could have your monitoring system consume endpoints like this on a regular basis, and raise alarms when the “MAX” exceeds a pre-determined threshold. You could monitor /metrics/http.server.requests?tag=outcome:SERVER_ERROR and raise a similar alarm. Or pull the counts and see what are the most popular endpoints.

Changing the Logging Level

The /loggers endpoint lets you view (GET) and even change (POST) the logging configuration at runtime without the need to restart the application. Think of how useful this when you’re troubleshooting an intermittent problem in production.

In Summary

Managing and Monitoring your Java applications tells you what is going on inside your application. It gives you a quick way to tell whether or not the application is up, running, and working. It lets you measure its usage, and gives you advance warning when performance is degrading.

Learn more at Spring Boot Actuator: Production-ready Features.

Share this: