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.
This commit is contained in:
40
scripts/demo-custom-health.sh
Executable file
40
scripts/demo-custom-health.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# The three custom indicators, and what happens when the upstream goes down.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/07-custom-health-indicators.txt
|
||||
B=http://localhost:8080
|
||||
{
|
||||
echo "### profile: details (show-details: always, show-components: always)"
|
||||
echo
|
||||
echo "--- upstream UP ---"
|
||||
curl -s -o /dev/null -u ops:ops-password -X POST "$B/stub/upstream/mode?value=up"
|
||||
sleep 1
|
||||
echo "\$ curl -s -o /dev/null -w '%{http_code}' $B/actuator/health"
|
||||
curl -s -o /dev/null -w ' HTTP %{http_code}\n' -u ops:ops-password "$B/actuator/health"
|
||||
curl -s -u ops:ops-password "$B/actuator/health" | python3 -m json.tool
|
||||
echo
|
||||
echo "--- flip the upstream to DOWN, change nothing else ---"
|
||||
echo "\$ curl -s -X POST '$B/stub/upstream/mode?value=down'"
|
||||
curl -s -u ops:ops-password -X POST "$B/stub/upstream/mode?value=down"; echo
|
||||
sleep 1
|
||||
curl -s -o /dev/null -w ' HTTP %{http_code}\n' -u ops:ops-password "$B/actuator/health"
|
||||
curl -s -u ops:ops-password "$B/actuator/health" | python3 -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print(json.dumps({"status":d["status"],"externalApi":d["components"]["externalApi"]},indent=2))'
|
||||
echo
|
||||
echo " The aggregate went DOWN and /actuator/health now answers 503. If that URL is your"
|
||||
echo " Kubernetes readiness probe, every pod in the deployment has just left the load"
|
||||
echo " balancer because a third party had a bad minute."
|
||||
echo
|
||||
echo "--- a single component, addressed directly ---"
|
||||
echo "\$ curl -s $B/actuator/health/ordersDatabase"
|
||||
curl -s -u ops:ops-password "$B/actuator/health/ordersDatabase" | python3 -m json.tool
|
||||
echo "\$ curl -s $B/actuator/health/kafka"
|
||||
curl -s -u ops:ops-password "$B/actuator/health/kafka" | python3 -m json.tool
|
||||
echo
|
||||
echo "--- restore ---"
|
||||
curl -s -u ops:ops-password -X POST "$B/stub/upstream/mode?value=up"; echo
|
||||
} > "$OUT" 2>&1
|
||||
echo "wrote $OUT"
|
||||
22
scripts/demo-default-exposure.sh
Executable file
22
scripts/demo-default-exposure.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# What Actuator exposes when you add the starter and configure nothing.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/01-default-exposure.txt
|
||||
{
|
||||
echo "### Spring Boot Actuator, starter added, ZERO management.* configuration"
|
||||
echo
|
||||
echo "\$ curl -s -u ops:ops-password http://localhost:8080/actuator"
|
||||
curl -s -u ops:ops-password http://localhost:8080/actuator | python3 -m json.tool
|
||||
echo
|
||||
echo "\$ curl -s -o /dev/null -w '%{http_code}' -u ops:ops-password http://localhost:8080/actuator/env"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -u ops:ops-password http://localhost:8080/actuator/env
|
||||
echo " 404 = discovered but NOT exposed over HTTP. Exposure and existence are different things."
|
||||
echo
|
||||
echo "\$ curl -s -u ops:ops-password http://localhost:8080/actuator/health"
|
||||
curl -s -u ops:ops-password http://localhost:8080/actuator/health | python3 -m json.tool
|
||||
echo
|
||||
echo " Only 'health' is web-exposed by default. show-details defaults to 'never', so even an"
|
||||
echo " authenticated caller sees a bare status until you say otherwise."
|
||||
} > "$OUT" 2>&1
|
||||
echo "wrote $OUT"
|
||||
13
scripts/demo-endpoint-catalogue.sh
Executable file
13
scripts/demo-endpoint-catalogue.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# The authoritative endpoint list: what the RUNNING application publishes with exposure = "*".
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/02-endpoint-catalogue.txt
|
||||
{
|
||||
echo "### Every web-exposed endpoint, read from the running application"
|
||||
echo "### profiles: exposeall,open management.endpoints.web.exposure.include: \"*\""
|
||||
echo
|
||||
echo "\$ curl -s http://localhost:8080/actuator/diag"
|
||||
curl -s http://localhost:8080/actuator/diag | python3 -m json.tool
|
||||
} > "$OUT" 2>&1
|
||||
echo "wrote $OUT"
|
||||
36
scripts/demo-groups-probes.sh
Executable file
36
scripts/demo-groups-probes.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Health groups: keeping a dependency outage out of the liveness probe.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/08-groups-and-probes.txt
|
||||
B=http://localhost:8080/actuator/health
|
||||
{
|
||||
echo "### profile: groups"
|
||||
echo
|
||||
echo "--- baseline: upstream UP ---"
|
||||
for g in "" liveness readiness startup; do
|
||||
u="$B${g:+/$g}"
|
||||
printf ' %-40s HTTP %s\n' "GET ${u#http://localhost:8080}" "$(curl -s -o /dev/null -w '%{http_code}' -u ops:ops-password "$u")"
|
||||
done
|
||||
echo
|
||||
echo "--- upstream DOWN (a third party is having an outage) ---"
|
||||
curl -s -o /dev/null -u ops:ops-password -X POST "http://localhost:8080/stub/upstream/mode?value=down"
|
||||
sleep 1
|
||||
for g in "" liveness readiness startup; do
|
||||
u="$B${g:+/$g}"
|
||||
printf ' %-40s HTTP %s\n' "GET ${u#http://localhost:8080}" "$(curl -s -o /dev/null -w '%{http_code}' -u ops:ops-password "$u")"
|
||||
done
|
||||
echo
|
||||
echo " liveness stayed 200. readiness went 503."
|
||||
echo " Kubernetes takes this instance out of the Service and leaves the process alone."
|
||||
echo " Wire readiness to /actuator/health and you get a restart loop instead."
|
||||
echo
|
||||
echo "--- what each group contains ---"
|
||||
echo "\$ curl -s $B/liveness"
|
||||
curl -s -u ops:ops-password "$B/liveness" | python3 -m json.tool
|
||||
echo "\$ curl -s $B/readiness"
|
||||
curl -s -u ops:ops-password "$B/readiness" | python3 -m json.tool
|
||||
echo
|
||||
curl -s -o /dev/null -u ops:ops-password -X POST "http://localhost:8080/stub/upstream/mode?value=up"
|
||||
} > "$OUT" 2>&1
|
||||
echo "wrote $OUT"
|
||||
23
scripts/demo-heapdump-leak.sh
Executable file
23
scripts/demo-heapdump-leak.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# The endpoint that really does hand over your secrets - once you turn it on.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/04-heapdump-leak.txt
|
||||
B=http://localhost:8080/actuator
|
||||
{
|
||||
echo "### profiles: exposeall,open PLUS --management.endpoint.heapdump.access=unrestricted"
|
||||
echo
|
||||
echo "\$ curl -s -o /tmp/heap.hprof -w 'status=%{http_code} bytes=%{size_download} type=%{content_type}' $B/heapdump"
|
||||
curl -s -o /tmp/heap.hprof -w 'status=%{http_code} bytes=%{size_download} type=%{content_type}\n' "$B/heapdump"
|
||||
echo
|
||||
echo "\$ strings /tmp/heap.hprof | grep -c 'S3CRET-partner-credential'"
|
||||
strings /tmp/heap.hprof 2>/dev/null | grep -c 'S3CRET-partner-credential'
|
||||
echo "\$ strings /tmp/heap.hprof | grep -o 'not-a-real-password[^\"]*' | head -1"
|
||||
strings /tmp/heap.hprof 2>/dev/null | grep -o 'not-a-real-password[^\"]*' | head -1
|
||||
echo
|
||||
echo " /actuator/env masked both of these to ******."
|
||||
echo " /actuator/heapdump handed over the process memory that contains them in plaintext."
|
||||
echo " Sanitisation is a property-rendering feature. It is not a security boundary."
|
||||
rm -f /tmp/heap.hprof
|
||||
} > "$OUT" 2>&1
|
||||
echo "wrote $OUT"
|
||||
19
scripts/demo-kafka-timeout.sh
Executable file
19
scripts/demo-kafka-timeout.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# The single most useful number in this repository: how long a naive Kafka health check blocks.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/09-kafka-timeout.txt
|
||||
MODE="${1:-tuned}"
|
||||
B=http://localhost:8080/actuator/health/kafka
|
||||
{
|
||||
echo "### Kafka health check against an unreachable broker -- ${MODE}"
|
||||
echo
|
||||
echo "\$ time curl -s $B"
|
||||
s=$(date +%s.%N)
|
||||
body=$(curl -s -u ops:ops-password --max-time 180 "$B")
|
||||
e=$(date +%s.%N)
|
||||
echo "$body" | python3 -m json.tool 2>/dev/null || echo "$body"
|
||||
echo
|
||||
printf 'wall clock: %.1f s\n' "$(echo "$e - $s" | bc)"
|
||||
} > "$OUT.$MODE" 2>&1
|
||||
echo "wrote $OUT.$MODE"
|
||||
34
scripts/demo-management-port.sh
Executable file
34
scripts/demo-management-port.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Actuator on its own port and path.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/06-management-port.txt
|
||||
{
|
||||
echo "### profile: mgmtport"
|
||||
echo "### management.server.port: 9001 / management.server.address: 127.0.0.1 / base-path: /manage"
|
||||
echo
|
||||
echo "--- the application port no longer serves Actuator at all ---"
|
||||
printf ' %-52s %s\n' "GET :8080/actuator/health" "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/actuator/health)"
|
||||
printf ' %-52s %s\n' "GET :8080/manage/health" "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/manage/health)"
|
||||
printf ' %-52s %s\n' "GET :8080/orders/count" "$(curl -s -o /dev/null -w '%{http_code}' -u ops:ops-password http://127.0.0.1:8080/orders/count)"
|
||||
echo
|
||||
echo "--- the management port serves it on the new base path ---"
|
||||
printf ' %-52s %s\n' "GET :9001/manage/health" "$(curl -s -o /dev/null -w '%{http_code}' -u ops:ops-password http://127.0.0.1:9001/manage/health)"
|
||||
printf ' %-52s %s\n' "GET :9001/actuator/health" "$(curl -s -o /dev/null -w '%{http_code}' -u ops:ops-password http://127.0.0.1:9001/actuator/health)"
|
||||
printf ' %-52s %s\n' "GET :9001/orders/count" "$(curl -s -o /dev/null -w '%{http_code}' -u ops:ops-password http://127.0.0.1:9001/orders/count)"
|
||||
echo
|
||||
echo " Note the last line. The management context has its own DispatcherServlet and does NOT"
|
||||
echo " see application controllers. That is the isolation you are paying for."
|
||||
echo
|
||||
echo "--- what the management context reports about itself ---"
|
||||
echo "\$ curl -s -u ops:ops-password http://127.0.0.1:9001/manage/diag"
|
||||
curl -s -u ops:ops-password http://127.0.0.1:9001/manage/diag | python3 -m json.tool
|
||||
echo
|
||||
echo "--- listening sockets ---"
|
||||
echo "\$ ss -ltn | grep -E ':(8080|9001)'"
|
||||
ss -ltn 2>/dev/null | grep -E ':(8080|9001)' || netstat -ltn 2>/dev/null | grep -E ':(8080|9001)'
|
||||
echo
|
||||
echo " 9001 is bound to 127.0.0.1 only. 8080 is bound to *. An ingress that forwards to 8080"
|
||||
echo " cannot reach Actuator no matter how the security rules are written."
|
||||
} > "$OUT" 2>&1
|
||||
echo "wrote $OUT"
|
||||
53
scripts/demo-open-actuator.sh
Executable file
53
scripts/demo-open-actuator.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# What an unauthenticated caller actually gets when exposure is "*" and the chain permits all.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/03-open-actuator-leak.txt
|
||||
B=http://localhost:8080/actuator
|
||||
{
|
||||
echo "### profiles: exposeall,open -- NO credentials are sent on any request below"
|
||||
echo
|
||||
echo "--- 1. /actuator/env does NOT leak values in Spring Boot 4 ---"
|
||||
echo "\$ curl -s $B/env/spring.datasource.password | jq .property"
|
||||
curl -s "$B/env/spring.datasource.password" | python3 -c 'import json,sys;print(json.dumps(json.load(sys.stdin)["property"],indent=2))'
|
||||
echo "\$ curl -s $B/env/acme.partner.credential | jq .property"
|
||||
curl -s "$B/env/acme.partner.credential" | python3 -c 'import json,sys;print(json.dumps(json.load(sys.stdin)["property"],indent=2))'
|
||||
echo
|
||||
echo " Note the second one. 'acme.partner.credential' matches none of the classic"
|
||||
echo " password/secret/token key patterns, and it is still masked. Masking is driven by"
|
||||
echo " management.endpoint.env.show-values, which defaults to 'never' - not by key names."
|
||||
echo
|
||||
echo "--- 2. /actuator/heapdump is NOT exposed by 'include: \"*\"' ---"
|
||||
echo "\$ curl -s -o /dev/null -w '%{http_code}' $B/heapdump"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' "$B/heapdump"
|
||||
echo " management.endpoint.heapdump.access defaults to 'none'. So does shutdown."
|
||||
echo " They are the only two endpoints that do."
|
||||
echo
|
||||
echo "--- 3. /actuator/loggers: an unauthenticated WRITE ---"
|
||||
echo "\$ curl -s -X POST -d '{\"configuredLevel\":\"TRACE\"}' -H 'Content-Type: application/json' $B/loggers/org.springframework"
|
||||
curl -s -o /dev/null -w ' status=%{http_code}\n' -X POST -H 'Content-Type: application/json' \
|
||||
-d '{"configuredLevel":"TRACE"}' "$B/loggers/org.springframework"
|
||||
echo "\$ curl -s $B/loggers/org.springframework"
|
||||
curl -s "$B/loggers/org.springframework" | python3 -m json.tool
|
||||
curl -s -o /dev/null -X POST -H 'Content-Type: application/json' \
|
||||
-d '{"configuredLevel":null}' "$B/loggers/org.springframework"
|
||||
echo " (level reset). An attacker who can flip your root logger to TRACE has both a"
|
||||
echo " denial-of-service primitive and a way to get request bodies written to disk."
|
||||
echo
|
||||
echo "--- 4. /actuator/beans and /actuator/mappings: your whole application, described ---"
|
||||
echo "\$ curl -s $B/mappings | python3 -c 'count the URL patterns'"
|
||||
curl -s "$B/mappings" | python3 -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
n=0
|
||||
for ctx in d["contexts"].values():
|
||||
for m in ctx["mappings"].get("dispatcherServlets",{}).values():
|
||||
n+=len(m)
|
||||
print(f" {n} servlet mappings disclosed")'
|
||||
curl -s "$B/beans" | python3 -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
n=sum(len(c["beans"]) for c in d["contexts"].values())
|
||||
print(f" {n} beans disclosed, each with its type and dependencies")'
|
||||
} > "$OUT" 2>&1
|
||||
echo "wrote $OUT"
|
||||
45
scripts/demo-secured.sh
Executable file
45
scripts/demo-secured.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# The authorisation matrix produced by SecuredActuatorConfig.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/05-secured-matrix.txt
|
||||
B=http://localhost:8080/actuator
|
||||
probe() { # $1 label, $2 path, $3 curl auth args...
|
||||
local label="$1" path="$2"; shift 2
|
||||
printf ' %-46s %s\n' "$label" "$(curl -s -o /dev/null -w '%{http_code}' "$@" "$B/$path")"
|
||||
}
|
||||
{
|
||||
echo "### profile: secured (SecuredActuatorConfig + application-secured.yaml)"
|
||||
echo "### exposure is \"*\" - the security chain, not the exposure list, is what protects it"
|
||||
echo
|
||||
echo "ANONYMOUS"
|
||||
probe "GET /actuator/health" health
|
||||
probe "GET /actuator/info" info
|
||||
probe "GET /actuator/env" env
|
||||
probe "GET /actuator/beans" beans
|
||||
probe "GET /actuator/threaddump" threaddump
|
||||
probe "GET /actuator (the links index)" ""
|
||||
echo
|
||||
echo "AUTHENTICATED as ops (ROLE_ACTUATOR)"
|
||||
probe "GET /actuator/health" health -u ops:ops-password
|
||||
probe "GET /actuator/env" env -u ops:ops-password
|
||||
probe "GET /actuator/beans" beans -u ops:ops-password
|
||||
probe "GET /actuator/threaddump" threaddump -u ops:ops-password
|
||||
echo
|
||||
echo "WRONG PASSWORD"
|
||||
probe "GET /actuator/env" env -u ops:wrong
|
||||
echo
|
||||
echo "--- health body, anonymous (show-details: when-authorized) ---"
|
||||
curl -s "$B/health" | python3 -m json.tool
|
||||
echo
|
||||
echo "--- health body, authenticated as ROLE_ACTUATOR ---"
|
||||
curl -s -u ops:ops-password "$B/health" | python3 -m json.tool
|
||||
echo
|
||||
echo " Same endpoint, same status code, different body. An anonymous prober learns that the"
|
||||
echo " service is unhealthy but not WHICH dependency is unhealthy."
|
||||
echo
|
||||
echo "--- the business endpoint is untouched by the actuator chain ---"
|
||||
printf ' %-46s %s\n' "GET /orders/count (anonymous)" \
|
||||
"$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/orders/count)"
|
||||
} > "$OUT" 2>&1
|
||||
echo "wrote $OUT"
|
||||
24
scripts/demo-slow-indicator.sh
Executable file
24
scripts/demo-slow-indicator.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# A health indicator with no timeout, and what it does to the endpoint.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/10-slow-upstream.txt
|
||||
B=http://localhost:8080
|
||||
{
|
||||
echo "### profile: details upstream deliberately sleeping 30s per request"
|
||||
echo "### demo.upstream.timeout-ms = 750, so the indicator gives up long before the stub replies"
|
||||
echo
|
||||
curl -s -o /dev/null -u ops:ops-password -X POST "$B/stub/upstream/mode?value=slow"
|
||||
sleep 1
|
||||
echo "\$ time curl -s $B/actuator/health/externalApi"
|
||||
s=$(date +%s.%N)
|
||||
curl -s -u ops:ops-password --max-time 60 "$B/actuator/health/externalApi" | python3 -m json.tool
|
||||
e=$(date +%s.%N)
|
||||
printf 'wall clock: %.2f s\n' "$(echo "$e - $s" | bc)"
|
||||
echo
|
||||
echo " The read timeout is what bounds this, not the endpoint. Remove setReadTimeout from"
|
||||
echo " ExternalApiHealthIndicator and this call blocks for the full 30 seconds, holding a"
|
||||
echo " Tomcat worker the whole time."
|
||||
curl -s -o /dev/null -u ops:ops-password -X POST "$B/stub/upstream/mode?value=up"
|
||||
} > "$OUT" 2>&1
|
||||
echo "wrote $OUT"
|
||||
19
scripts/demo-versions.sh
Executable file
19
scripts/demo-versions.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Exactly what this repository was built and run against.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output/00-versions.txt
|
||||
{
|
||||
echo "\$ java -version"
|
||||
java -version 2>&1 | grep -viE 'JAVA_TOOL_OPTIONS|Picked up'
|
||||
echo
|
||||
echo "\$ mvn -v | head -3"
|
||||
mvn -v 2>&1 | grep -viE 'WARNING|sun\.misc|Picked up|Please consider' | head -3
|
||||
echo
|
||||
echo "\$ mvn dependency:list -- the resolved versions behind spring-boot-starter-parent 4.1.1"
|
||||
mvn -B dependency:list 2>/dev/null \
|
||||
| grep -oE '(org\.springframework[a-z.]*|io\.micrometer|org\.apache\.kafka|com\.h2database):[A-Za-z0-9.-]+:jar:[0-9][A-Za-z0-9.-]*' \
|
||||
| awk -F':' '{printf " %-52s %s\n", $1":"$2, $4}' | sort -u \
|
||||
| grep -E 'spring-boot-actuator |spring-boot-health |spring-boot |spring-core |spring-web |spring-security-core |spring-security-web |micrometer-core |micrometer-registry-prometheus |kafka-clients |h2 |spring-boot-restclient '
|
||||
} > "$OUT" 2>&1
|
||||
echo "wrote $OUT"
|
||||
8
scripts/env.sh
Executable file
8
scripts/env.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared environment. Point JAVA_HOME at a JDK 25 (or newer) installation.
|
||||
: "${JAVA_HOME:?set JAVA_HOME to a JDK 25+ installation}"
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
MVN="${MVN:-mvn}"
|
||||
APP_MAIN="com.ankurm.actuator.ActuatorProductionApplication"
|
||||
APP_PORT="${APP_PORT:-8080}"
|
||||
MGMT_PORT="${MGMT_PORT:-9001}"
|
||||
70
scripts/run-all.sh
Executable file
70
scripts/run-all.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerate every file in docs/output/ from scratch.
|
||||
#
|
||||
# JAVA_HOME=/path/to/jdk25 ./scripts/run-all.sh
|
||||
#
|
||||
# Each scenario starts a fresh JVM, runs its probes, and stops it. Timing figures vary between
|
||||
# machines; the status codes and bodies do not.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
: "${JAVA_HOME:?set JAVA_HOME to a JDK 25+ installation}"
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
JAR=target/actuator-production-1.0.0.jar
|
||||
[ -f "$JAR" ] || mvn -B -q -DskipTests package
|
||||
HEAP="${HEAP:--Xmx384m}"
|
||||
APP_PID=""
|
||||
|
||||
start() { # $1 profiles, rest: extra --args
|
||||
local profiles="$1"; shift
|
||||
java $HEAP -jar "$JAR" --spring.profiles.active="$profiles" "$@" > /tmp/actuator-demo.log 2>&1 &
|
||||
APP_PID=$!
|
||||
local port=8080 url="http://127.0.0.1:8080/orders/count"
|
||||
for _ in $(seq 1 90); do
|
||||
c=$(curl -s -o /dev/null -w '%{http_code}' -u ops:ops-password "$url" || true)
|
||||
[ -n "$c" ] && [ "$c" != "000" ] && return 0
|
||||
sleep 1
|
||||
done
|
||||
echo "FAILED TO START (profiles=$profiles)" >&2; tail -30 /tmp/actuator-demo.log >&2; return 1
|
||||
}
|
||||
stop() {
|
||||
[ -n "$APP_PID" ] && kill -9 "$APP_PID" 2>/dev/null
|
||||
wait "$APP_PID" 2>/dev/null
|
||||
APP_PID=""
|
||||
for _ in $(seq 1 40); do
|
||||
(exec 3<>/dev/tcp/127.0.0.1/8080) 2>/dev/null || break
|
||||
sleep 0.25
|
||||
done
|
||||
exec 3<&- 2>/dev/null || true
|
||||
}
|
||||
trap stop EXIT
|
||||
|
||||
mkdir -p docs/output
|
||||
|
||||
scripts/demo-versions.sh
|
||||
|
||||
start "" && { scripts/demo-default-exposure.sh; stop; }
|
||||
start "exposeall,open" && { scripts/demo-endpoint-catalogue.sh;
|
||||
scripts/demo-open-actuator.sh; stop; }
|
||||
start "exposeall,open" --management.endpoint.heapdump.access=unrestricted \
|
||||
&& { scripts/demo-heapdump-leak.sh; stop; }
|
||||
start "secured" && { scripts/demo-secured.sh; stop; }
|
||||
start "details" && { scripts/demo-custom-health.sh;
|
||||
scripts/demo-slow-indicator.sh;
|
||||
scripts/demo-kafka-timeout.sh tuned; stop; }
|
||||
start "details,kafkanaive" && { scripts/demo-kafka-timeout.sh naive; stop; }
|
||||
start "groups" && { scripts/demo-groups-probes.sh; stop; }
|
||||
|
||||
# The management-port scenario readies on a different URL, so it is started by hand.
|
||||
java $HEAP -jar "$JAR" --spring.profiles.active=mgmtport > /tmp/actuator-demo.log 2>&1 &
|
||||
APP_PID=$!
|
||||
for _ in $(seq 1 90); do
|
||||
c=$(curl -s -o /dev/null -w '%{http_code}' -u ops:ops-password http://127.0.0.1:9001/manage/health || true)
|
||||
[ -n "$c" ] && [ "$c" != "000" ] && break
|
||||
sleep 1
|
||||
done
|
||||
scripts/demo-management-port.sh
|
||||
stop
|
||||
|
||||
echo
|
||||
echo "docs/output/ regenerated:"
|
||||
ls -1 docs/output/
|
||||
26
scripts/run.sh
Executable file
26
scripts/run.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start the application with the given comma-separated profiles and block until it answers.
|
||||
# ./scripts/run.sh # defaults
|
||||
# ./scripts/run.sh exposeall,open # every endpoint, no authentication
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
PROFILES="${1:-}"
|
||||
LOG="${LOG:-/tmp/actuator-demo.log}"
|
||||
|
||||
scripts/stop.sh
|
||||
|
||||
ARGS=(-B -o org.springframework.boot:spring-boot-maven-plugin:run)
|
||||
[ -n "$PROFILES" ] && ARGS+=("-Dspring-boot.run.profiles=$PROFILES")
|
||||
|
||||
setsid nohup "$MVN" "${ARGS[@]}" > "$LOG" 2>&1 < /dev/null &
|
||||
|
||||
READY_URL="${READY_URL:-http://127.0.0.1:${APP_PORT}/orders/count}"
|
||||
for _ in $(seq 1 90); do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -u ops:ops-password "$READY_URL" || true)
|
||||
[ "$code" != "000" ] && [ -n "$code" ] && exit 0
|
||||
sleep 2
|
||||
done
|
||||
echo "application did not start; tail of $LOG:" >&2
|
||||
tail -40 "$LOG" >&2
|
||||
exit 1
|
||||
14
scripts/stop.sh
Executable file
14
scripts/stop.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Kill by main class, never by a pattern that could match this script's own command line.
|
||||
# `pkill -f spring-boot` matches the shell running it and takes the terminal with it.
|
||||
set -u
|
||||
for p in $(ps -eo pid,args | grep '[A]ctuatorProductionApplication' | awk '{print $1}'); do
|
||||
kill -9 "$p" 2>/dev/null || true
|
||||
done
|
||||
# Wait for the port to actually close. Killing the PID is not the same as the socket being
|
||||
# free, and a stale listener looks exactly like your configuration change having no effect.
|
||||
for _ in $(seq 1 40); do
|
||||
if ! (exec 3<>/dev/tcp/127.0.0.1/"${APP_PORT:-8080}") 2>/dev/null; then break; fi
|
||||
sleep 0.25
|
||||
done
|
||||
exec 3<&- 2>/dev/null || true
|
||||
Reference in New Issue
Block a user