Dieser Artikel ist auch auf Deutsch verfügbar

The presentation held in December 2024 at the 38th Chaos Communication Congress, the conference of the Chaos Computer Club e. V., entitled “We know where your car is – Volkswagen’s people data” “Wir wissen, wo dein Auto steht – Volksdaten von Volkswagen” put Spring Boot’s Actuator module in the spotlight. Although the module usually does its work quietly and secretly, it played a major role there as a gateway.

In his post “Spring Actuator Security”, Gerrit Meier already showed that the use of Actuator does not lead to a security vulnerability per se. And yet all the hype surrounding this topic has shown me that the Actuator module is relatively unknown. This is precisely why we want to take a closer look at it below.

Of course, the question arises: What more can I tell you here than the official documentation? The honest answer is: “Probably nothing.” I encourage everyone to always refer to the official documentation when in doubt. However, there are people who will benefit from reading this again in German, or they may appreciate the slightly different wording or the coherent text. If you are not one of them, you can stop reading here and look at the documentation for Actuator if you are interested or need to.

Production-ready

Under the motto “production-ready”, Actuator primarily bundles features in the Actuator module that support the operation of the application. Most of the features allow us to read information from a running application from outside. In some cases, however, it is also possible to trigger actions within the application from outside. These features can be roughly divided into two larger areas.

The first area is endpoints. This is the abstraction chosen by Actuator to provide information and actions independently of the actual transport route. All features that revolve around the topic of observability can be summarized in the second area.

Due to limited space, we will only look at the first area, endpoints.

Activation

As is typical for most Spring Boot modules, the first and most important step is to define the appropriate starter spring-boot-starter-actuator in the project as a dependency. If we now start our application, the features are already available to us.

In addition to pure activation, we must now provide access to Actuator, an important step that should not be forgotten. The first step is to decide whether we want to communicate with Actuator via HTTP or JMX. If our application is a web application, the path via HTTP is automatically activated and Actuator can be accessed via the HTTP port of the application under the path /actuator; see Listing 1.

$ http :8080/actuator
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Wed, 08 Jan 2025 20:08:37 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "_links": {
    "health": {
      "href": "http://localhost:8080/actuator/health",
      "templated": false
    },
    "health-path": {
      "href": "http://localhost:8080/actuator/health/{*path}",
      "templated": true
    },
    "self": {
      "href": "http://localhost:8080/actuator",
      "templated": false
    }
  }
}
Listing 1: http call of /actuator with default settings

In addition to the path via the property management.endpoints.web.base-path, it is also possible to ensure that Actuator listens on a different port than its own application in this case. To do this, we configure management.server.port to the desired port. We can also use management.server.address to select the network interface, or localhost, in order to restrict possible access at network level.

If we do not want to access it via HTTP, access can be switched off completely by setting management.server.port to the value -1. This makes sense if we are not using endpoints or if we are accessing everything via JMX, the Java Mangagement Extensions. To do this, we must set the property spring.jmx.enabled to true, since it is not activated by default. We can then access the MBeans provided by Actuator via JMX, for example via the jconsole; see Figure 1.

Abb. 1: Display Actuator MBeans via jconsole

Below, we will use the HTTP route to look at the endpoint area.

Endpoints

As already mentioned, endpoints primarily enable read access to information within the running application. Spring Boot comes with up to 25 endpoints by default. The activated, or more precisely the exposed, endpoints are listed under /actuator. In Listing 1, we can see that only the health endpoint can be accessed by default.

To make other endpoints accessible from outside, we use the property management.endpoints.web.exposure.include. This property receives a list of endpoint IDs or the asterisk to activate all endpoints. If we use the *, we can also deactivate individual endpoints using management.endpoints.web.exposure.exclude. Personally, however, I would only use the first option, mainly for security reasons. I would also only activate the endpoints that are really needed. For this article, however, we are activating all endpoints available in this application. Calling /actuator again (see Listing 2) now returns significantly more entries.

