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
}
}
}
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.
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
}
}
}
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"
}
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"
}
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();
}
}
}
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"
}
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"
}
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"
}
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"
}
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
{}
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>
…
…
management.info.env.enabled=true
management.info.java.enabled=true
management.info.os.enabled=true
management.info.process.enabled=true
info.greeting=Hallo
…
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"
}
}
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);
}
}
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": "******"
},
…
}
},
…
]
}
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"
},
...
},
...
]
}
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;
};
}
}
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"
]
},
...
}
}
}
}
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",
...
]
}
}
}
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;
}
}
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()));
}
}
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"
}