Files
2025-11-29 18:28:34 +08:00

29 KiB

Spring Boot Actuator Examples

Complete Application Example

Application Configuration

@SpringBootApplication
public class MonitoringApplication {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(MonitoringApplication.class);
        // Enable startup tracking
        app.setApplicationStartup(new BufferingApplicationStartup(2048));
        app.run(args);
    }

    @Bean
    public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
        return registry -> registry.config()
            .commonTags("application", "order-service", "environment", "production");
    }
}

Application Properties

spring:
  application:
    name: order-service

info:
  app:
    name: ${spring.application.name}
    description: Order Processing Service
    version: "@project.version@"
    encoding: "@project.build.sourceEncoding@"
    java:
      version: "@java.version@"

management:
  endpoints:
    web:
      exposure:
        include: "health,info,metrics,prometheus,startup"
      base-path: "/actuator"
  
  endpoint:
    health:
      show-details: when-authorized
      show-components: always
      probes:
        enabled: true
      group:
        liveness:
          include: "ping,diskSpace"
        readiness:
          include: "readinessState,db,redis,externalApi"
          show-details: always
      status:
        order: "fatal,down,out-of-service,warning,unknown,up"
        http-mapping:
          down: 503
          fatal: 503
          warning: 500
    
    info:
      enabled: true
    
    metrics:
      enabled: true
  
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: ${spring.application.name}
      region: eu-west-1

  info:
    git:
      mode: full
    build:
      enabled: true

Health Indicators Examples

Database Health Indicator

@Component
public class DatabaseHealthIndicator implements HealthIndicator {

    private static final Logger log = LoggerFactory.getLogger(DatabaseHealthIndicator.class);
    private final DataSource dataSource;

    public DatabaseHealthIndicator(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Health health() {
        try (Connection connection = dataSource.getConnection()) {
            long startTime = System.currentTimeMillis();
            boolean valid = connection.isValid(1000);
            long responseTime = System.currentTimeMillis() - startTime;

            if (!valid) {
                return Health.down()
                    .withDetail("database", "Connection not valid")
                    .build();
            }

            DatabaseMetaData metaData = connection.getMetaData();
            
            Health.Builder builder = Health.up()
                .withDetail("database", metaData.getDatabaseProductName())
                .withDetail("version", metaData.getDatabaseProductVersion())
                .withDetail("responseTime", responseTime + "ms");

            if (responseTime > 500) {
                builder.status("WARNING")
                    .withDetail("warning", "Slow database connection");
            }

            return builder.build();

        } catch (SQLException ex) {
            log.error("Database health check failed", ex);
            return Health.down()
                .withDetail("error", ex.getMessage())
                .withException(ex)
                .build();
        }
    }
}

External API Health Indicator with Circuit Breaker

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final RestTemplate restTemplate;
    private final CircuitBreaker circuitBreaker;

    public PaymentGatewayHealthIndicator(
            RestTemplate restTemplate,
            @Qualifier("paymentCircuitBreaker") CircuitBreaker circuitBreaker) {
        this.restTemplate = restTemplate;
        this.circuitBreaker = circuitBreaker;
    }

    @Override
    public Health health() {
        CircuitBreaker.State state = circuitBreaker.getState();
        
        Health.Builder builder = Health.up()
            .withDetail("circuitBreaker", state.toString())
            .withDetail("service", "Payment Gateway");

        if (state == CircuitBreaker.State.OPEN) {
            return builder
                .down()
                .withDetail("reason", "Circuit breaker is open")
                .build();
        }

        if (state == CircuitBreaker.State.HALF_OPEN) {
            builder.status("WARNING")
                .withDetail("reason", "Circuit breaker is testing");
        }

        try {
            long startTime = System.currentTimeMillis();
            ResponseEntity<Map> response = restTemplate.getForEntity(
                "https://api.payment.com/health", 
                Map.class
            );
            long responseTime = System.currentTimeMillis() - startTime;

            return builder
                .withDetail("responseTime", responseTime + "ms")
                .withDetail("statusCode", response.getStatusCode().value())
                .build();

        } catch (Exception ex) {
            return builder
                .down()
                .withDetail("error", ex.getMessage())
                .build();
        }
    }
}

Cache Health Indicator

@Component
public class CacheHealthIndicator implements HealthIndicator {

    private final CacheManager cacheManager;