$ http :8080/actuator
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Thu, 09 Jan 2025 19:36:26 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "_links": {
    "beans": {
      "href": "http://localhost:8080/actuator/beans",
      "templated": false
    },
    "caches": {
      "href": "http://localhost:8080/actuator/caches",
      "templated": false
    },
    "caches-cache": {
      "href": "http://localhost:8080/actuator/caches/{cache}",
      "templated": true
    },
    "conditions": {
      "href": "http://localhost:8080/actuator/conditions",
      "templated": false
    },
    "configprops": {
      "href": "http://localhost:8080/actuator/configprops",
      "templated": false
    },
    "configprops-prefix": {
      "href": "http://localhost:8080/actuator/configprops/{prefix}",
      "templated": true
    },
    "env": {
      "href": "http://localhost:8080/actuator/env",
      "templated": false
    },
    "env-toMatch": {
      "href": "http://localhost:8080/actuator/env/{toMatch}",
      "templated": true
    },
    "health": {
      "href": "http://localhost:8080/actuator/health",
      "templated": false
    },
    "health-path": {
      "href": "http://localhost:8080/actuator/health/{*path}",
      "templated": true
    },
    "heapdump": {
      "href": "http://localhost:8080/actuator/heapdump",
      "templated": false
    },
    "info": {
      "href": "http://localhost:8080/actuator/info",
      "templated": false
    },
    "loggers": {
      "href": "http://localhost:8080/actuator/loggers",
      "templated": false
    },
    "loggers-name": {
      "href": "http://localhost:8080/actuator/loggers/{name}",
      "templated": true
    },
    "mappings": {
      "href": "http://localhost:8080/actuator/mappings",
      "templated": false
    },
    "metrics": {
      "href": "http://localhost:8080/actuator/metrics",
      "templated": false
    },
    "metrics-requiredMetricName": {
      "href": "http://localhost:8080/actuator/metrics/{requiredMetricName}",
      "templated": true
    },
    "sbom": {
      "href": "http://localhost:8080/actuator/sbom",
      "templated": false
    },
    "sbom-id": {
      "href": "http://localhost:8080/actuator/sbom/{id}",
      "templated": true
    },
    "scheduledtasks": {
      "href": "http://localhost:8080/actuator/scheduledtasks",
      "templated": false
    },
    "self": {
      "href": "http://localhost:8080/actuator",
      "templated": false
    },
    "threaddump": {
      "href": "http://localhost:8080/actuator/threaddump",
      "templated": false
    }
  }
}
Listing 2: Calling /actuator after activating all available endpoints

All endpoints can be provided with a time-to-live (TTL). This ensures that the actual logic is only run through every x time units and that the last result is returned in between for new calls. This ensures that unnecessary work is avoided and that the entire system is thus relieved. If we want to activate this feature for an endpoint, we set the property management.endpoint.<EndpointID>.cache.time-to-live to a desired time unit.

We will now take a closer look at a few of the supplied endpoints.

health endpoint

One of the central endpoints is the health endpoint. This endpoint provides information on whether the status of the application is OK or not. To do this, we can call /actuator/health; see Listing 3.

$ http :8080/actuator/health
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Wed, 08 Jan 2025 20:13:52 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "status": "UP"
}
Listing 3: Calling /actuator/health with default settings

The returned status is an aggregation of several checks. If we also want to see their results, we can use management.endpoint.health.show-details=always to ensure that, in addition to displaying the overall result, we also receive the individual tests, even with more details; see Listing 4.

$ http :8080/actuator/health
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Wed, 08 Jan 2025 20:16:39 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "components": {
    "diskSpace": {
      "details": {
        "exists": true,
        "free": 1170781949952,
        "path": "…/javaspektrum-spring-boot-actuator/.",
        "threshold": 10485760,
        "total": 2000796545024
      },
      "status": "UP"
    },
    "ping": {
      "status": "UP"
    },
    "ssl": {
      "details": {
        "invalidChains": [],
        "validChains": []
      },
      "status": "UP"
    }
  },
  "status": "UP"
}
Listing 4: Calling /actuator/health with activated details

In addition to the three checks shown here (diskspace, ping, and ssl), Actuator includes several other checks, which are only activated if the check can be carried out. For example, there is a db check that checks whether the connection to the configured database can be established. This check is activated as soon as the software recognizes that a connection has been configured.

Of course, you can also write your own checks. To do so, see Listing 5, where we implement the interface HealthIndicator.

