Spring Boot Actuator

1. Giới Thiệu

1.1 Actuator là gì?

Spring Boot Actuator là một sub-project của Spring Boot cung cấp production-ready features cho ứng dụng của bạn: - Monitor và quản lý ứng dụng trong runtime - Expose các endpoint để thu thập metrics, health checks, application info - Hỗ trợ tích hợp với các monitoring systems như Prometheus, Grafana - Cung cấp insights về performance và behavior của ứng dụng

1.2 Cấu hình Dependencies

<!-- Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Prometheus support -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

<!-- Security (optional) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.springframework.boot:spring-boot-starter-security'

1.3 Cấu hình cơ bản

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,loggers,httptrace
        exclude: shutdown
      base-path: /actuator
      cors:
        allowed-origins: "*"
        allowed-methods: GET,POST
    jmx:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: when-authorized
      show-components: always
      probes:
        enabled: true
    info:
      enabled: true
    metrics:
      enabled: true
  metrics:
    tags:
      application: ${spring.application.name}
      environment: ${spring.profiles.active:default}
  server:
    port: 8081
    address: 127.0.0.1

2. Built-in Endpoints

2.1 Health Endpoint

@Component
public class DatabaseHealthIndicator implements HealthIndicator {

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

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

    @Override
    public Health health() {
        try (Connection connection = dataSource.getConnection()) {
            if (connection.isValid(1)) {
                return Health.up()
                    .withDetail("database", "Available")
                    .withDetail("validationQuery", "SELECT 1")
                    .withDetail("connectionPoolSize", getConnectionPoolSize())
                    .build();
            } else {
                return Health.down()
                    .withDetail("database", "Connection validation failed")
                    .build();
            }
        } catch (SQLException e) {
            logger.error("Database health check failed", e);
            return Health.down()
                .withDetail("database", "Unavailable")
                .withDetail("error", e.getMessage())
                .withException(e)
                .build();
        }
    }

    private int getConnectionPoolSize() {
        if (dataSource instanceof HikariDataSource) {
            return ((HikariDataSource) dataSource).getHikariPoolMXBean().getActiveConnections();
        }
        return -1;
    }
}

// Reactive Health Indicator
@Component
public class ReactiveRedisHealthIndicator implements ReactiveHealthIndicator {

    private final ReactiveRedisTemplate<String, String> redisTemplate;

    public ReactiveRedisHealthIndicator(ReactiveRedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public Mono<Health> health() {
        return redisTemplate.execute(connection -> connection.ping())
            .map(result -> Health.up()
                .withDetail("redis", "Available")
                .withDetail("ping", result)
                .build())
            .onErrorReturn(Health.down()
                .withDetail("redis", "Unavailable")
                .build());
    }
}

2.2 Custom Health Groups

management:
  endpoint:
    health:
      group:
        readiness:
          include: readinessState,database,redis
          show-details: always
        liveness:
          include: livenessState,diskSpace
          show-details: always
@Component
public class CustomReadinessIndicator implements HealthIndicator {

    private final ApplicationEventPublisher eventPublisher;
    private volatile boolean ready = false;

    @Override
    public Health health() {
        if (ready) {
            return Health.up()
                .withDetail("status", "Application is ready to serve traffic")
                .build();
        }
        return Health.down()
            .withDetail("status", "Application is not ready")
            .build();
    }

    @EventListener
    public void onApplicationReady(ApplicationReadyEvent event) {
        ready = true;
    }
}

2.3 Info Endpoint với Git Information

@Component
public class CustomInfoContributor implements InfoContributor {

    private final Environment environment;