    public CacheHealthIndicator(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    @Override
    public Health health() {
        Collection<String> cacheNames = cacheManager.getCacheNames();
        
        Map<String, Object> cacheDetails = new HashMap<>();
        boolean allHealthy = true;

        for (String cacheName : cacheNames) {
            Cache cache = cacheManager.getCache(cacheName);
            if (cache != null) {
                try {
                    // Test cache operations
                    cache.put("health-check", "test");
                    String value = cache.get("health-check", String.class);
                    cache.evict("health-check");
                    
                    cacheDetails.put(cacheName, "UP");
                } catch (Exception ex) {
                    cacheDetails.put(cacheName, "DOWN: " + ex.getMessage());
                    allHealthy = false;
                }
            }
        }

        Health.Builder builder = allHealthy ? Health.up() : Health.down();
        return builder
            .withDetail("caches", cacheDetails)
            .withDetail("totalCaches", cacheNames.size())
            .build();
    }
}

Reactive Health Indicator

@Component
public class ReactiveExternalServiceHealthIndicator implements ReactiveHealthIndicator {

    private final WebClient webClient;

    public ReactiveExternalServiceHealthIndicator(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder
            .baseUrl("https://api.example.com")
            .build();
    }

    @Override
    public Mono<Health> health() {
        return webClient
            .get()
            .uri("/health")
            .retrieve()
            .toBodilessEntity()
            .map(response -> Health.up()
                .withDetail("statusCode", response.getStatusCode().value())
                .withDetail("service", "External API")
                .build())
            .timeout(Duration.ofSeconds(2))
            .onErrorResume(TimeoutException.class, ex -> 
                Mono.just(Health.down()
                    .withDetail("error", "Timeout after 2 seconds")
                    .build()))
            .onErrorResume(ex -> 
                Mono.just(Health.down()
                    .withDetail("error", ex.getMessage())
                    .build()));
    }
}

Custom Endpoints Examples

Application Statistics Endpoint

@Component
@Endpoint(id = "appstats")
public class AppStatisticsEndpoint {

    private final UserRepository userRepository;
    private final OrderRepository orderRepository;
    private final MeterRegistry meterRegistry;

    public AppStatisticsEndpoint(
            UserRepository userRepository,
            OrderRepository orderRepository,
            MeterRegistry meterRegistry) {
        this.userRepository = userRepository;
        this.orderRepository = orderRepository;
        this.meterRegistry = meterRegistry;
    }

    @ReadOperation
    public Map<String, Object> getStatistics() {
        Map<String, Object> stats = new HashMap<>();
        
        // User statistics
        stats.put("users", Map.of(
            "total", userRepository.count(),
            "active", userRepository.countByStatus("ACTIVE"),
            "inactive", userRepository.countByStatus("INACTIVE")
        ));
        
        // Order statistics
        stats.put("orders", Map.of(
            "total", orderRepository.count(),
            "pending", orderRepository.countByStatus("PENDING"),
            "completed", orderRepository.countByStatus("COMPLETED"),
            "cancelled", orderRepository.countByStatus("CANCELLED")
        ));
        
        // JVM statistics
        stats.put("jvm", Map.of(
            "memoryUsed", getMetricValue("jvm.memory.used"),
            "memoryMax", getMetricValue("jvm.memory.max"),
            "threadCount", getMetricValue("jvm.threads.live")
        ));
        
        stats.put("timestamp", Instant.now());
        
        return stats;
    }

    @ReadOperation
    public Map<String, Object> getStatisticsByType(@Selector String type) {
        return switch (type.toLowerCase()) {
            case "users" -> Map.of(
                "total", userRepository.count(),
                "byStatus", userRepository.countByStatusGrouped()
            );
            case "orders" -> Map.of(
                "total", orderRepository.count(),
                "byStatus", orderRepository.countByStatusGrouped()
            );
            default -> Map.of("error", "Unknown type: " + type);
        };
    }

    private Double getMetricValue(String meterName) {
        return meterRegistry.find(meterName)
            .gauge()
            .map(Gauge::value)
            .orElse(0.0);
    }
}

Feature Flags Endpoint

@Component
@Endpoint(id = "features")
public class FeatureFlagsEndpoint {

    private final Map<String, FeatureFlag> features = new ConcurrentHashMap<>();
    private final ApplicationEventPublisher eventPublisher;

    public FeatureFlagsEndpoint(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
        initializeDefaultFeatures();
    }

    private void initializeDefaultFeatures() {
        features.put("dark-mode", new FeatureFlag(true, "Dark mode UI"));
        features.put("new-checkout", new FeatureFlag(false, "New checkout flow"));
        features.put("ai-recommendations", new FeatureFlag(false, "AI-powered recommendations"));
    }

    @ReadOperation
    public Map<String, FeatureFlag> getAllFeatures() {
        return features;
    }

    @ReadOperation
    public FeatureFlag getFeature(@Selector String name) {
        return features.get(name);
    }