@Component
public class CustomHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        var now = System.currentTimeMillis();
        if (now % 2 == 0) {
            return Health.up()
                .withDetail("now", now)
                .build();
        } else {
            return Health.down()
                .withDetail("now", now)
                .build();
        }
    }
}
Listing 5: Own HealthIndicator

If we now call the health endpoint again and run into an odd time, we can see that our check has been carried out and that the overall status has now changed from up to down; see Listing 6.

$ http :8080/actuator/health
HTTP/1.1 503
Connection: close
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 11:17:24 GMT
Transfer-Encoding: chunked

{
  "components": {
    "custom": {
      "details": {
        "now": 1736507844857
      },
      "status": "DOWN"
    },
    …
  },
  "status": "DOWN"
}
Listing 6: Calling /actuator/health with its own HealthIndicator

Actuator uses a configured implementation of the interface StatusAggregator for this purpose. This interface aggregates the status values of the sub-checks into an overall result. If we do not like the aggregation, we can either configure the standard implementation via the property management.endpoint.health.status.order or provide our own implementation of StatusAggregator.

Listing 6 also shows that the HTTP status code is no longer 200 OK, but rather 503 Service Unavailable. There is a mapping option from status to status code values for this purpose. By default, the response is 200 OK, unless the health status is DOWN or OUT_OF_SERVICE, in which case the response is 503 Service Unavailable. If this mapping does not work for us, or if we do not want to respond with 200 OK for another status either, it can also be solved via configuration. To do this, we can define the mapping with management.endpoint.health.status.http-mapping.<STATUS>=<StatusCode>. It should be noted that as soon as we do this, the default setting no longer applies, which is why we also need to reset DOWN and OUT_OF_SERVICE to 503 again.

In addition to the aggregated view under /actuator/health, we can also call up individual checks if we are allowed to see details; see Listing 7.

$ http :8080/actuator/health/custom
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 19:18:28 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "details": {
    "now": 1736536708924
  },
  "status": "UP"
}
Listing 7: Calling /actuator/health/custom

Several checks can also be grouped together. To do this, the property management.endpoint.health.group.<GROUP_NAME>.include needs to be configured for a list of checks. We can then call this group in the same way as an individual check; see Listing 8.

$ http :8080/actuator/health/customgroup
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 19:22:17 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "components": {
    "custom": {
      "details": {
        "now": 1736536937142
      },
      "status": "UP"
    },
    "ping": {
      "status": "UP"
    }
  },
  "status": "UP"
}
Listing 8: Calling /actuator/health/customgroup

Unlike the individual check, however, this call also works if the details are not to be displayed.

Another special feature of the health endpoint is the dedicated support for operation in a Kubernetes cluster. Various probes can be set up there for containers, including the liveness and readiness probes. These probes are used to restart the container if necessary (liveness) or to determine whether the container should receive network traffic (readiness).

To provide proper support for the probes, the module includes two checks for the health endpoint that can be used specifically for this purpose. They are automatically activated when Spring Boot recognizes that our application is running in a cluster. Alternatively, we can also activate them manually using the property management.endpoint.health.probes.enabled. Two new checks are then available; see Listing 9.

$ http :8080/actuator/health/liveness
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 20:28:23 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "status": "UP"
}


$ http :8080/actuator/health/readiness
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 20:28:25 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "status": "UP"
}
Listing 9: Calling /actuator/health/liveness and /actuator/health/readiness

Actuator uses the concept of Application Availability provided by Spring Boot for this purpose. Since these checks are registered internally as groups, it is possible to configure additional checks alongside Application Availability. However, we should be aware of the impact this has on the evaluating system. For example, if we were to extend the liveness check to check whether the database is accessible, Kubernetes would simply restart our application, if it is not accessible. However, a restart is often useless here, since it means the database cannot be reached.

If Actuator is operated on its own port, these two checks also do not ensure that the actual application is still accessible. In this case, it may be useful to configure the property management.endpoint.health.probes.add-additional-paths. The two groups can then also be reached under /livez and /readyz via the application’s regular HTTP port.

info endpoint

The second endpoint we want to look at is the info endpoint. This endpoint can be used to query any values that seem relevant to us at runtime. By default, this endpoint returns an empty object under /actuator/info; see Listing 10.