    public CustomInfoContributor(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void contribute(Info.Builder builder) {
        Map<String, Object> appInfo = new HashMap<>();
        appInfo.put("name", environment.getProperty("spring.application.name"));
        appInfo.put("version", getClass().getPackage().getImplementationVersion());
        appInfo.put("profiles", environment.getActiveProfiles());
        appInfo.put("jvm", Map.of(
            "version", System.getProperty("java.version"),
            "vendor", System.getProperty("java.vendor")
        ));

        builder.withDetail("application", appInfo);
        builder.withDetail("build", getBuildInfo());
    }

    private Map<String, Object> getBuildInfo() {
        return Map.of(
            "timestamp", Instant.now(),
            "user", System.getProperty("user.name"),
            "machine", getHostname()
        );
    }

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

// Git Info Configuration
@ConfigurationProperties("info.git")
@Data
public class GitProperties {
    private String branch;
    private String commit;
    private String time;
    private String remote;
}

3. Advanced Custom Endpoints

3.1 Feature Toggle Endpoint

@Component
@WebEndpoint(id = "features")
public class FeatureToggleEndpoint {

    private final Map<String, FeatureToggle> features = new ConcurrentHashMap<>();
    private final FeatureToggleService featureService;

    public FeatureToggleEndpoint(FeatureToggleService featureService) {
        this.featureService = featureService;
        initializeFeatures();
    }

    @ReadOperation
    public Map<String, Object> getAllFeatures() {
        return Map.of(
            "features", features,
            "lastUpdated", Instant.now(),
            "total", features.size()
        );
    }

    @ReadOperation
    public FeatureToggle getFeature(@Selector String featureName) {
        return features.get(featureName);
    }

    @WriteOperation
    public void updateFeature(@Selector String featureName, 
                             @Nullable Boolean enabled,
                             @Nullable String description) {
        FeatureToggle feature = features.get(featureName);
        if (feature != null) {
            if (enabled != null) feature.setEnabled(enabled);
            if (description != null) feature.setDescription(description);
            featureService.updateFeature(feature);
        }
    }

    @DeleteOperation
    public void removeFeature(@Selector String featureName) {
        features.remove(featureName);
        featureService.removeFeature(featureName);
    }

    private void initializeFeatures() {
        features.putAll(featureService.getAllFeatures());
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class FeatureToggle {
    private String name;
    private boolean enabled;
    private String description;
    private Instant lastModified;
    private String modifiedBy;
}

3.2 Cache Management Endpoint

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

    private final CacheManager cacheManager;
    private final List<CacheManager> cacheManagers;

    public CacheManagementEndpoint(List<CacheManager> cacheManagers) {
        this.cacheManagers = cacheManagers;
        this.cacheManager = cacheManagers.isEmpty() ? null : cacheManagers.get(0);
    }

    @ReadOperation
    public Map<String, Object> getCaches() {
        Map<String, Object> result = new HashMap<>();

        for (CacheManager cm : cacheManagers) {
            Map<String, Object> cacheInfo = new HashMap<>();

            for (String cacheName : cm.getCacheNames()) {
                Cache cache = cm.getCache(cacheName);
                if (cache != null) {
                    cacheInfo.put(cacheName, getCacheStatistics(cache));
                }
            }

            result.put(cm.getClass().getSimpleName(), cacheInfo);
        }

        return result;
    }

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

    @WriteOperation
    public void evictCacheKey(@Selector String cacheName, @Selector String key) {
        if (cacheManager != null) {
            Cache cache = cacheManager.getCache(cacheName);
            if (cache != null) {
                cache.evict(key);
            }
        }
    }

    private Map<String, Object> getCacheStatistics(Cache cache) {
        if (cache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) {
            @SuppressWarnings("unchecked")
            com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache =
                (com.github.benmanes.caffeine.cache.Cache<Object, Object>) cache.getNativeCache();

            CacheStats stats = caffeineCache.stats();
            return Map.of(
                "size", caffeineCache.estimatedSize(),
                "hitCount", stats.hitCount(),
                "missCount", stats.missCount(),
                "hitRate", stats.hitRate(),
                "evictionCount", stats.evictionCount()
            );
        }

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

4. Security và Access Control

4.1 Endpoint Security Configuration

@Configuration
@EnableWebSecurity
public class ActuatorSecurityConfig {

    @Bean
    public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(authz -> authz
                .requestMatchers(EndpointRequest.to("health", "info")).permitAll()
                .requestMatchers(EndpointRequest.to("metrics", "prometheus")).hasRole("METRICS_READER")
                .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR_ADMIN")
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    public UserDetailsService actuatorUserDetailsService() {
        UserDetails metricsUser = User.builder()
            .username("metrics")
            .password("{bcrypt}$2a$10$...")
            .roles("METRICS_READER")
            .build();

        UserDetails adminUser = User.builder()
            .username("admin")
            .password("{bcrypt}$2a$10$...")
            .roles("ACTUATOR_ADMIN", "METRICS_READER")
            .build();

        return new InMemoryUserDetailsManager(metricsUser, adminUser);
    }
}

4.2 Custom Authentication Provider

@Component
public class ActuatorAuthenticationProvider implements AuthenticationProvider {

    private final ActuatorUserService userService;
    private final PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) 
            throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        ActuatorUser user = userService.findByUsername(username);
        if (user != null && passwordEncoder.matches(password, user.getPassword())) {
            List<GrantedAuthority> authorities = user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());

            return new UsernamePasswordAuthenticationToken(username, password, authorities);
        }

        throw new BadCredentialsException("Invalid credentials");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

5. Advanced Metrics và Monitoring

5.1 Custom Metrics với Micrometer

@Service
public class OrderMetricsService {

    private final MeterRegistry meterRegistry;
    private final Counter orderCreatedCounter;
    private final Counter orderProcessedCounter;
    private final Timer orderProcessingTimer;
    private final Gauge orderQueueSize;
    private final DistributionSummary orderValueSummary;

    private final AtomicInteger queueSize = new AtomicInteger(0);

    public OrderMetricsService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;

        this.orderCreatedCounter = Counter.builder("orders.created")
            .description("Number of orders created")
            .tag("type", "business")
            .register(meterRegistry);

        this.orderProcessedCounter = Counter.builder("orders.processed")
            .description("Number of orders processed")
            .register(meterRegistry);

        this.orderProcessingTimer = Timer.builder("orders.processing.duration")
            .description("Order processing duration")
            .register(meterRegistry);

        this.orderQueueSize = Gauge.builder("orders.queue.size")
            .description("Current order queue size")
            .register(meterRegistry, queueSize, AtomicInteger::get);

        this.orderValueSummary = DistributionSummary.builder("orders.value")
            .description("Order value distribution")
            .baseUnit("currency")
            .register(meterRegistry);
    }

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        orderCreatedCounter.increment(
            Tags.of(
                "customer_type", event.getCustomerType(),
                "product_category", event.getProductCategory()
            )
        );

        orderValueSummary.record(event.getOrderValue().doubleValue());
        queueSize.incrementAndGet();
    }

    @EventListener
    public void onOrderProcessed(OrderProcessedEvent event) {
        orderProcessedCounter.increment(
            Tags.of(
                "status", event.getStatus().name(),
                "processing_type", event.getProcessingType()
            )
        );

        queueSize.decrementAndGet();
    }

    @Timed(value = "orders.processing.duration", description = "Time taken to process order")
    public void processOrder(Order order) {
        // Order processing logic
    }
}

5.2 Database Metrics

@Configuration
public class DatabaseMetricsConfig {

    @Bean
    public MeterBinder dataSourceMetrics(DataSource dataSource) {
        return new DataSourcePoolMetrics(dataSource, "hikari", Tags.empty());
    }

    @Bean
    public MeterBinder jpaMetrics() {
        return new HibernateMetrics();
    }
}

@Component
public class CustomDatabaseMetrics {

    private final JdbcTemplate jdbcTemplate;
    private final MeterRegistry meterRegistry;

    public CustomDatabaseMetrics(JdbcTemplate jdbcTemplate, MeterRegistry meterRegistry) {
        this.jdbcTemplate = jdbcTemplate;
        this.meterRegistry = meterRegistry;

        Gauge.builder("database.connections.active")
            .register(meterRegistry, this, CustomDatabaseMetrics::getActiveConnections);

        Gauge.builder("database.size.total")
            .register(meterRegistry, this, CustomDatabaseMetrics::getDatabaseSize);
    }

    public double getActiveConnections() {
        try {
            return jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM information_schema.processlist WHERE db IS NOT NULL",
                Double.class);
        } catch (Exception e) {
            return 0;
        }
    }

    public double getDatabaseSize() {
        try {
            return jdbcTemplate.queryForObject(
                "SELECT SUM(data_length + index_length) / 1024 / 1024 " +
                "FROM information_schema.tables WHERE table_schema = DATABASE()",
                Double.class);
        } catch (Exception e) {
            return 0;
        }
    }
}

5.3 Application Performance Metrics

@Component
public class ApplicationPerformanceMetrics {

    private final MeterRegistry meterRegistry;

    public ApplicationPerformanceMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        registerCustomMetrics();
    }

    private void registerCustomMetrics() {
        // JVM Metrics
        Gauge.builder("jvm.memory.heap.utilization")
            .register(meterRegistry, this, ApplicationPerformanceMetrics::getHeapUtilization);

        Gauge.builder("jvm.gc.pause.total")
            .register(meterRegistry, this, ApplicationPerformanceMetrics::getTotalGcPause);

        // Application Metrics
        Timer.builder("application.startup.duration")
            .register(meterRegistry);

        Counter.builder("application.errors.total")
            .register(meterRegistry);
    }

    public double getHeapUtilization() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        return (double) heapUsage.getUsed() / heapUsage.getMax();
    }