    @WriteOperation
    public void updateFeature(
            @Selector String name,
            @Nullable Boolean enabled,
            @Nullable String description) {
        
        features.compute(name, (key, existing) -> {
            if (existing == null) {
                existing = new FeatureFlag(false, "");
            }
            
            if (enabled != null) {
                existing.setEnabled(enabled);
            }
            if (description != null) {
                existing.setDescription(description);
            }
            
            return existing;
        });

        eventPublisher.publishEvent(new FeatureFlagChangedEvent(name, features.get(name)));
    }

    @DeleteOperation
    public void deleteFeature(@Selector String name) {
        FeatureFlag removed = features.remove(name);
        if (removed != null) {
            eventPublisher.publishEvent(new FeatureFlagDeletedEvent(name));
        }
    }

    public static class FeatureFlag {
        private boolean enabled;
        private String description;
        private Instant lastModified = Instant.now();

        public FeatureFlag() {}

        public FeatureFlag(boolean enabled, String description) {
            this.enabled = enabled;
            this.description = description;
        }

        // Getters and setters
        public boolean isEnabled() { return enabled; }
        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
            this.lastModified = Instant.now();
        }
        public String getDescription() { return description; }
        public void setDescription(String description) { this.description = description; }
        public Instant getLastModified() { return lastModified; }
    }
}

Cache Management Endpoint

@Component
@Endpoint(id = "caches")
public class CacheManagementEndpoint {

    private final CacheManager cacheManager;

    public CacheManagementEndpoint(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    @ReadOperation
    public Map<String, Object> getCaches() {
        Collection<String> cacheNames = cacheManager.getCacheNames();
        Map<String, Object> result = new HashMap<>();
        
        result.put("totalCaches", cacheNames.size());
        result.put("caches", cacheNames);
        
        return result;
    }

    @ReadOperation
    public Map<String, Object> getCache(@Selector String cacheName) {
        Cache cache = cacheManager.getCache(cacheName);
        if (cache == null) {
            return Map.of("error", "Cache not found: " + cacheName);
        }

        return Map.of(
            "name", cacheName,
            "type", cache.getClass().getSimpleName()
        );
    }

    @DeleteOperation
    public void clearCache(@Selector String cacheName) {
        Cache cache = cacheManager.getCache(cacheName);
        if (cache != null) {
            cache.clear();
        }
    }

    @WriteOperation
    public void clearAllCaches() {
        cacheManager.getCacheNames()
            .forEach(name -> {
                Cache cache = cacheManager.getCache(name);
                if (cache != null) {
                    cache.clear();
                }
            });
    }
}

Custom Info Contributors

Detailed Application Info

@Component
public class DetailedApplicationInfoContributor implements InfoContributor {

    private final Environment environment;
    private final UserRepository userRepository;
    private final OrderRepository orderRepository;

    public DetailedApplicationInfoContributor(
            Environment environment,
            UserRepository userRepository,
            OrderRepository orderRepository) {
        this.environment = environment;
        this.userRepository = userRepository;
        this.orderRepository = orderRepository;
    }

    @Override
    public void contribute(Info.Builder builder) {
        // Runtime information
        Runtime runtime = Runtime.getRuntime();
        builder.withDetail("runtime", Map.of(
            "processors", runtime.availableProcessors(),
            "freeMemory", runtime.freeMemory(),
            "totalMemory", runtime.totalMemory(),
            "maxMemory", runtime.maxMemory(),
            "uptime", ManagementFactory.getRuntimeMXBean().getUptime()
        ));

        // Active profiles
        builder.withDetail("profiles", List.of(environment.getActiveProfiles()));

        // Database statistics
        builder.withDetail("database", Map.of(
            "users", Map.of(
                "total", userRepository.count(),
                "active", userRepository.countByStatus("ACTIVE")
            ),
            "orders", Map.of(
                "total", orderRepository.count(),
                "pending", orderRepository.countByStatus("PENDING"),
                "completed", orderRepository.countByStatus("COMPLETED")
            )
        ));

        // Deployment information
        builder.withDetail("deployment", Map.of(
            "environment", environment.getProperty("app.environment", "unknown"),
            "region", environment.getProperty("app.region", "unknown"),
            "instance", getHostname()
        ));
    }

    private String getHostname() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (Exception e) {
            return "unknown";
        }
    }
}

Dependency Version Info

@Component
public class DependencyVersionInfoContributor implements InfoContributor {

