Appearance
Actuator: Health, Metrics and Monitoring
The application now handles requests and errors, stores data, and is secured. Once it runs in production, you also need to see inside it: is it up, how much memory does it use, how many requests did it handle, what are its threads doing? This chapter covers Actuator, step by step: adding it, the endpoints it exposes, securing them (with the Spring Security from the previous module), writing your own, and pushing metrics to a monitoring platform.
Actuator provides production-ready endpoints to monitor and manage a Spring Boot application: is it up, how much memory it uses, how many requests it handled, what its threads are doing, and more.
Setup
Add the dependency (or tick Spring Boot Actuator when creating the project):
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>Two properties, both optional:
properties
spring.application.name=order-service
# the default base path is /actuator
management.endpoints.web.base-path=/manage
# which endpoints are reachable over HTTP; '*' means all
# or pick some, comma separated: health,info,metrics,loggers
management.endpoints.web.exposure.include=*- Base path: every endpoint lives under
/actuator(/actuator/health,/actuator/metrics, ...). Here it is changed to/manage. - Exposure: without this property, only
/healthis exposed over HTTP. Every other endpoint,/infoincluded, returns 404 until you expose it.
GET /manage lists every exposed endpoint with its link.
/health
http
GET /manage/health
{"status":"UP"}The status is UP, DOWN, OUT_OF_SERVICE or UNKNOWN. When it is DOWN, the HTTP status is 503 instead of 200, so load balancers and monitoring tools can act on it.
Adding your own checks
"The server is up" is often not enough. You may want the application to count as up only if the database, the cache and the message broker are up too. Implement HealthIndicator:
java
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Override
public Health health() {
boolean isDBUp = checkDBConnection();
return isDBUp ? Health.up().withDetail("DB", "Available").build()
: Health.down().withDetail("DB", "Not-Available").build();
}
private boolean checkDBConnection() {
// for example, try a simple query
return true;
}
}java
@Component
public class CacheHealthIndicator implements HealthIndicator {
@Override
public Health health() {
boolean isCacheUp = checkCacheStatus();
return isCacheUp ? Health.up().withDetail("Cache", "Available").build()
: Health.down().withDetail("Cache", "Not-Available").build();
}
private boolean checkCacheStatus() {
return false; // down on purpose, for the demo
}
}Now /manage/health returns only {"status":"DOWN"}, with HTTP 503. The top status is an aggregate: it is UP only if every component is up; one component down makes the whole application down.
To see the components, add:
properties
management.endpoint.health.show-details=alwaysThe default is never. Now:
json
{
"status": "DOWN",
"components": {
"cache": { "status": "DOWN", "details": { "Cache": "Not-Available" } },
"database": { "status": "UP", "details": { "DB": "Available" } },
"diskSpace": { "status": "UP", "details": { ... } }
}
}Each component is named after its class without the HealthIndicator suffix: DatabaseHealthIndicator becomes database. diskSpace (and others, such as db when a real DataSource exists) are built in.
/metrics
GET /manage/metrics returns the names of all available metrics. To read one: GET /manage/metrics/{metric name}.
http
GET /manage/metrics/jvm.threads.live
{"name":"jvm.threads.live","measurements":[{"statistic":"VALUE","value":22.0}], ...}Some useful ones:
| Metric | What it represents |
|---|---|
jvm.memory.used | Memory currently used by the JVM, in bytes |
jvm.memory.max | The most memory the JVM can use |
jvm.gc.pause | Garbage collection: COUNT (GC events so far), TOTAL_TIME (time spent in GC), MAX (longest single pause) |
jvm.threads.live | Live threads now |
jvm.threads.peak | Most live threads at once since the JVM started |
system.cpu.usage | CPU usage, from 0.0 to 1.0 (0.10 means 10%) |
http.server.requests | COUNT (requests received), TOTAL_TIME (time handling them), MAX (slowest request); tags such as method and status |
jdbc.connections.active / .idle / .max | Database connection pool: in use, idle, allowed |
executor.pool.core | Core size of thread pool executors |
Tags narrow a metric down: /manage/metrics/http.server.requests?tag=status:500.
/threaddump
Helps diagnose deadlocks and thread leaks. For each thread: name, id, state (RUNNABLE, BLOCKED, WAITING, ...), how long and how often it was blocked or waiting, and the stack trace showing which code it is running:
json
{
"threadName": "main",
"threadId": 1,
"threadState": "RUNNABLE",
"stackTrace": [ "com.example.MyService.methodName(MyService.java:142)", "..." ]
}Other endpoints
| Endpoint | Method | What it does |
|---|---|---|
/heapdump | GET | Downloads the JVM heap as an .hprof file |
/mappings | GET | Lists all Spring MVC request mappings |
/beans | GET | Lists all the Spring beans in the application |
/configprops | GET | Lists all @ConfigurationProperties beans |
/loggers | GET | Lists the loggers and their levels (a POST can change a level at runtime) |
/env | GET | Shows the environment properties; /env/{property} shows one |
/shutdown | POST | Shuts the application down gracefully |
The official Spring Boot documentation (Actuator → Endpoints) lists them all.
/env and /configprops show values as ****** by default, since they could contain secrets.
The two dangerous ones
Even with exposure.include=*, /shutdown and /heapdump are not available:
/shutdownstops your application./heapdumpcan expose sensitive data held in memory, such as tokens and passwords.
You must allow them explicitly, as a way of saying "I accept the risk":
properties
management.endpoint.heapdump.access=unrestricted
management.endpoint.shutdown.access=unrestricted(The access property exists from Spring Boot 3.4. Older versions use management.endpoint.shutdown.enabled=true.)
Securing the endpoints
All exposed endpoints are public: anyone who knows your host and port can call them, and they reveal a lot about your system. Protect them with Spring Security (covered in depth in the security chapters):
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/manage/health", "/manage/info").permitAll() // public
.anyRequest().authenticated()) // the rest needs login
.httpBasic(Customizer.withDefaults()); // basic auth, for simplicity
return http.build();
}
}properties
spring.security.user.name=user
spring.security.user.password=pass
spring.security.user.roles=ADMINNow GET /manage/env without credentials returns 401; with user/pass it returns the data. In a real application, also require a role, for example .requestMatchers("/manage/**").hasRole("ADMIN").
Custom endpoints
Add your own endpoint with @Endpoint. Its id becomes the path:
java
@Component
@Endpoint(id = "my-custom-stats")
public class MyCustomStatsEndpoint { ... }
// available at /manage/my-custom-stats (or /actuator/my-custom-stats)Methods say which HTTP method they answer:
| Annotation | HTTP method |
|---|---|
@ReadOperation | GET |
@WriteOperation | POST |
@DeleteOperation | DELETE |
java
@Component
@Endpoint(id = "my-custom-stats")
public class MyCustomStatsEndpoint {
// GET /manage/my-custom-stats
// any serializable return type: String, Map, List, POJO, primitive...
@ReadOperation
public String readAll() {
return "Hello, Spring Boot!";
}
// GET /manage/my-custom-stats/{name}/{message}
@ReadOperation
public String read(@Selector String name, @Selector String message) {
return "Hello: " + name + " msg for you is: " + message;
}
// POST /manage/my-custom-stats
@WriteOperation
public String refresh() {
return "refreshed"; // for example, refresh a cache
}
// DELETE /manage/my-custom-stats/{key}
@DeleteOperation
public String remove(@Selector String key) {
return "reset done for key: " + key;
}
}@Selector parameters are the extra path segments, matched in order: GET /manage/my-custom-stats/John/hello calls read("John", "hello"). Spring picks the method by HTTP method and number of selectors.
A custom endpoint must be exposed like any other: *, or its id in the list:
properties
management.endpoints.web.exposure.include=my-custom-stats,health,infoPOST and DELETE with Spring Security. Without Spring Security, the write and delete operations above work like the read ones. Once Spring Security is added, POST and DELETE calls are rejected even with the right username and password: Spring Security's CSRF protection blocks state-changing requests that carry no CSRF token. For an API called with basic authentication (no browser session), disable CSRF:
java
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.csrf(csrf -> csrf.disable())
.httpBasic(Customizer.withDefaults());Now POST and DELETE with credentials succeed. (CSRF is explained in the chapter on attacks.)
Spring itself uses this mechanism: several Spring projects add their own actuator endpoints, for example the refresh endpoint used with centralized configuration.
Pushing metrics to a monitoring platform
Companies watch these metrics on a monitoring platform such as Datadog, Prometheus or CloudWatch. Spring Boot can push them for you. For Datadog, add the Micrometer registry:
xml
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-datadog</artifactId>
</dependency>properties
# the API key created in your Datadog account
management.datadog.metrics.export.api-key=${DATADOG_API_KEY}
# push only when true
management.datadog.metrics.export.enabled=true
# push every 5 seconds
management.datadog.metrics.export.step=5sEvery Actuator metric is then sent to Datadog, where you can graph it (for example http.server.requests count). Never write the API key into application.properties; read it from an environment variable as above.
Next: Production applications carry a lot of configuration. Chapter 44, the last, returns to configuration with @ConfigurationProperties: binding a whole group of properties to a typed, validated Java object, instead of many separate @Value fields.
Check yourself
- What is the default base path, and how do you change it?
- Without
management.endpoints.web.exposure.include, which endpoint is reachable over HTTP? - How do you add a database check to
/health? What name will it appear under if the class isDatabaseHealthIndicator? - The DB indicator is up and the cache indicator is down. What does
/healthreturn, and with which HTTP status? - Why do you see only
{"status":"DOWN"}and no components, and how do you fix it? - How do you list all metrics, and read one?
- When would you use
/threaddump? - With
include=*, why can you still not call/shutdownor/heapdump? How do you allow them? - How do you map a custom endpoint method to GET, POST and DELETE? What does
@Selectordo? - With Spring Security added,
POST /manage/my-custom-statsfails even with correct credentials. Why? - Why should the Datadog API key not be written into
application.properties?
Answers
/actuator. Setmanagement.endpoints.web.base-path, for example/manage.- Only
/health. - A
@ComponentimplementingHealthIndicatorthat returnsHealth.up()orHealth.down(). It appears asdatabase. {"status":"DOWN"}with HTTP 503: the aggregate is down if any component is down.- Details are hidden by default (
never). Setmanagement.endpoint.health.show-details=always. GET /manage/metrics;GET /manage/metrics/{name}, e.g./manage/metrics/jvm.memory.used.- To diagnose deadlocks or thread leaks: which threads are running, blocked or waiting, and what code they run.
- They are dangerous (stop the app; leak secrets from memory), so they are restricted by default. Set
management.endpoint.shutdown.access=unrestrictedandmanagement.endpoint.heapdump.access=unrestricted. @ReadOperation,@WriteOperation,@DeleteOperation.@Selectorparameters take the extra path segments, in order.- Spring Security's CSRF protection rejects POST and DELETE without a CSRF token. Disable CSRF for such APIs.
- Anyone with access to the code would have the key. Pass it through an environment variable.