    public double getTotalGcPause() {
        return ManagementFactory.getGarbageCollectorMXBeans().stream()
            .mapToLong(GarbageCollectorMXBean::getCollectionTime)
            .sum();
    }
}

6. Production Monitoring Integration

6.1 Prometheus Configuration

management:
  metrics:
    export:
      prometheus:
        enabled: true
        step: 1m
        descriptions: true
    distribution:
      percentiles-histogram:
        http.server.requests: true
        orders.processing.duration: true
      percentiles:
        http.server.requests: 0.5, 0.95, 0.99
        orders.processing.duration: 0.5, 0.95, 0.99
      slo:
        http.server.requests: 100ms,200ms,400ms

6.2 Grafana Dashboard Configuration

@Configuration
public class GrafanaMetricsConfig {

    @Bean
    public MeterRegistryCustomizer<PrometheusMeterRegistry> configureMetricsRegistry() {
        return registry -> {
            registry.config()
                .commonTags(
                    "application", "my-spring-boot-app",
                    "instance", getInstanceId(),
                    "environment", getEnvironment()
                )
                .meterFilter(MeterFilter.deny(id -> {
                    String name = id.getName();
                    return name.startsWith("jvm.gc") && name.contains("memory.promoted");
                }))
                .meterFilter(MeterFilter.denyNameStartsWith("tomcat.sessions"));
        };
    }

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