    @Override
    public void contribute(Info.Builder builder) {
        Map<String, String> versions = new HashMap<>();
        
        // Spring versions
        versions.put("spring-boot", SpringBootVersion.getVersion());
        versions.put("spring-framework", SpringVersion.getVersion());
        
        // Java version
        versions.put("java", System.getProperty("java.version"));
        versions.put("java-vendor", System.getProperty("java.vendor"));
        
        // Other dependencies (if available)
        addVersionIfPresent(versions, "hibernate", "org.hibernate.Version", "getVersionString");
        addVersionIfPresent(versions, "jackson", "com.fasterxml.jackson.core.Version", "versionString");
        
        builder.withDetail("dependencies", versions);
    }

    private void addVersionIfPresent(Map<String, String> versions, String key, 
                                      String className, String methodName) {
        try {
            Class<?> clazz = Class.forName(className);
            Object versionInstance = clazz.getDeclaredConstructor().newInstance();
            String version = (String) clazz.getMethod(methodName).invoke(versionInstance);
            versions.put(key, version);
        } catch (Exception e) {
            // Dependency not present or version not accessible
        }
    }
}

Metrics Examples

Service Metrics

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final MeterRegistry meterRegistry;
    private final Counter orderCreatedCounter;
    private final Counter orderFailedCounter;
    private final Timer orderProcessingTimer;
    private final DistributionSummary orderAmountSummary;

    public OrderService(OrderRepository orderRepository, MeterRegistry meterRegistry) {
        this.orderRepository = orderRepository;
        this.meterRegistry = meterRegistry;

        // Counters
        this.orderCreatedCounter = Counter.builder("orders.created")
            .description("Total number of orders created")
            .tag("service", "order")
            .register(meterRegistry);

        this.orderFailedCounter = Counter.builder("orders.failed")
            .description("Total number of failed orders")
            .tag("service", "order")
            .register(meterRegistry);

        // Timer for processing duration
        this.orderProcessingTimer = Timer.builder("order.processing.time")
            .description("Order processing duration")
            .publishPercentiles(0.5, 0.95, 0.99)
            .register(meterRegistry);

        // Distribution summary for order amounts
        this.orderAmountSummary = DistributionSummary.builder("order.amount")
            .description("Order amount distribution")
            .baseUnit("EUR")
            .publishPercentiles(0.5, 0.95, 0.99)
            .register(meterRegistry);

        // Gauge for pending orders
        Gauge.builder("orders.pending", orderRepository, repo -> repo.countByStatus("PENDING"))
            .description("Number of pending orders")
            .register(meterRegistry);
    }

    public Order createOrder(OrderRequest request) {
        return orderProcessingTimer.record(() -> {
            try {
                Order order = processOrder(request);
                orderCreatedCounter.increment();
                orderAmountSummary.record(order.getTotalAmount());
                
                // Tag by payment method
                Counter.builder("orders.created.by.payment")
                    .tag("paymentMethod", order.getPaymentMethod())
                    .register(meterRegistry)
                    .increment();
                
                return order;
            } catch (Exception ex) {
                orderFailedCounter.increment();
                throw ex;
            }
        });
    }

    private Order processOrder(OrderRequest request) {
        // Implementation
        return new Order();
    }
}

Custom Metrics with Tags

@Service
public class MetricsService {

    private final MeterRegistry registry;

    public MetricsService(MeterRegistry registry) {
        this.registry = registry;
    }

    public void recordHttpRequest(String method, String endpoint, int statusCode, long duration) {
        Timer.builder("http.requests")
            .tag("method", method)
            .tag("endpoint", endpoint)
            .tag("status", String.valueOf(statusCode))
            .register(registry)
            .record(duration, TimeUnit.MILLISECONDS);
    }

    public void recordDatabaseQuery(String query, long duration, boolean success) {
        Timer.builder("db.queries")
            .tag("query", query)
            .tag("success", String.valueOf(success))
            .register(registry)
            .record(duration, TimeUnit.MILLISECONDS);
    }

    public void trackCacheHit(String cacheName, boolean hit) {
        Counter.builder("cache.operations")
            .tag("cache", cacheName)
            .tag("result", hit ? "hit" : "miss")
            .register(registry)
            .increment();
    }

    public void recordBusinessMetric(String metricName, double value, Map<String, String> tags) {
        DistributionSummary.Builder builder = DistributionSummary.builder(metricName);
        tags.forEach(builder::tag);
        builder.register(registry).record(value);
    }
}

Security Configuration Examples

Complete Security Setup

@Configuration
@EnableWebSecurity
public class ActuatorSecurityConfiguration {

    @Bean
    public SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth
                // Public health check (for load balancers)
                .requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
                