$ http :8080/actuator/info
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 12:53:47 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{}
Listing 10: Calling /actuator/info with default settings

Therefore, we need to configure more in order to be able to see anything here. Like the health endpoint, the info endpoint consists of a set of subcomponents that implement InfoContributor, the sum of which is displayed.

Actuator already includes the contributors build, env, git, java, os, process, and ssl by default. However, it should be noted that only build and git are activated by default. Since both also require a file that is created during the build, we do not see any values by default.

In order to see the maximum integrated values, we expand the build of our project with the things shown in Listing 11 and activate the remaining contributors in the application.properties of the application; see Listing 12.

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <!-- erzeugt META-INF/build.properties -->
      <executions>
        <execution>
          <goals>
            <goal>build-info</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
    <!-- erzeugt META-INF/build.properties -->
    <plugin>
      <groupId>io.github.git-commit-id</groupId>
      <artifactId>git-commit-id-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>
Listing 11: Build extension for build and git information
…
management.info.env.enabled=true
management.info.java.enabled=true
management.info.os.enabled=true
management.info.process.enabled=true
info.greeting=Hallo
…
Listing 12: Activating the remaining InfoContributors

If we now start the application via INFO_START_DATE=$(date) mvn spring-boot:run and call the info endpoint again, we get the result in Listing 13.

$ http :8080/actuator/info
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 16:34:55 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "build": {
    "artifact": "spring-boot-actuator",
    "group": "de.mvitz",
    "name": "spring-boot-actuator",
    "time": "2025-01-10T16:34:51.301Z",
    "version": "0.0.1-SNAPSHOT"
  },
  "git": {
    "branch": "main",
    "commit": {
      "id": "e775d35",
      "time": "2025-01-10T16:26:53Z"
    }
  },
  "greeting": "Hallo",
  "java": {
    "jvm": {
      "name": "OpenJDK 64-Bit Server VM",
      "vendor": "Eclipse Adoptium",
      "version": "21.0.5+11-LTS"
    },
    "runtime": {
      "name": "OpenJDK Runtime Environment",
      "version": "21.0.5+11-LTS"
    },
    "vendor": {
      "name": "Eclipse Adoptium",
      "version": "Temurin-21.0.5+11"
    },
    "version": "21.0.5"
  },
  "os": {
    "arch": "x86_64",
    "name": "Mac OS X",
    "version": "14.7.1"
  },
  "process": {
    "cpus": 16,
    "memory": {
      "heap": {
        "committed": 67108864,
        "init": 1073741824,
        "max": 17179869184,
        "used": 26722600
      },
      "nonHeap": {
        "committed": 62849024,
        "init": 2555904,
        "max": -1,
        "used": 61210864
      }
    },
    "owner": "mvitz",
    "parentPid": 53199,
    "pid": 53226
  },
  "start": {
      "date": "Fr 10 Jan 2025 17:34:49 CET"
  }
}
Listing 13: Calling /actuator/info after activating all InfoContributors

Here we can clearly see which contributor has added which information. A unique feature here is the envcontributor, which adds all properties that begin with info. In this example, this refers to the info.greeting entry from application.properties and the environment variable INFO_START_DATE that we configured at startup.

If this information is still not enough, we can output even more. We can also use management.info.git.mode=full to output the remaining entries in the git.properties file and we can create any additional entries by providing our own contributors; see Listing 14.

@Component
public class CustomInfoContributor implements InfoContributor {

    @Override
    public void contribute(Info.Builder builder) {
        builder
            .withDetail("foo", "bar")
            .withDetail("bar", "foo")
            .withDetail("answer", 42);
    }
}
Listing 14: Implementation of a separate InfoContributor

env endpoint

At first glance, the env endpoint appears to overlap with the env contributor. However, if we call it up (see Listing 15), we quickly see that this is not the case.