    private String getEnvironment() {
        return System.getProperty("spring.profiles.active", "default");
    }
}

6.3 Alerting Integration

@Component
public class AlertingMetrics {

    private final MeterRegistry meterRegistry;
    private final NotificationService notificationService;

    public AlertingMetrics(MeterRegistry meterRegistry, 
                          NotificationService notificationService) {
        this.meterRegistry = meterRegistry;
        this.notificationService = notificationService;
        setupAlerts();
    }

    private void setupAlerts() {
        // High error rate alert
        meterRegistry.more().counter("application.errors.total", Tags.of("severity", "high"))
            .threshold(10)
            .whenExceeded(this::sendHighErrorRateAlert);

        // Memory usage alert  
        Gauge.builder("jvm.memory.heap.utilization")
            .register(meterRegistry, this, AlertingMetrics::getHeapUtilization)
            .threshold(0.85)
            .whenExceeded(this::sendMemoryAlert);
    }

    private void sendHighErrorRateAlert() {
        notificationService.sendAlert(
            "High Error Rate",
            "Application error rate exceeded threshold"
        );
    }

    private void sendMemoryAlert() {
        notificationService.sendAlert(
            "High Memory Usage", 
            "JVM heap utilization exceeded 85%"
        );
    }

    public double getHeapUtilization() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        return (double) heapUsage.getUsed() / heapUsage.getMax();
    }
}

7. Performance Optimization

7.1 Endpoint Performance Tuning

@Configuration
public class ActuatorPerformanceConfig {

