What Actuator is
The problem. The Java services on this box (orders, payments) and in the cluster are black boxes to the platform: is it healthy, which config value won, what are its threads doing? Opening a shell inside the container is slow or impossible. Spring Boot apps can answer those questions over HTTP - if someone switches it on.
What you need to know already: the JVM, thread dumps and heap dumps (20.1, 20.20, 20.28), probes (17.20, 17.22), curl and HTTP status codes (9.21), jq (7.11).
A few words first, for someone who has never written Java:
- Spring Boot - the most common Java framework for web services: it starts a web server (Tomcat) inside the app and wires the app's pieces together. Think "Express for Java, with batteries included".
- starter - a dependency (like an npm package) that adds one feature;
spring-boot-starter-actuatoris the one we care about. - property - one config setting,
key=value, usually inapplication.properties(lesson 21.5 shows every place they can come from). - endpoint - one URL the app answers.
Actuator (spring-boot-starter-actuator) adds operational endpoints to a Spring Boot app: health, metrics, environment, thread dumps, heap dumps, loggers. It is how the platform - Kubernetes probes, monitoring tools, you at 3am - talks to the app without a shell. From the platform side it is the most important API the developers ship.
Exposure: only health, by default
Endpoints exist, but over HTTP only health is exposed out of the box (since Boot 2.5). The rest return 404 until someone opts in. (curl -s URL | jq . = fetch quietly and pretty-print the JSON; the _links list is what is exposed.)
$ curl -s localhost:8080/actuator | jq .
{
"_links": {
"self": { "href": "http://localhost:8080/actuator", "templated": false },
"health": { "href": "http://localhost:8080/actuator/health", "templated": false },
"health-path": { "href": "http://localhost:8080/actuator/health/{*path}", "templated": true }
}
}
$ curl -s localhost:8080/actuator/prometheus
{"timestamp":"2026-09-23T10:00:04.605+00:00","status":404,"error":"Not Found","path":"/actuator/prometheus"}
Exposure is configuration:
management.endpoints.web.exposure.include=health,info,metrics,prometheus,threaddump,env
management.endpoints.web.exposure.exclude=env # exclude wins over include
management.endpoints.web.exposure.include=* # everything - think twice
After a restart the startup log tells you what it did:
o.s.b.a.e.web.EndpointLinksResolver : Exposing 6 endpoints beneath base path '/actuator'
The endpoints you will use
/actuator/health UP/DOWN, and the liveness/readiness groups
/actuator/info build and git info, if the app adds it
/actuator/metrics list of meter names; /actuator/metrics/{name}?tag=k:v
/actuator/prometheus every counter and timer as a plain-text page any monitoring tool can read (21.11)
/actuator/env every property source and where each value came from
/actuator/env/{name} one property, across all sources
/actuator/configprops the app's settings objects ("beans", see below) and the values they got
/actuator/loggers read and CHANGE log levels at runtime (POST)
/actuator/threaddump thread dump: JSON by default, text with Accept: text/plain
/actuator/heapdump an HPROF heap dump, streamed to you
/actuator/circuitbreakers circuit breaker state (21.22), when the Resilience4j library is present
(A bean is an object Spring creates and manages for the app - a database pool, an HTTP client, a settings holder. Actuator can list them all.)
The thread dump over HTTP is the one to remember when the image has no JDK (so no jcmd, 20.3). -H 'Accept: text/plain' asks for the classic text format instead of JSON:
# once threaddump is exposed (the next mission does that)
curl -s -H 'Accept: text/plain' localhost:8080/actuator/threaddump | head -5
2026-09-23 10:00:08
Full thread dump OpenJDK 64-Bit Server VM (21.0.8+9-Ubuntu-0ubuntu1~26.04 mixed mode, sharing):
...
curl -s localhost:8080/actuator/threaddump | jq -r '.threads[].threadState' | sort | uniq -c
10 RUNNABLE
4 TIMED_WAITING
32 WAITING
And the heap dump - a binary; do not let it hit your terminal:
# once heapdump is exposed
curl -s localhost:8080/actuator/heapdump
Warning: Binary output can mess up your terminal. Use "--output -" to tell curl to output it to your terminal anyway, or consider "--output <FILE>" to save to a file.
curl -s -o /tmp/orders.hprof localhost:8080/actuator/heapdump
ls -l /tmp/orders.hprof
Security: these endpoints are dangerous
/env can show secrets, /heapdump contains every secret in memory, /loggers lets anyone turn on DEBUG logging of request bodies, /shutdown (off by default) stops the app. Standard practice:
- Put management on its own port (
management.server.port=8081) that is not routed through the Ingress (16.19) - only the kubelet and your monitoring reach it. - Expose the minimum: health, prometheus, and what your runbooks actually use.
- If sensitive endpoints are exposed, protect them with Spring Security (Spring's login-and-roles module).
The env endpoint masks values
Since Boot 3.0 the default is management.endpoint.env.show-values=never: every value is ******. You still get where each value came from, which is what you need to debug precedence:
# with env exposed (the config mission's setup does that)
curl -s localhost:8080/actuator/env/db.pool.max | jq .
{
"property": { "source": "systemEnvironment", "value": "******" },
"propertySources": [
{ "name": "commandLineArgs" },
{ "name": "systemProperties" },
{ "name": "systemEnvironment",
"property": { "value": "******", "origin": "System Environment Property \"DB_POOL_MAX\"" } },
{ "name": "Config resource 'file [/etc/orders/app.conf]' via location 'optional:file:/etc/orders/app.conf[.properties]'",
"property": { "value": "******", "origin": "URL [file:/etc/orders/app.conf] - 6:13" } },
...
]
}
show-values=always shows everything - including passwords, because Boot 3 no longer masks by key name. when-authorized shows them to users with a role. Treat always as a debugging switch you turn off again.
Actuator is also on a Kubernetes manifest
livenessProbe: { httpGet: { path: /actuator/health/liveness, port: 8081 } }
readinessProbe: { httpGet: { path: /actuator/health/readiness, port: 8081 } }
startupProbe: { httpGet: { path: /actuator/health/liveness, port: 8081 } }
annotations: { prometheus.io/path: /actuator/prometheus } # tells a monitoring tool where the metrics page is
The next lesson is about getting those probe endpoints right.
Later (Ch 27): Prometheus is the monitoring system that reads (scrapes) that page every few seconds.
What you can now do
- Say what Actuator is and which endpoints are exposed by default (only health).
- Expose endpoints with
management.endpoints.web.exposure.includeand read them with curl. - Take a thread dump or heap dump over HTTP, and keep the dangerous endpoints off public ports.