fix(obs): wire Prometheus endpoint for Spring Boot 4.0
Four Spring Boot 4.0-specific issues prevented /actuator/prometheus from working: 1. spring-boot-starter-micrometer-metrics missing — Spring Boot 4.0 splits Micrometer metrics export (including the Prometheus scrape endpoint) out of spring-boot-starter-actuator into its own starter. Added dependency. 2. management.prometheus.metrics.export.enabled not set — Spring Boot 4.0 defaults metrics export to false (opt-in). Added the property to application.yaml. 3. SecurityConfig did not permit /actuator/prometheus — Spring Boot 4.0 with Jetty serves the management port (8081) via the same security filter chain as the main port (8080). The previous commit's exclusion of ManagementWebSecurityAutoConfiguration was a no-op (that class no longer exists in Spring Boot 4.0); removed it and added the correct permitAll() rule. Updated the architecture comment in application.yaml to reflect the true filter-chain behaviour. 4. Reverted invalid FamilienarchivApplication.java change from the prior commit (ManagementWebSecurityAutoConfiguration import compiled against a class that does not exist in the Spring Boot 4.0 BOM). Also adds ActuatorPrometheusIT — an integration test that asserts the /actuator/prometheus endpoint returns 200 with jvm_memory_used_bytes without credentials, serving as regression protection against future Spring Boot upgrades silently breaking metrics collection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,11 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Spring Boot 4.0 splits Micrometer metrics export (incl. Prometheus scrape endpoint) into its own starter -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-micrometer-metrics</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package org.raddatz.familienarchiv;
|
package org.raddatz.familienarchiv;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
// Excluded: management port (8081) is network-isolated inside archiv-net; no app-level auth needed.
|
@SpringBootApplication
|
||||||
@SpringBootApplication(exclude = {ManagementWebSecurityAutoConfiguration.class})
|
|
||||||
public class FamilienarchivApplication {
|
public class FamilienarchivApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -54,8 +54,14 @@ public class SecurityConfig {
|
|||||||
.csrf(csrf -> csrf.disable())
|
.csrf(csrf -> csrf.disable())
|
||||||
|
|
||||||
.authorizeHttpRequests(auth -> {
|
.authorizeHttpRequests(auth -> {
|
||||||
// Health endpoint must be open so CI/Docker health checks work without credentials
|
// Both /actuator/health and /actuator/prometheus must be open.
|
||||||
auth.requestMatchers("/actuator/health").permitAll();
|
// In Spring Boot 4.0 the management server (port 8081) shares the security filter chain;
|
||||||
|
// network isolation (port 8081 not published in docker-compose) is the security boundary.
|
||||||
|
// Health and Prometheus must be open — no credentials for Docker health checks or Prometheus scraping.
|
||||||
|
// Note: in Spring Boot 4.0 the management port shares the security filter chain,
|
||||||
|
// so these paths must be explicitly permitted here even though they are served on port 8081.
|
||||||
|
// Network isolation (port 8081 not published in docker-compose) is the outer security boundary.
|
||||||
|
auth.requestMatchers("/actuator/health", "/actuator/prometheus").permitAll();
|
||||||
// Password reset endpoints are unauthenticated by nature
|
// Password reset endpoints are unauthenticated by nature
|
||||||
auth.requestMatchers("/api/auth/forgot-password", "/api/auth/reset-password").permitAll();
|
auth.requestMatchers("/api/auth/forgot-password", "/api/auth/reset-password").permitAll();
|
||||||
// Invite-based registration endpoints are public
|
// Invite-based registration endpoints are public
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ management:
|
|||||||
# Management port is separate from the app port so that:
|
# Management port is separate from the app port so that:
|
||||||
# (a) Caddy never proxies /actuator/* (it only routes :8080 → the app port)
|
# (a) Caddy never proxies /actuator/* (it only routes :8080 → the app port)
|
||||||
# (b) Prometheus scrapes backend:8081 directly inside archiv-net, not via Caddy
|
# (b) Prometheus scrapes backend:8081 directly inside archiv-net, not via Caddy
|
||||||
# (c) Spring Security's session-authenticated filter chain on :8080 never sees actuator requests
|
# Note: in Spring Boot 4.0 the management port shares the security filter chain; /actuator/health
|
||||||
|
# and /actuator/prometheus must be explicitly permitted in SecurityConfig — see SecurityConfig.java.
|
||||||
port: 8081
|
port: 8081
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
@@ -58,6 +59,11 @@ management:
|
|||||||
endpoint:
|
endpoint:
|
||||||
prometheus:
|
prometheus:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
# Spring Boot 4.0: metrics export is disabled by default — explicitly opt in for Prometheus
|
||||||
|
prometheus:
|
||||||
|
metrics:
|
||||||
|
export:
|
||||||
|
enabled: true
|
||||||
health:
|
health:
|
||||||
mail:
|
mail:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package org.raddatz.familienarchiv;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.server.LocalManagementPort;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import org.springframework.web.client.DefaultResponseErrorHandler;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@Import(PostgresContainerConfig.class)
|
||||||
|
class ActuatorPrometheusIT {
|
||||||
|
|
||||||
|
@LocalManagementPort
|
||||||
|
private int managementPort;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
S3Client s3Client;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void prometheus_endpoint_returns_jvm_metrics_without_credentials() {
|
||||||
|
ResponseEntity<String> response = noThrowTemplate().getForEntity(
|
||||||
|
"http://localhost:" + managementPort + "/actuator/prometheus", String.class);
|
||||||
|
|
||||||
|
assertThat(response.getStatusCode().value()).isEqualTo(200);
|
||||||
|
assertThat(response.getBody()).contains("jvm_memory_used_bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
private RestTemplate noThrowTemplate() {
|
||||||
|
RestTemplate template = new RestTemplate();
|
||||||
|
template.setErrorHandler(new DefaultResponseErrorHandler() {
|
||||||
|
@Override
|
||||||
|
public boolean hasError(org.springframework.http.client.ClientHttpResponse response) throws IOException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user