    @Bean
    @ConditionalOnProperty(name = "management.endpoints.web.cache.enabled", havingValue = "true")
    public CacheManager actuatorCacheManager() {
        return CacheManagerBuilder.newCacheManagerBuilder()
            .withCache("health-cache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(
                    String.class, Health.class,
                    ResourcePoolsBuilder.heap(100))
                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(30))))
            .withCache("metrics-cache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(
                    String.class, Object.class,
                    ResourcePoolsBuilder.heap(1000))
                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(10))))
            .build(true);
    }

    @Bean
    public WebMvcConfigurer actuatorCorsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/actuator/**")
                    .allowedOrigins("*")
                    .allowedMethods("GET", "POST")
                    .maxAge(3600);
            }
        };
    }
}

@Component
public class CachedHealthIndicator implements HealthIndicator {

    private final ExternalService externalService;

    @Cacheable(value = "health-cache", key = "'external-service'")
    @Override
    public Health health() {
        try {
            externalService.healthCheck();
            return Health.up()
                .withDetail("external-service", "Available")
                .build();
        } catch (Exception e) {
            return Health.down()
                .withDetail("external-service", "Unavailable")
                .withException(e)
                .build();
        }
    }
}

7.2 Async Metrics Collection

@Configuration
@EnableAsync
public class AsyncMetricsConfig {

    @Bean
    public TaskExecutor metricsTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(5);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("metrics-");
        executor.initialize();
        return executor;
    }
}

@Service
public class AsyncMetricsCollector {

    private final MeterRegistry meterRegistry;

    @Async("metricsTaskExecutor")
    public CompletableFuture<Void> collectBusinessMetrics() {
        // Expensive metrics collection
        Timer.Sample sample = Timer.start(meterRegistry);

        try {
            // Collect metrics from external sources
            collectExternalMetrics();
        } finally {
            sample.stop(Timer.builder("metrics.collection.duration")
                .tag("type", "business")
                .register(meterRegistry));
        }

        return CompletableFuture.completedFuture(null);
    }

    private void collectExternalMetrics() {
        // Implementation for collecting external metrics
    }
}

8. Testing Actuator Endpoints

8.1 Integration Testing

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
    "management.endpoints.web.exposure.include=health,info,metrics",
    "management.endpoint.health.show-details=always"
})
class ActuatorIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @LocalServerPort
    private int port;

    @Test
    void healthEndpointShouldReturnUp() {
        ResponseEntity<Map> response = restTemplate.getForEntity(
            "http://localhost:" + port + "/actuator/health", 
            Map.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().get("status")).isEqualTo("UP");
    }

    @Test
    void metricsEndpointShouldReturnMetrics() {
        ResponseEntity<Map> response = restTemplate.getForEntity(
            "http://localhost:" + port + "/actuator/metrics", 
            Map.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).containsKey("names");
    }

    @Test
    void customEndpointShouldBeAvailable() {
        ResponseEntity<Map> response = restTemplate.getForEntity(
            "http://localhost:" + port + "/actuator/features", 
            Map.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

8.2 Security Testing

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase
class ActuatorSecurityTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void healthEndpointShouldBePublic() {
        ResponseEntity<String> response = restTemplate.getForEntity(
            "/actuator/health", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }

    @Test
    void metricsEndpointShouldRequireAuthentication() {
        ResponseEntity<String> response = restTemplate.getForEntity(
            "/actuator/metrics", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
    }

    @Test
    void metricsEndpointShouldAllowAuthenticatedAccess() {
        ResponseEntity<String> response = restTemplate
            .withBasicAuth("metrics", "password")
            .getForEntity("/actuator/metrics", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

9. Best Practices

9.1 Production Checklist

# Production configuration
management:
  endpoints:
    web:
      exposure:
        # Only expose necessary endpoints
        include: health,info,metrics,prometheus
        exclude: shutdown,env,configprops
      base-path: /actuator
  endpoint:
    health:
      # Don't show sensitive details in production
      show-details: when-authorized
    info:
      enabled: true
  # Use different port for management endpoints
  server:
    port: 8081
    address: 127.0.0.1
  # Security
  security:
    enabled: true
    roles: ACTUATOR_ADMIN

9.2 Monitoring Strategy

@Configuration
public class MonitoringStrategy {

    @Bean
    public MeterRegistryCustomizer<MeterRegistry> monitoringCustomizer() {
        return registry -> {
            // Common tags for all metrics
            registry.config()
                .commonTags(
                    "application", "my-app",
                    "environment", getEnvironment(),
                    "region", getRegion()
                )
                // Filter out noisy metrics
                .meterFilter(MeterFilter.deny(id -> 
                    id.getName().startsWith("tomcat.sessions")))
                // Rename metrics for consistency
                .meterFilter(MeterFilter.renameTag("jvm.memory.used", "area", "heap", "heap"))
                // Set distribution summaries
                .meterFilter(MeterFilter.maximumExpectedValue("http.server.requests", 
                    Duration.ofSeconds(10)));
        };
    }

    private String getEnvironment() {
        return System.getProperty("spring.profiles.active", "default");
    }

    private String getRegion() {
        return System.getProperty("aws.region", "us-east-1");
    }
}

10. References

10.1 Official Documentation

10.2 Monitoring Tools Integration