$ http :8080/actuator/env
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 16:47:05 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "activeProfiles": [],
  "defaultProfiles": [
    "default"
  ],
  "propertySources": [{
      "name": "systemProperties",
      "properties": {
        "CONSOLE_LOG_CHARSET": {
          "value": "******"
        },
        …
      }
    },
    {
      "name": "systemEnvironment",
      "properties": {"EDITOR": {
          "origin": "System Environment Property \"EDITOR\"",
          "value": "******"
        },
        …
      }
    },
    {
      "name": "Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'",
      "properties": {
        "info.greeting": {
          "origin": "class path resource [application.properties] - 7:15",
          "value": "******"
        },
        …
      }
    },
    {
      "name": "devtools",
      "properties": {
        "server.error.include-binding-errors": {
          "value": "******"
        },
        …
      }
    },
    …
  ]
}
Listing 15: Calling /actuator/env with default settings

In addition to the utilized profiles, all configuration values that could potentially be used within the application are output here. They are grouped according to their source, such as environment variables. Also, for some sources we receive even more detailed information, such as the exact file and the line and column number within it.

However, we also see that all values have been masked for security reasons. This is done for security reasons, since this endpoint could otherwise possibly also output sensitive data such as credentials. If we now want to see the actual values, we need to set the property management.endpoint.env.show-values to always or when-authorized. If we now call the endpoint again (see Listing 16), we can also see the real values.

$ http :8080/actuator/env
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 18:15:04 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "activeProfiles": [],
  "defaultProfiles": [
    "default"
  ],
  "propertySources": [
    ...
    {
      "name": "systemProperties",
      "properties": {
        "CONSOLE_LOG_CHARSET": {
          "value": "UTF-8"
        },
        ...
      }
    },
    {
      "name": "systemEnvironment",
      "properties": {
        ...
        "EDITOR": {
          "origin": "System Environment Property \"EDITOR\"",
          "value": "vi"
        },
        ...
      }
    },
    {
      "name": "Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'",
      "properties": {
        "info.greeting": {
          "origin": "class path resource [application.properties] - 7:15",
          "value": "Hallo"
        },
        ...
      }
    },
    {
      "name": "devtools",
      "properties": {
        "server.error.include-binding-errors": {
          "value": "always"
        },
        ...
      },
      ...
    ]
}
Listing 16: Calling /actuator/env with displayed values

Alternatively, we can also provide our own implementations of SanitizingFunction as beans in order to only mask certain values; see Listing 17.

@Component
public class CustomSanitizingFunction implements SanitizingFunction {

    @Override
    public SanitizableData apply(SanitizableData data) {
        return switch (data.getKey()) {
            case "CONSOLE_LOG_CHARSET" -> data.withSanitizedValue();
            case "EDITOR" -> data.withValue("nano");
            default -> data;
        };
    }
}
Listing 17: Own SanitizingFunction

It should be noted, however, that this function is then also executed for the values of the configprops and quartz endpoints.

beans and conditions endpoint

Two other endpoints that have often helped me during development are beans and conditions. As you can guess from the name, the former lists all the Spring beans present in the application; see Listing 18.

$ http :8080/actuator/beans
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 18:38:05 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "contexts": {
    "application": {
      "beans": {
        ...
        "customHealthIndicator": {
          "aliases": [],
          "scope": "singleton",
          "type": "de.mvitz.sb.actuator.CustomHealthIndicator",
          "resource": "file [.../target/classes/de/mvitz/sb/actuator/CustomHealthIndicator.class]",
          "dependencies": []
        },
        "jacksonObjectMapperBuilder": {
          "aliases": [],
          "scope": "prototype",
          "type": "org.springframework.http.converter.json.Jackson2ObjectMapperBuilder",
          "resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration.class]",
          "dependencies": [
            "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration",
            "org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@40a75085",
            "standardJacksonObjectMapperBuilderCustomizer"
          ]
        },
        ...
      }
    }
  }
}
Listing 18: Calling /actuator/beans

Besides the name of the bean, we also receive additional information, such as the location where it was declared or its dependencies.

The situation is similar with the conditions endpoint; see Listing 19.

