Durch die im Dezember 2024 auf dem 38. Chaos Communication Congress, der Konferenz des Chaos Computer Club e. V., gehaltene Präsentation „Wir wissen, wo dein Auto steht – Volksdaten von Volkswagen” ist das Actuator-Modul von Spring Boot ins Rampenlicht gerückt worden. Obwohl das Modul in der Regel still und heimlich seine Arbeit verrichtet, spielte es dort als Einfallstor eine Hauptrolle.
Dass der Einsatz von Acutator nicht per se zu einer Sicherheitslücke führt, hat bereits Gerrit Meier in seinem englischen Post „Spring Actuator Security” gezeigt. Und doch hat mir der ganze Wirbel zu dem Thema gezeigt, dass das Modul Actuator relativ ungekannt ist. Genau deswegen wollen wir es im Folgenden anschauen.
Natürlich stellt sich die Frage, was kann ich hier mehr erzählen als die offizielle Dokumentation? Die ehrliche Antwort ist: „Vermutlich nichts.” Ich ermuntere jeden dazu, im Zweifel stets in die offizielle Dokumentation zu schauen, allerdings gibt es Menschen, die davon profitieren, das hier noch einmal auf Deutsch und in anderen Worten beziehungsweise als zusammenhängenden Text zu lesen. Sollten Sie nicht zu diesen gehören, können Sie hier aufhören zu lesen und sollten bei Interesse oder Bedarf zu Acutator in die Dokumentation schauen.
Production-ready
Actuator bündelt unter dem Motto „production-ready” im Actuator-Modul vor allem Features, die den Betrieb der Anwendung unterstützen. Dabei ermöglichen es uns die meisten Features, Informationen aus einer laufenden Anwendung von außen auszulesen. Vereinzelt ist es allerdings auch möglich, Aktionen innerhalb der Anwendung von außen anzustoßen. Diese Features lassen sich dabei grob in zwei größere Bereiche unterteilen.
Der erste Bereich sind Endpoints. Das ist die von Actuator gewählte Abstraktion, um Informationen und Aktionen, unabhängig vom eigentlichen Transportweg, zur Verfügung zu stellen. In einen zweiten Bereich lassen sich alle Features, die sich um das Thema Observability drehen, zusammenfassen.
Hier schauen wir uns, vor allem aus Platzgründen, ausschließlich den ersten Bereich, Endpoints, an.
Aktivierung
Wie für die meisten Module aus Spring Boot üblich, besteht der erste und wichtigste Schritt darin, den passenden Starter spring-boot-starter-actuator
im Projekt als Abhängigkeit zu definieren. Starten wir nun unsere Anwendung, stehen uns die Features bereits zur Verfügung.
Neben der reinen Aktivierung müssen und sollten wir uns nun noch um den Zugriff auf Actuator kümmern. Hierzu besteht der erste Schritt daraus zu entscheiden, ob wir über HTTP oder JMX mit Actuator kommunizieren möchten. Handelt es sich bei unserer Anwendung um eine Web-Anwendung, wird der Weg über HTTP automatisch aktiviert und Actuator ist über den HTTP-Port der Anwendung unter dem Pfad /actuator, siehe Listing 1, erreichbar.
$ 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
}
}
}
Neben dem Pfad, über das Property management.endpoints.web.base-path
, ist es auch möglich, dafür zu sorgen, dass Actuator in diesem Fall auf einem anderen Port als die eigene Anwendung lauscht. Hierzu setzen wir management.server.port
auf den gewünschten Port. Dabei können wir mittels management.server.address
auch noch das Netzwerkinterface beziehungsweise localhost, auswählen, um den möglichen Zugriff bereits auf Netzwerkebene einzuschränken.
Möchten wir nicht per HTTP zugreifen, lässt sich der Zugriff durch das Setzen von management.server.port
auf den Wert -1 ganz ausschalten. Dies ist dann sinnvoll, wenn wir keine Endpoints verwenden oder den Zugriff komplett über JMX, die Java Mangagement Extensions, erledigen. Hierfür müssen wir, da es nicht standardmäßig aktiviert ist, das Property spring.jmx.enabled
auf true setzen. Anschließend können wir, beispielsweise über die jconsole, per JMX auf die von Acutator bereitgestellten MBeans zugreifen, siehe Abbildung 1.
Im Weiteren nutzen wir den Weg über HTTP, um uns den Bereich der Endpoints anzuschauen.
Endpoints
Wie bereits gesagt, ermöglichen Endpoints vor allem einen lesenden Zugriff auf Informationen innerhalb der laufenden Anwendung. Spring Boot bringt dabei von Haus aus bereits bis zu 25 Endpoints mit. Die aktivierten, beziehungsweise genau genommen die exponierten, Endpoints werden dabei unter /actuator gelistet. In Listing 1 konnten wir also sehen, dass standardmäßig nur der health-Endpoint abrufbar ist.
Um weitere Endpoints von außen erreichbar zu machen, nutzen wir das Property management.endpoints.web.exposure.include
. Dieses erhält eine Liste von Endpoint-IDs oder den Stern, um alle Endpoints zu aktiveren. Nutzen wir den *, können wir zudem durch management.endpoints.web.exposure.exclude
einzelne Endpoints deaktivieren. Ich persönlich würde aber, vor allem aus Sicherheitsgründen, nur die erste Option nutzen und dabei nur die wirklich benötigten Endpoints aktivieren. Für diesen Artikel aktiveren wir jedoch alle in dieser Anwendung verfügbaren Endpoints. Ein erneuter Aufruf von /actuator, siehe Listing 2, liefert nun deutlich mehr Einträge.
$ 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
}
}
}
Sämtliche Endpoints lassen sich mit einer Time-to-Live (TTL) versehen. Diese sorgt dafür, dass nur alle x Zeiteinheiten die eigentliche Logik durchlaufen und in der Zwischenzeit bei erneuten Aufrufen das letzte Ergebnis zurückgeliefert wird. Das sorgt dafür, dass unnötige Arbeit vermieden und das gesamte System somit entlastet wird. Wollen wir dieses Feature für einen Endpoint aktivieren, setzten wir das Property management.endpoint.<EndpointID>.cache.time-to-live
auf eine gewünschte Zeiteinheit.
Im Folgenden schauen wir uns nun ein paar der mitgelieferten Endpoints im Detail an.
health-Endpoint
Einer der zentralen Endpoints ist der health-Endpoint. Dieser gibt Auskunft darüber, ob der Zustand der Anwendung in Ordnung ist oder nicht. Hierzu können wir /actuator/health, siehe Listing 3, aufrufen.
$ 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"
}
Der dabei zurückgelieferte Status ist eine Aggregation von mehreren Prüfungen. Möchten wir auch deren Ergebnisse sehen, können wir über management.endpoint.health.show-details=always
dafür sorgen, dass nicht nur das Gesamtergebnis, sondern auch die einzelnen Prüfungen, sogar mit weiteren Details, angezeigt werden, siehe 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"
}
Actuator bringt neben den drei hier zu sehenden Prüfungen, diskspace, ping und ssl, eine Reihe weiterer mit, die allerdings nur aktiviert werden, wenn sich die Prüfung auch durchführen lässt. So gibt es beispielsweise eine Prüfung db, die prüft, ob sich die Verbindung zur konfigurierten Datenbank aufbauen lässt. Diese wird aktiviert, sobald erkannt wird, dass eine Verbindung konfiguriert wurde.
Natürlich lassen sich auch eigene Prüfungen schreiben. Hierzu implementieren wir, siehe Listing 5, das 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();
}
}
}
Rufen wir nun den health-Endpoint erneut auf und erwischen einen ungeraden Zeitpunkt, können wir sehen, dass unsere Prüfung mit durchgeführt wurde, und auch, dass nun der Gesamtstatus von up auf down gewechselt ist, siehe 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"
}
Hierzu nutzt Actuator eine konfigurierte Implementierung des Interface StatusAggregator
. Diese aggregiert die Statuswerte der Unterprüfungen zu einem Gesamtergebnis. Gefällt uns die Aggregation nicht, können wir entweder die Standardimplementierung über das Property management.endpoint.health.status.order
konfigurieren oder eine eigene Implementierung von StatusAggregator
bereitstellen.
In Listing 6 ist auch zu sehen, dass der HTTP-Statuscode nicht mehr 200 OK, sondern nun 503 Service Unavailable ist. Hierzu gibt es ein Mapping von Status- auf Statuscodewerte. Standardmäßig wird dabei mit 200 OK geantwortet, außer der Health-Status ist DOWN oder OUT_OF_SERVICE, dann wird eben mit 503 Service Unavailable geantwortet. Passt uns dieses Mapping nicht, oder wir möchten für einen anderen Status ebenfalls nicht mit 200 OK antworten, lässt sich auch dies über Konfiguration lösen. Hierzu können wir mit management.endpoint.health.status.http-mapping.<STATUS>=<StatusCode>
das Mapping definieren. Dabei ist zu beachten, dass sobald wir das tun, der Standard nicht mehr greift und wir somit auch DOWN und OUT_OF_SERVICE selbst wieder auf 503 setzen müssen.
Neben der aggregierten Sicht unter /actuator/health können wir auch, wenn wir Details sehen dürfen, einzelne Prüfungen, siehe Listing 7, aufrufen.
$ 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"
}
Außerdem lassen sich auch mehrere Prüfungen in Gruppen zusammenfassen. Hierzu wird das Property management.endpoint.health.group.<GROUP_NAME>.include
auf eine Liste von Prüfungen gesetzt. Eine solche Gruppe können wir anschließend, siehe Listing 8, wie eine Einzelprüfung aufrufen.
$ 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"
}
Im Gegensatz zur Einzelprüfung funktioniert dieser Aufruf allerdings auch, wenn die Details nicht angezeigt werden sollen.
Eine Besonderheit des health-Endpoints ist zudem die dedizierte Unterstützung für den Betrieb in einem Kubernetes-Cluster. Dort lassen sich für Container verschiedene Probes einrichten, darunter die Liveness- und Readiness-Probes. Diese werden dazu verwendet, den Container bei Bedarf neu zu starten (Liveness) beziehungsweise festzustellen, ob der Container Netzwerkverkehr erhalten soll (Readiness).
Um diese ideal zu unterstützen, bringt das Modul zwei speziell hierfür nutzbare Prüfungen für den health-Endpoint mit. Diese sind automatisch aktiviert, wenn Spring Boot erkennt, dass unsere Anwendung in einem Cluster läuft. Alternativ können wir diese über das Property management.endpoint.health.probes.enabled
auch manuell aktivieren. Anschließend sind zwei neue Prüfungen vorhanden, siehe 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 nutzt hierfür das von Spring Boot bereitgestellte Konzept von Application Availability. Da diese Prüfungen intern als Gruppen registriert sind, ist es möglich, neben der Application Availability zusätzliche Prüfungen zu konfigurieren. Hierbei sollten wir allerdings beachten, welchen Einfluss dies auf das auswertende System hat. Würden wir beispielsweise die Liveness-Prüfung um die Prüfung erweitern, ob die Datenbank erreichbar ist, würde im Falle der Nichterreichbarkeit Kubernetes unsere Anwendung einfach neu starten. Ein Neustart hat hier aber oft keinen Nutzen, da dadurch die Datenbank nicht erreichbar wird.
Sollte Actuator auf einem eigenen Port betrieben werden, stellen diese beiden Prüfungen außerdem nicht sicher, ob die eigentliche Anwendung noch erreichbar ist. In diesem Fall kann es sinnvoll sein, das Property management.endpoint.health.probes.add-additional-paths
zu setzen. Anschließend sind die beiden Gruppen auch unter /livez und /readyz über den regulären HTTP-Port der Anwendung erreichbar.
info-Endpoint
Der zweite Endpoint, den wir uns anschauen wollen, ist der info-Endpoint. Über diesen lassen sich beliebige Werte, die uns relevant erscheinen, zur Laufzeit abfragen. Standardmäßig liefert dieser Endpunkt unter /actuator/info ein leeres Objekt, siehe 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
{}
Wir müssen also noch mehr konfigurieren, um hier etwas sehen zu können. Wie auch der health-Endpoint besteht der info-Endpoint aus einer Menge von Unterkomponenten, die InfoContributor
implementieren, deren Summe angezeigt wird.
Actuator bringt bereits standardmäßig die Contributors build, env, git, java, os, process und ssl mit. Von diesen sind jedoch nur build und git auch standardmäßig aktiviert. Da beide jedoch zusätzlich noch eine Datei benötigen, die während des Builds erzeugt wird, sehen wir standardmäßig keine Werte.
Um das Maximum an eingebauten Werten zu sehen, erweitern wir den Build unseres Projekts um die in Listing 11 zu sehenden Dinge und aktivieren in der application.properties der Anwendung, siehe Listing 12, die restlichen Contributors.
…
<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
…
Wenn wir die Anwendung nun über INFO_START_DATE=$(date) mvn spring-boot:run
starten und den info-Endpoint erneut aufrufen, erhalten wir das Ergebnis 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"
}
}
In diesem können wir gut sehen, welcher Contributor welche Informationen hinzugefügt hat. Eine Besonderheit ist dabei der env-Contributor, dieser fügt alle Properties, die mit info beginnen, hinzu. In diesem Beispiel ist das sowohl der Eintrag info.greeting
aus der application.properties als auch die Umgebungsvariable INFO_START_DATE
, die wir beim Starten gesetzt haben.
Reichen uns diese Informationen immer noch nicht, können wir zusätzlich noch mehr ausgeben. Zum einen können wir über management.info.git.mode=full
auch noch die restlichen in der Datei git.properties vorhandenen Einträge mit ausgeben lassen und zum anderen können wir durch die Bereitstellung von eigenen Contributors, siehe Listing 14, beliebige weitere Einträge erzeugen.
@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
Auf den ersten Blick scheint sich der env-Endpoint mit dem env-Contributor zu überschneiden. Rufen wir diesen jedoch auf, siehe Listing 15, sehen wir schnell, dass dies nicht der Fall ist.
$ 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": "******"
},
…
}
},
…
]
}
Neben den genutzten Profilen werden hier nämlich sämtliche Konfigurationswerte ausgegeben, die sich potenziell innerhalb der Anwendung nutzen ließen. Diese werden dabei nach ihrer Quelle, wie beispielsweise Umgebungsvariablen, gruppiert, und bei einigen Quellen erhalten wir sogar noch detailliertere Informationen wie die genaue Datei und die Zeilen- und Spaltennummer innerhalb dieser.
Wir sehen allerdings auch, dass aus Sicherheitsgründen sämtliche Werte maskiert wurden. Das geschieht aus Sicherheitsgründen, da dieser Endpunkt ansonsten möglicherweise auch sensitive Daten wie Credentials ausgeben könnte. Um nun aber die wirklichen Werte sehen zu können, müssen wir das Property management.endpoint.env.show-values
auf always oder when-authorized setzen. Rufen wir nun den Endpoint erneut auf, siehe Listing 16, sehen wir auch die wirklichen Werte.
$ 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"
},
...
},
...
]
}
Alternativ können wir auch eigene Implementierungen von SanitizingFunction
als Beans bereitstellen, um nur bestimmte Werte zu maskieren, siehe 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;
};
}
}
Dabei ist allerdings zu beachten, dass diese Funktion dann auch für die Werte des configprops- und quartz-Endpoints ausgeführt werden.
beans- und conditions-Endpoint
Zwei weitere Endpoints, die mir während der Entwicklung schon häufiger geholfen haben, sind beans und conditions. Ersterer listet, wie bereits am Namen zu erahnen, alle in der Anwendung vorhandenen Spring Beans, siehe Listing 18, auf.
$ 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"
]
},
...
}
}
}
}
Neben dem Namen der Bean erhalten wir allerdings noch weitere Informationen, wie der Ort, an dem diese deklariert wurde, oder deren Abhängigkeiten.
Ähnlich verhält es sich beim conditions-Endpoint, siehe 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",
...
]
}
}
}
Dieser listet sämtliche in der Anwendung vorhandenen Autokonfigurationen auf und gibt jeweils an, ob diese beim Start aktiviert wurden und somit Auswirkungen haben. Dabei wird für jede Konfiguration auch genau aufgelistet, warum es zu diesem Ergebnis kam.
Beide Endpoints können, vor allem während der Entwicklung, sehr hilfreich sein, um beim Debuggen zu helfen, wenn unerwartete Dinge passieren, die darauf zurückzuführen sind, dass erwartete Beans nicht vorhanden sind oder eben zu viel da sind.
Eigene Endpoints schreiben
Sollten trotz der Menge an bereits vorhandenen Endpoints noch weitere Wünsche offen sein, lassen sich auch eigene schreiben. Hierzu müssen wir unsere Bean mit @Endpoint
annotieren und können dann mit den Annotationen @ReadOperation
, @WriteOperation
oder @DeleteOperation
einzelne Methoden annotieren, um diese als Endpoint zur Verfügung zu stellen, siehe Listing 20.
@Component
@Endpoint(id = "answer")
public class CustomEndpoint {
@ReadOperation
public int answer() {
return 42;
}
}
Dieser Endpoint ist dabei automatisch sowohl über HTTP als auch JMX nutzbar. Alternativ könnten wir auch @WebEndpoint
oder @JmxEndpoint
als Annotation nutzen, um den Endpoint nur über einen der beiden Wege erreichbar zu machen. Benötigen wir für einen generischen @Endpoint
noch spezifische Logik, um die Repräsentation über HTTP oder JMX anzupassen, können wir auch noch eine @EndpointWebExtension
, siehe Listing 21, oder @EndpointJmxExtension
hinzufügen.
@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()));
}
}
Rufen wir nun unseren eigenen Endpoint über /actuator/answer auf, erhalten wir die Antwort aus 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"
}