                // Info endpoint for authenticated users
                .requestMatchers(EndpointRequest.to(InfoEndpoint.class)).authenticated()
                
                // Read-only metrics for monitoring role
                .requestMatchers(HttpMethod.GET, "/actuator/metrics/**")
                    .hasAnyRole("MONITOR", "ADMIN")
                
                // Prometheus endpoint for monitoring tools
                .requestMatchers(EndpointRequest.to("prometheus"))
                    .hasRole("MONITOR")
                
                // Write operations only for admin
                .requestMatchers(HttpMethod.POST, "/actuator/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.DELETE, "/actuator/**").hasRole("ADMIN")
                
                // Everything else requires admin
                .anyRequest().hasRole("ADMIN")
            )
            .httpBasic(Customizer.withDefaults())
            .build();
    }

    @Bean
    public UserDetailsService actuatorUsers() {
        UserDetails monitor = User.builder()
            .username("monitor")
            .password("{noop}monitor-password")
            .roles("MONITOR")
            .build();

        UserDetails admin = User.builder()
            .username("admin")
            .password("{noop}admin-password")
            .roles("ADMIN", "MONITOR")
            .build();

        return new InMemoryUserDetailsManager(monitor, admin);
    }
}

IP-Based Access Control

@Configuration
public class IpBasedActuatorSecurity {

    @Bean
    public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
        return http
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(request -> 
                    isFromAllowedIp(request.getRemoteAddr())
                ).permitAll()
                .anyRequest().denyAll()
            )
            .build();
    }

    private boolean isFromAllowedIp(String remoteAddr) {
        // Allow localhost and specific IPs
        return remoteAddr.equals("127.0.0.1") ||
               remoteAddr.equals("0:0:0:0:0:0:0:1") ||
               remoteAddr.startsWith("10.0.0.");
    }
}

Testing Examples

Health Indicator Tests

@SpringBootTest
class DatabaseHealthIndicatorTest {

    @Autowired
    private DatabaseHealthIndicator healthIndicator;

    @MockBean
    private DataSource dataSource;

    @Test
    void shouldReturnUpWhenDatabaseIsHealthy() throws Exception {
        Connection connection = mock(Connection.class);
        when(dataSource.getConnection()).thenReturn(connection);
        when(connection.isValid(1000)).thenReturn(true);

        DatabaseMetaData metaData = mock(DatabaseMetaData.class);
        when(connection.getMetaData()).thenReturn(metaData);
        when(metaData.getDatabaseProductName()).thenReturn("PostgreSQL");

        Health health = healthIndicator.health();

        assertThat(health.getStatus()).isEqualTo(Status.UP);
        assertThat(health.getDetails()).containsKey("database");
    }

    @Test
    void shouldReturnDownWhenDatabaseConnectionFails() throws Exception {
        when(dataSource.getConnection()).thenThrow(new SQLException("Connection failed"));

        Health health = healthIndicator.health();

        assertThat(health.getStatus()).isEqualTo(Status.DOWN);
        assertThat(health.getDetails()).containsKey("error");
    }
}

Endpoint Tests

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class ActuatorEndpointIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void healthEndpointShouldBeAccessible() throws Exception {
        mockMvc.perform(get("/actuator/health"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("UP"));
    }

    @Test
    void metricsEndpointShouldListAvailableMetrics() throws Exception {
        mockMvc.perform(get("/actuator/metrics"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.names").isArray())
            .andExpect(jsonPath("$.names[*]", hasItem("jvm.memory.used")));
    }

    @Test
    void customEndpointShouldWork() throws Exception {
        mockMvc.perform(get("/actuator/features"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    void securedEndpointShouldRequireAuthentication() throws Exception {
        mockMvc.perform(post("/actuator/features/test")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"enabled\":true}"))
            .andExpect(status().isOk());
    }
}

Kubernetes Integration Example

Deployment with Probes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      - name: order-service
        image: order-service:1.0.0
        ports:
        - containerPort: 8080
          name: http
        - containerPort: 8081
          name: management
        
        env:
        - name: MANAGEMENT_SERVER_PORT
          value: "8081"
        - name: MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE
          value: "health,info,prometheus"
        
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: management
          initialDelaySeconds: 60
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: management
          initialDelaySeconds: 30
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3
        
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1000m"
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
  - name: http
    port: 8080
    targetPort: http
  - name: management
    port: 8081
    targetPort: management

ServiceMonitor for Prometheus Operator

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: order-service-metrics
  labels:
    app: order-service
spec:
  selector:
    matchLabels:
      app: order-service
  endpoints:
  - port: management
    path: /actuator/prometheus
    interval: 30s
    scrapeTimeout: 10s