Spring Boot Actuator provides production-ready features to help you monitor and manage your application. It exposes various endpoints that give insight into application metrics, health, configuration, and more.
What is Spring Boot Actuator?
Spring Boot Actuator offers built-in endpoints that allow you to monitor and interact with your application. It's essential for:
- Application monitoring - Health checks, metrics, and performance data
- Production readiness - Insights into application state and behavior
- Troubleshooting - Diagnostic information and environment details
- Management - Shutdown, configuration changes, and more
Setup and Configuration
Dependencies
Maven:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- For web endpoints --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- For Micrometer metrics (optional) --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>
Gradle:
implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'io.micrometer:micrometer-registry-prometheus'
Basic Configuration
# application.yml management: endpoints: web: exposure: include: health,info,metrics,env,beans base-path: /management # Custom base path enabled-by-default: true endpoint: health: show-details: always show-components: always info: enabled: true info: env: enabled: true build: enabled: true
Core Actuator Endpoints
Example 1: Health Endpoint
import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; import java.util.concurrent.ThreadLocalRandom; @Component public class CustomHealthIndicator implements HealthIndicator { private final DatabaseService databaseService; private final ExternalService externalService; public CustomHealthIndicator(DatabaseService databaseService, ExternalService externalService) { this.databaseService = databaseService; this.externalService = externalService; } @Override public Health health() { // Check database connectivity boolean dbHealthy = databaseService.isConnected(); // Check external service boolean externalHealthy = externalService.isAvailable(); // Custom health logic if (!dbHealthy) { return Health.down() .withDetail("database", "Connection failed") .withDetail("timestamp", System.currentTimeMillis()) .build(); } if (!externalHealthy) { return Health.outOfService() .withDetail("external-service", "Temporarily unavailable") .withException(new RuntimeException("Service timeout")) .build(); } // Simulate some custom metrics double systemLoad = ThreadLocalRandom.current().nextDouble(0.0, 1.0); return Health.up() .withDetail("database", "Connected") .withDetail("external-service", "Available") .withDetail("system-load", systemLoad) .withDetail("active-threads", Thread.activeCount()) .build(); } } // Additional health indicators @Component public class DatabaseHealthIndicator implements HealthIndicator { @Override public Health health() { try { // Simulate database check boolean isHealthy = checkDatabaseConnection(); if (isHealthy) { return Health.up() .withDetail("database", "PostgreSQL") .withDetail("connection-pool", "Active") .withDetail("active-connections", 15) .build(); } else { return Health.down() .withDetail("database", "PostgreSQL") .withDetail("error", "Connection timeout") .build(); } } catch (Exception e) { return Health.down(e).build(); } } private boolean checkDatabaseConnection() { // Actual database connectivity check return Math.random() > 0.2; // 80% success rate for demo } } @Component public class DiskSpaceHealthIndicator implements HealthIndicator { @Override public Health health() { long freeDiskSpace = new File("/").getFreeSpace(); long totalDiskSpace = new File("/").getTotalSpace(); double freePercentage = (double) freeDiskSpace / totalDiskSpace * 100; if (freePercentage > 10) { return Health.up() .withDetail("total-space", formatBytes(totalDiskSpace)) .withDetail("free-space", formatBytes(freeDiskSpace)) .withDetail("free-percentage", String.format("%.2f%%", freePercentage)) .build(); } else { return Health.down() .withDetail("error", "Low disk space") .withDetail("free-percentage", String.format("%.2f%%", freePercentage)) .build(); } } private String formatBytes(long bytes) { return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)); } } Health Endpoint Response:
{ "status": "UP", "components": { "custom": { "status": "UP", "details": { "database": "Connected", "external-service": "Available", "system-load": 0.45, "active-threads": 25 } }, "diskSpace": { "status": "UP", "details": { "total-space": "465.76 GB", "free-space": "125.34 GB", "free-percentage": "26.92%" } }, "ping": { "status": "UP" } } } Custom Endpoints
Example 2: Creating Custom Actuator Endpoints
import org.springframework.boot.actuate.endpoint.annotation.*; import org.springframework.stereotype.Component; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Component @Endpoint(id = "features") public class FeaturesEndpoint { private final Map<String, Feature> features = new ConcurrentHashMap<>(); public FeaturesEndpoint() { // Initialize with some features features.put("user-auth", new Feature("user-auth", true, "User authentication system")); features.put("payment-processing", new Feature("payment-processing", false, "Payment gateway integration")); features.put("analytics", new Feature("analytics", true, "User analytics dashboard")); } @ReadOperation public Map<String, Object> features() { Map<String, Object> result = new HashMap<>(); result.put("timestamp", new Date()); result.put("features", features.values()); result.put("enabledCount", features.values().stream() .filter(Feature::isEnabled) .count()); return result; } @ReadOperation public Feature feature(@Selector String name) { return features.get(name); } @WriteOperation public void updateFeature(@Selector String name, boolean enabled) { Feature feature = features.get(name); if (feature != null) { feature.setEnabled(enabled); feature.setLastModified(new Date()); } } @DeleteOperation public void removeFeature(@Selector String name) { features.remove(name); } public static class Feature { private String name; private boolean enabled; private String description; private Date lastModified; public Feature(String name, boolean enabled, String description) { this.name = name; this.enabled = enabled; this.description = description; this.lastModified = new Date(); } // Getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; this.lastModified = new Date(); } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Date getLastModified() { return lastModified; } public void setLastModified(Date lastModified) { this.lastModified = lastModified; } } } Example 3: Web-specific Endpoint
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.lang.management.*; import java.util.*; @RestControllerEndpoint(id = "jvm") public class JvmInfoEndpoint { @GetMapping public Map<String, Object> jvmInfo() { RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean(); MemoryMXBean memoryMxBean = ManagementFactory.getMemoryMXBean(); ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean(); Map<String, Object> info = new LinkedHashMap<>(); info.put("jvm-name", runtimeMxBean.getVmName()); info.put("jvm-version", runtimeMxBean.getVmVersion()); info.put("start-time", runtimeMxBean.getStartTime()); info.put("uptime", runtimeMxBean.getUptime()); // Memory information Map<String, Object> memory = new HashMap<>(); memory.put("heap-used", memoryMxBean.getHeapMemoryUsage().getUsed()); memory.put("heap-max", memoryMxBean.getHeapMemoryUsage().getMax()); memory.put("non-heap-used", memoryMxBean.getNonHeapMemoryUsage().getUsed()); info.put("memory", memory); // Thread information Map<String, Object> threads = new HashMap<>(); threads.put("thread-count", threadMxBean.getThreadCount()); threads.put("daemon-thread-count", threadMxBean.getDaemonThreadCount()); threads.put("peak-thread-count", threadMxBean.getPeakThreadCount()); info.put("threads", threads); // System properties info.put("system-properties", runtimeMxBean.getSystemProperties()); return info; } @GetMapping("/memory") public ResponseEntity<Map<String, Object>> memoryDetails() { MemoryMXBean memoryMxBean = ManagementFactory.getMemoryMXBean(); Map<String, Object> memoryInfo = new HashMap<>(); memoryInfo.put("heap", formatMemoryUsage(memoryMxBean.getHeapMemoryUsage())); memoryInfo.put("non-heap", formatMemoryUsage(memoryMxBean.getNonHeapMemoryUsage())); memoryInfo.put("object-pending-finalization", memoryMxBean.getObjectPendingFinalizationCount()); return ResponseEntity.ok(memoryInfo); } @GetMapping("/threads") public List<Map<String, Object>> threadDump() { ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean(); long[] threadIds = threadMxBean.getAllThreadIds(); List<Map<String, Object>> threads = new ArrayList<>(); for (long threadId : threadIds) { ThreadInfo threadInfo = threadMxBean.getThreadInfo(threadId); if (threadInfo != null) { Map<String, Object> threadData = new HashMap<>(); threadData.put("id", threadInfo.getThreadId()); threadData.put("name", threadInfo.getThreadName()); threadData.put("state", threadInfo.getThreadState().name()); threadData.put("cpu-time", threadMxBean.getThreadCpuTime(threadId)); threadData.put("user-time", threadMxBean.getThreadUserTime(threadId)); threads.add(threadData); } } return threads; } @PostMapping("/gc") public ResponseEntity<Map<String, Object>> triggerGc() { long startTime = System.currentTimeMillis(); System.gc(); long endTime = System.currentTimeMillis(); Map<String, Object> result = new HashMap<>(); result.put("message", "Garbage collection triggered"); result.put("duration-ms", endTime - startTime); result.put("timestamp", new Date()); return ResponseEntity.ok(result); } private Map<String, Object> formatMemoryUsage(MemoryUsage usage) { Map<String, Object> formatted = new HashMap<>(); formatted.put("init", usage.getInit()); formatted.put("used", usage.getUsed()); formatted.put("committed", usage.getCommitted()); formatted.put("max", usage.getMax()); return formatted; } } Metrics and Monitoring
Example 4: Custom Metrics with Micrometer
import io.micrometer.core.instrument.*; import org.springframework.stereotype.Component; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; @Component public class ApplicationMetrics { private final MeterRegistry meterRegistry; private final Counter apiRequestCounter; private final Timer apiResponseTimer; private final Gauge cacheSizeGauge; private final AtomicLong activeUsers; private final Map<String, Counter> endpointCounters; public ApplicationMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.endpointCounters = new ConcurrentHashMap<>(); this.activeUsers = new AtomicLong(0); // Initialize counters and timers this.apiRequestCounter = Counter.builder("api.requests.total") .description("Total number of API requests") .tag("application", "my-spring-app") .register(meterRegistry); this.apiResponseTimer = Timer.builder("api.response.time") .description("API response time") .register(meterRegistry); this.cacheSizeGauge = Gauge.builder("cache.size") .description("Current cache size") .register(meterRegistry, new AtomicLong(0)); // Register custom metrics registerCustomMetrics(); } public void incrementRequestCount(String endpoint, String method, int status) { apiRequestCounter.increment(); // Endpoint-specific counter String counterName = "api.requests." + endpoint.toLowerCase(); Counter endpointCounter = endpointCounters.computeIfAbsent(counterName, key -> Counter.builder(key) .description("Requests for " + endpoint) .tag("method", method) .tag("status", String.valueOf(status)) .register(meterRegistry)); endpointCounter.increment(); } public void recordResponseTime(String endpoint, long durationMs) { apiResponseTimer.record(java.time.Duration.ofMillis(durationMs)); Timer.builder("api.response.time.endpoint") .tag("endpoint", endpoint) .register(meterRegistry) .record(java.time.Duration.ofMillis(durationMs)); } public void setCacheSize(long size) { // Update gauge value meterRegistry.gauge("cache.size", size); } public void userLoggedIn() { activeUsers.incrementAndGet(); meterRegistry.gauge("users.active", activeUsers); } public void userLoggedOut() { activeUsers.decrementAndGet(); } public void recordBusinessTransaction(String type, double amount, boolean success) { Counter.builder("business.transactions") .tag("type", type) .tag("status", success ? "success" : "failure") .register(meterRegistry) .increment(); if (success) { Counter.builder("business.revenue") .tag("type", type) .register(meterRegistry) .increment(amount); } } private void registerCustomMetrics() { // JVM metrics (already provided by Micrometer, but here's custom example) Gauge.builder("jvm.memory.used.percent") .description("JVM memory used percentage") .baseUnit("percent") .register(meterRegistry, this, metrics -> { Runtime runtime = Runtime.getRuntime(); long used = runtime.totalMemory() - runtime.freeMemory(); long max = runtime.maxMemory(); return (double) used / max * 100; }); } } // Usage in services @Service public class UserService { private final ApplicationMetrics metrics; private final Map<Long, User> userCache = new ConcurrentHashMap<>(); public UserService(ApplicationMetrics metrics) { this.metrics = metrics; } public User getUserById(Long id) { long startTime = System.currentTimeMillis(); try { metrics.incrementRequestCount("getUser", "GET", 200); User user = userCache.computeIfAbsent(id, this::fetchUserFromDatabase); metrics.recordResponseTime("getUser", System.currentTimeMillis() - startTime); metrics.setCacheSize(userCache.size()); return user; } catch (Exception e) { metrics.incrementRequestCount("getUser", "GET", 500); throw e; } } public void userLogin(User user) { metrics.userLoggedIn(); metrics.recordBusinessTransaction("login", 0.0, true); } public void userLogout(User user) { metrics.userLoggedOut(); } private User fetchUserFromDatabase(Long id) { // Database fetch logic return new User(id, "User " + id); } } Info Endpoint Customization
Example 5: Custom Info Contributions
import org.springframework.boot.actuate.info.Info; import org.springframework.boot.actuate.info.InfoContributor; import org.springframework.stereotype.Component; import java.net.InetAddress; import java.util.HashMap; import java.util.Map; @Component public class CustomInfoContributor implements InfoContributor { private final ApplicationProperties properties; private final UserService userService; public CustomInfoContributor(ApplicationProperties properties, UserService userService) { this.properties = properties; this.userService = userService; } @Override public void contribute(Info.Builder builder) { // Application-specific info Map<String, Object> appInfo = new HashMap<>(); appInfo.put("name", properties.getName()); appInfo.put("version", properties.getVersion()); appInfo.put("environment", properties.getEnvironment()); appInfo.put("startup-time", new java.util.Date()); // System info Map<String, Object> systemInfo = new HashMap<>(); try { systemInfo.put("hostname", InetAddress.getLocalHost().getHostName()); systemInfo.put("ip", InetAddress.getLocalHost().getHostAddress()); } catch (Exception e) { systemInfo.put("hostname", "unknown"); } systemInfo.put("java-version", System.getProperty("java.version")); systemInfo.put("os", System.getProperty("os.name")); // Runtime info Runtime runtime = Runtime.getRuntime(); Map<String, Object> runtimeInfo = new HashMap<>(); runtimeInfo.put("available-processors", runtime.availableProcessors()); runtimeInfo.put("free-memory", runtime.freeMemory()); runtimeInfo.put("total-memory", runtime.totalMemory()); runtimeInfo.put("max-memory", runtime.maxMemory()); // Business metrics Map<String, Object> businessInfo = new HashMap<>(); businessInfo.put("active-users", userService.getActiveUserCount()); businessInfo.put("total-users", userService.getTotalUserCount()); businessInfo.put("cache-hit-rate", userService.getCacheHitRate()); builder.withDetail("application", appInfo) .withDetail("system", systemInfo) .withDetail("runtime", runtimeInfo) .withDetail("business", businessInfo) .withDetail("custom-message", "Application is running smoothly!"); } } @Component public class BuildInfoContributor implements InfoContributor { @Override public void contribute(Info.Builder builder) { Map<String, Object> buildInfo = new HashMap<>(); buildInfo.put("artifact", getClass().getPackage().getImplementationTitle()); buildInfo.put("version", getClass().getPackage().getImplementationVersion()); buildInfo.put("build-time", "2024-01-15T10:30:00Z"); // Should come from build properties buildInfo.put("builder", "Spring Boot Maven Plugin"); builder.withDetail("build", buildInfo); } } // Configuration properties @ConfigurationProperties(prefix = "app.info") @Component public class ApplicationProperties { private String name; private String version; private String environment; // Getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getEnvironment() { return environment; } public void setEnvironment(String environment) { this.environment = environment; } } application.yml for Info Endpoint:
management: info: env: enabled: true build: enabled: true git: mode: full app: info: name: "My Spring Application" version: "2.1.0" environment: "production" info: app: name: "${app.info.name}" version: "${app.info.version}" environment: "${app.info.environment}" build: artifact: "@project.artifactId@" name: "@project.name@" version: "@project.version@" time: "@build.time@" Security and Access Control
Example 6: Securing Actuator Endpoints
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; @Configuration @EnableWebSecurity public class ActuatorSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz // Public endpoints .requestMatchers("/public/**").permitAll() // Actuator endpoints with different access levels .requestMatchers(EndpointRequest.to("health", "info")).permitAll() .requestMatchers(EndpointRequest.to("metrics", "env")).hasRole("MONITOR") .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ADMIN") // Application endpoints .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/**").hasRole("USER") .anyRequest().authenticated() ) .httpBasic(withDefaults()) .csrf(csrf -> csrf // Disable CSRF for actuator endpoints (typically not needed for monitoring) .ignoringRequestMatchers(EndpointRequest.toAnyEndpoint()) ); return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails user = User.withUsername("user") .password("{noop}password") // {noop} for plain text (use proper encoding in production) .roles("USER") .build(); UserDetails monitor = User.withUsername("monitor") .password("{noop}monitor123") .roles("USER", "MONITOR") .build(); UserDetails admin = User.withUsername("admin") .password("{noop}admin123") .roles("USER", "MONITOR", "ADMIN") .build(); return new InMemoryUserDetailsManager(user, monitor, admin); } } Example 7: Custom Security for Specific Endpoints
@Component public class ActuatorAccessManager { private static final Set<String> SENSITIVE_ENDPOINTS = Set.of( "env", "configprops", "loggers", "heapdump", "threaddump" ); public boolean canAccess(String endpoint, String userRole, String clientIp) { // Allow health and info for everyone if (endpoint.equals("health") || endpoint.equals("info")) { return true; } // Check if endpoint is sensitive if (SENSITIVE_ENDPOINTS.contains(endpoint)) { return "ADMIN".equals(userRole) && isInternalIp(clientIp); } // Metrics and beans require MONITOR role if (endpoint.equals("metrics") || endpoint.equals("beans")) { return "MONITOR".equals(userRole) || "ADMIN".equals(userRole); } return false; } private boolean isInternalIp(String ip) { return ip.startsWith("192.168.") || ip.startsWith("10.") || ip.equals("127.0.0.1"); } } Complete Configuration Example
application.yml - Complete Actuator Configuration
management: endpoints: web: exposure: include: health,info,metrics,env,beans,loggers,features,jvm exclude: shutdown base-path: /actuator discovery: enabled: true jmx: exposure: include: "*" enabled-by-default: true endpoint: health: show-details: when_authorized show-components: always enabled: true probes: enabled: true info: enabled: true metrics: enabled: true env: enabled: true beans: enabled: true loggers: enabled: true features: enabled: true jvm: enabled: true health: db: enabled: true diskspace: enabled: true redis: enabled: true mail: enabled: true info: env: enabled: true build: enabled: true git: mode: full metrics: export: prometheus: enabled: true distribution: percentiles-histogram: http.server.requests: true enable: jvm: true logback: true system: true process: true server: port: 8081 # Separate management port address: 127.0.0.1 # Bind to localhost only trace: http: enabled: true # Custom application info app: info: name: "My Spring Boot Application" version: "2.1.0" environment: "production" info: app: name: "${app.info.name}" version: "${app.info.version}" environment: "${app.info.environment}" build: artifact: "@project.artifactId@" name: "@project.name@" version: "@project.version@" time: "@build.time@" Testing Actuator Endpoints
Example 8: Testing with Spring Boot Test
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.context.TestPropertySource; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @TestPropertySource(properties = { "management.endpoints.web.exposure.include=*", "management.endpoint.health.show-details=always" }) public class ActuatorEndpointsTest { @Autowired private MockMvc mockMvc; @Test public void testHealthEndpoint() throws Exception { mockMvc.perform(get("/actuator/health")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("UP")) .andExpect(jsonPath("$.components.diskSpace.status").value("UP")); } @Test public void testInfoEndpoint() throws Exception { mockMvc.perform(get("/actuator/info")) .andExpect(status().isOk()) .andExpect(jsonPath("$.app.name").exists()) .andExpect(jsonPath("$.build.version").exists()); } @Test public void testMetricsEndpoint() throws Exception { mockMvc.perform(get("/actuator/metrics")) .andExpect(status().isOk()) .andExpect(jsonPath("$.names").isArray()); } @Test public void testCustomFeaturesEndpoint() throws Exception { mockMvc.perform(get("/actuator/features")) .andExpect(status().isOk()) .andExpect(jsonPath("$.timestamp").exists()) .andExpect(jsonPath("$.features").isArray()) .andExpect(jsonPath("$.enabledCount").isNumber()); } @Test public void testJvmEndpoint() throws Exception { mockMvc.perform(get("/actuator/jvm")) .andExpect(status().isOk()) .andExpect(jsonPath("$.jvm-name").exists()) .andExpect(jsonPath("$.memory").exists()); } } Best Practices
- Security: Always secure sensitive endpoints in production
- Exposure: Only expose necessary endpoints
- Monitoring: Integrate with monitoring systems (Prometheus, Grafana)
- Health Checks: Implement meaningful health indicators
- Customization: Add business-specific metrics and info
- Documentation: Document your custom endpoints
- Testing: Test all actuator endpoints
Common Endpoints Summary
| Endpoint | Description | Typically Enabled |
|---|---|---|
health | Application health information | Yes |
info | Application information | Yes |
metrics | Application metrics | Yes |
env | Environment properties | In development |
beans | Spring beans | In development |
loggers | Logger configurations | In development |
configprops | Configuration properties | In development |
mappings | URL mappings | In development |
shutdown | Graceful shutdown | No (production) |
Spring Boot Actuator provides comprehensive monitoring and management capabilities that are essential for production applications. By leveraging these endpoints, you can gain deep insights into your application's behavior and health.