Files
spring-boot-demo/README.md
Ankur Mhatre 4b6cefa60a Spring Boot 4 Actuator in production: endpoints, security, custom health indicators
Companion repository for the ankurm.com article. Every transcript in docs/output/
was produced by running this project; scripts/run-all.sh regenerates all of them.

Verified against Spring Boot 4.1.1 / Framework 7.0.9 / Security 7.1.1 /
Micrometer 1.17.1 / kafka-clients 4.2.1 on Temurin JDK 25.0.4.1+1.
2026-09-04 10:40:04 +05:30

170 lines
7.5 KiB
Markdown
Executable File

# spring-boot-demo — Actuator in production
Companion repository for **[Spring Boot Actuator in Production](https://ankurm.com/spring-boot-actuator-production-endpoints-security-health-indicators/)** on ankurm.com.
Every status code, JSON body, byte count and timing figure in the article was produced by
running this project. The transcripts live in [`docs/output/`](docs/output) and are regenerated
by a single command.
---
## Versions
Verified with `mvn dependency:list` on the machine that produced `docs/output/` — see
[`docs/output/00-versions.txt`](docs/output/00-versions.txt).
| Component | Version | Notes |
|---|---|---|
| Spring Boot | 4.1.1 | GA 20 August 2026 |
| Spring Framework | 7.0.9 | via `spring-boot-starter-parent` |
| Spring Security | 7.1.1 | via `spring-boot-starter-parent` |
| `spring-boot-actuator` | 4.1.1 | |
| `spring-boot-health` | 4.1.1 | **new module in Boot 4**`HealthIndicator` lives here now |
| `spring-boot-restclient` | 4.1.1 | **not pulled in by the web starter** |
| Micrometer | 1.17.1 | there is no Micrometer 2.x GA; see [`docs/02`](docs/02-boot-4-changes.md) |
| `kafka-clients` | 4.2.1 | what Boot 4.1.1 manages (4.3.1 is the latest on Central) |
| H2 | 2.4.240 | |
| JDK | Temurin 25.0.4.1+1 LTS | |
| Maven | 3.9.11 | |
---
## Quickstart
```bash
export JAVA_HOME=/path/to/jdk-25
mvn -DskipTests package
# defaults: only /actuator/health is exposed
java -jar target/actuator-production-1.0.0.jar
# every endpoint, no authentication - the configuration you should never ship
java -jar target/actuator-production-1.0.0.jar --spring.profiles.active=exposeall,open
# the configuration you should ship
java -jar target/actuator-production-1.0.0.jar --spring.profiles.active=secured
```
Credentials for every profile that requires them: **`ops` / `ops-password`**.
Regenerate every transcript in `docs/output/`:
```bash
JAVA_HOME=/path/to/jdk-25 ./scripts/run-all.sh
```
That takes roughly three minutes, most of which is the naive Kafka scenario blocking for its
full 60 seconds. That is the point of it.
---
## Profiles
| Profile | What it demonstrates |
|---|---|
| *(none)* | Boot defaults. Only `health` on the web, `show-details: never` |
| `exposeall` | `management.endpoints.web.exposure.include: "*"` |
| `open` | A `permitAll` security chain — the misconfiguration, kept on purpose |
| `secured` | `EndpointRequest.toAnyEndpoint()` + `ROLE_ACTUATOR` + `when-authorized` details |
| `mgmtport` | Actuator on port 9001, base path `/manage`, bound to loopback |
| `details` | `show-details: always` — the full component breakdown |
| `groups` | `liveness` / `readiness` / `startup` groups wired correctly |
| `kafkanaive` | The textbook `AdminClient` health check, so its 60-second block can be timed |
Profiles compose: `--spring.profiles.active=exposeall,open`.
---
## Endpoints
Read from the running application, not from the documentation — see
[`docs/output/02-endpoint-catalogue.txt`](docs/output/02-endpoint-catalogue.txt).
| Endpoint | Web-exposed by default | `access` default | Notes |
|---|---|---|---|
| `health` | **yes** | `unrestricted` | the only one exposed out of the box |
| `info` | no | `unrestricted` | Boot 4.1 added `process.*` fields |
| `beans` | no | `unrestricted` | 426 beans in this app, with types and wiring |
| `conditions` | no | `unrestricted` | the auto-configuration report |
| `configprops` | no | `unrestricted` | values masked like `env` |
| `env` | no | `unrestricted` | **masks every value** unless `show-values` says otherwise |
| `loggers` | no | `unrestricted` | has a `POST` — a write endpoint |
| `mappings` | no | `unrestricted` | every URL your app serves |
| `metrics` | no | `unrestricted` | from `spring-boot-micrometer-metrics` |
| `prometheus` | no | `unrestricted` | needs `micrometer-registry-prometheus` |
| `sbom` | no | `unrestricted` | |
| `scheduledtasks` | no | `unrestricted` | |
| `threaddump` | no | `unrestricted` | two operations: JSON and `text/plain` |
| `heapdump` | no | **`none`** | `include: "*"` is **not** enough |
| `shutdown` | no | **`none`** | `include: "*"` is **not** enough |
| `startup` | no | `unrestricted` | needs a `BufferingApplicationStartup` |
| `httpexchanges` | no | `unrestricted` | needs an `HttpExchangeRepository` bean |
| `auditevents` | no | `unrestricted` | needs an `AuditEventRepository` bean |
| `logfile` | no | `unrestricted` | needs `logging.file.name` |
| `caches`, `flyway`, `liquibase`, `quartz`, `sessions`, `integrationgraph` | no | `unrestricted` | conditional on the relevant module |
| `diag` | no | `unrestricted` | **this repository's own** — delete before shipping |
`heapdump` and `shutdown` are the only two endpoints whose `access` defaults to `none`. That
list came from Spring Boot's own `spring-configuration-metadata.json`, not from a blog.
---
## Documentation
| # | Chapter |
|---|---|
| 01 | [What Actuator actually exposes](docs/01-what-actuator-exposes.md) |
| 02 | [What changed in Spring Boot 4](docs/02-boot-4-changes.md) |
| 03 | [The endpoint catalogue](docs/03-endpoint-catalogue.md) |
| 04 | [Securing Actuator](docs/04-securing-actuator.md) |
| 05 | [Custom health indicators](docs/05-custom-health-indicators.md) |
| 06 | [Health indicator failure modes](docs/06-health-indicator-failure-modes.md) |
| 07 | [Groups, probes and Kubernetes](docs/07-groups-and-probes.md) |
| 08 | [The diagnostics endpoint](docs/08-diagnostics.md) |
| 09 | [Testing Actuator](docs/09-testing-actuator.md) |
---
## Captured output
| File | Scenario |
|---|---|
| [`00-versions.txt`](docs/output/00-versions.txt) | resolved dependency versions |
| [`01-default-exposure.txt`](docs/output/01-default-exposure.txt) | Actuator with zero configuration |
| [`02-endpoint-catalogue.txt`](docs/output/02-endpoint-catalogue.txt) | every exposed endpoint, from the running app |
| [`03-open-actuator-leak.txt`](docs/output/03-open-actuator-leak.txt) | what an anonymous caller really gets |
| [`04-heapdump-leak.txt`](docs/output/04-heapdump-leak.txt) | 59 MB, plaintext credentials inside |
| [`05-secured-matrix.txt`](docs/output/05-secured-matrix.txt) | the full authorisation matrix |
| [`06-management-port.txt`](docs/output/06-management-port.txt) | port 9001, and what it isolates |
| [`07-custom-health-indicators.txt`](docs/output/07-custom-health-indicators.txt) | DB, Kafka and external API |
| [`08-groups-and-probes.txt`](docs/output/08-groups-and-probes.txt) | liveness 200 while readiness 503 |
| [`09-kafka-timeout.txt.tuned`](docs/output/09-kafka-timeout.txt.tuned) | 1.6 s |
| [`09-kafka-timeout.txt.naive`](docs/output/09-kafka-timeout.txt.naive) | **60.2 s** |
| [`10-slow-upstream.txt`](docs/output/10-slow-upstream.txt) | a read timeout doing its job |
Status codes and bodies are reproducible. Timing figures are indicative and drift between
machines — except the 60-second one, which is a Kafka default and lands on 60.0 s every time.
---
## Layout
```
pom.xml
scripts/
run-all.sh regenerate everything below docs/output/
run.sh / stop.sh start and stop with given profiles
demo-*.sh one script per captured scenario
src/main/java/com/ankurm/actuator/
health/ OrdersDatabase, Kafka, ExternalApi indicators + UpstreamState
config/ Secured, Open and baseline security chains
web/ DiagnosticsEndpoint, stub upstream, business controller
src/test/java/ contract tests for the surprising behaviour
docs/ numbered chapters
docs/output/ captured real output
```
## Licence
MIT — see [LICENSE](LICENSE).