$ http :8080/actuator/conditions
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 18:45:36 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "contexts": {
    "application": {
      "positiveMatches": {
        ...
        "...MappingJackson2HttpMessageConverterConfiguration": [
          {
            "condition": "OnClassCondition",
            "message": "@ConditionalOnClass found required class 'com.fasterxml.jackson.databind.ObjectMapper'"
          },
          {
            "condition": "OnPropertyCondition",
            "message": "@ConditionalOnProperty (spring.mvc.converters.preferred-json-mapper=jackson) matched"
          },
          {
            "condition": "OnBeanCondition",
            "message": "@ConditionalOnBean (types: com.fasterxml.jackson.databind.ObjectMapper; SearchStrategy: all) found bean 'jacksonObjectMapper'"
          }
        ],
        ...
      },
      "negativeMatches": {
        "RabbitHealthContributorAutoConfiguration": {
          "notMatched": [
            {
              "condition": "OnClassCondition",
              "message": "@ConditionalOnClass did not find required class 'org.springframework.amqp.rabbit.core.RabbitTemplate'"
            }
          ],
          "matched": []
        },
        ...
      },
      "unconditionalClasses": [
        "...ConfigurationPropertiesAutoConfiguration",
        ...
      ]
    }
  }
}
Listing 19: Calling /actuator/conditions

It lists all existing auto-configurations in the application and indicates whether they were activated at startup and thus have an effect. For each configuration, the exact reason for this result is also listed.

Both endpoints can be very helpful, especially during development, to help with debugging when unexpected things happen that are caused by the fact that expected beans are not present or there are too many of them.

Write your own endpoints

If, despite the amount of already available endpoints, you still have other requirements, you can also write your own. To do this, we must annotate our bean with @Endpoint. Then, we can use the annotations @ReadOperation, @WriteOperation, or @DeleteOperation to annotate individual methods in order to make them available as endpoints; see Listing 20.

@Component
@Endpoint(id = "answer")
public class CustomEndpoint {

    @ReadOperation
    public int answer() {
        return 42;
    }
}
Listing 20: Own endpoint

This endpoint can be used automatically via HTTP as well as JMX. Alternatively, we could also use @WebEndpoint or @JmxEndpoint as an annotation to make the endpoint accessible via only one of the two paths. If we still need specific logic for a generic @Endpoint in order to adapt the representation via HTTP or JMX, we can also add a @EndpointWebExtension (see Listing 21) or @EndpointJmxExtension.

@Component
@EndpointWebExtension(endpoint = CustomEndpoint.class)
public class CustomEndpointWebExtension {

    private final CustomEndpoint delegate;

    public CustomEndpointWebExtension(CustomEndpoint delegate) {
        this.delegate = delegate;
    }

    @ReadOperation
    public Map<String, String> answer() {
        return Map.of("answer", Integer.toString(delegate.answer()));
    }
}
Listing 21: Own EndpointWebExtension

If we now call our own endpoint via /actuator/answer, we receive the response from Listing 22.

$ http :8080/actuator/answer
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 10 Jan 2025 19:02:54 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "answer": "42"
}
Listing 22: Calling our own endpoint via /actuator/answer

Conclusion

In this installment of my series, we took a closer look at the concept of endpoints from Spring Boot Actuator. These endpoints make it possible to query information from a running application or, in some cases, to change values. To do this, we took a detailed look at the supplied endpoints health, info, env, beans, and conditions.

The health endpoint can be used to query the status of components that are important for the application. This is primarily intended to be used when automated actions can be performed on negative responses so that the application can return to a clean state. In addition to many built-in checks, we also saw how we can write our own and we learned about the specific support for Kubernetes clusters.

The infoendpoint allows us to query a variety of information at runtime. This mainly involves things like the Java version or information about the operating system and similar. In addition to the integrated things, we also saw how we can add our own information.

Additional runtime information can be queried using the env endpoint. All configuration values, including the point at which they were set, are displayed here.

The two endpoints beans and conditions allow us to find out which Spring beans were configured as well as where and which autoconfiguration was switched on or off and why. This is particularly helpful for application debugging if things do not work as we think they should.

There is a whole range of other endpoints that are already supplied by default. I can only recommend that you take a look at these endpoints, so that you know what else they can be used for. If the supplied endpoints are not sufficient, we also learned how we can write our own and thus expand Actuator.

However, we should always keep an eye on security. Although I feel that Actuator comes with safe defaults, we have seen in the introduction how quickly accidents can happen. Therefore, it is generally a good idea to run Actuator on a separate port that can only be accessed from the management network. Addition, especially if a separate port is not an option, Actuator should be secured via Spring Security.