Master Spring Boot: Create Custom Actuators for Powerful App Insights and Control
Aarav Joshi
Posted on November 15, 2024
Spring Boot's actuator framework is a powerful tool for monitoring and managing applications. But sometimes, you need more than what's offered out of the box. That's where custom actuators come in handy.
I've been working with Spring Boot for years, and I've found that creating custom actuators can give you deep insights into your application's health and performance. Let's dive into how you can build these tailored monitoring solutions.
First, let's set up a basic custom actuator. We'll start by adding the necessary dependency to our pom.xml file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Now, let's create a simple custom actuator that exposes some basic information about our application:
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
@Endpoint(id = "custom")
public class CustomActuator {
@ReadOperation
public Map<String, Object> customEndpoint() {
Map<String, Object> map = new HashMap<>();
map.put("message", "This is a custom actuator endpoint");
map.put("timestamp", System.currentTimeMillis());
return map;
}
}
This creates a new endpoint at /actuator/custom that returns a JSON object with a message and the current timestamp. It's a simple start, but it demonstrates the basic structure of a custom actuator.
But we can do much more than this. Let's create a more advanced actuator that exposes critical metrics about our application. We'll call it the ApplicationInsightsActuator:
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.ThreadMXBean;
@Component
@Endpoint(id = "appinsights")
public class ApplicationInsightsActuator {
@ReadOperation
public Map<String, Object> getAppInsights() {
Map<String, Object> insights = new HashMap<>();
// Memory usage
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
insights.put("heapMemoryUsage", memoryBean.getHeapMemoryUsage());
insights.put("nonHeapMemoryUsage", memoryBean.getNonHeapMemoryUsage());
// Thread information
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
insights.put("threadCount", threadBean.getThreadCount());
insights.put("peakThreadCount", threadBean.getPeakThreadCount());
// Uptime
long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
insights.put("uptime", String.format("%d days, %d hours, %d minutes",
uptime / (1000 * 60 * 60 * 24),
(uptime / (1000 * 60 * 60)) % 24,
(uptime / (1000 * 60)) % 60));
return insights;
}
}
This actuator provides a wealth of information about our application's memory usage, thread count, and uptime. It's much more useful for monitoring the health of our application.
But what if we want to not just read information, but also make changes to our application at runtime? We can do that too. Let's create an actuator that allows us to dynamically adjust logging levels:
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import org.slf4j.LoggerFactory;
@Component
@Endpoint(id = "loglevel")
public class LoggingActuator {
@ReadOperation
public Map<String, String> getLogLevels() {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
Map<String, String> logLevels = new HashMap<>();
for (ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) {
if(logger.getLevel() != null) {
logLevels.put(logger.getName(), logger.getLevel().toString());
}
}
return logLevels;
}
@WriteOperation
public void setLogLevel(String loggerName, String level) {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
ch.qos.logback.classic.Logger logger = loggerContext.getLogger(loggerName);
if (logger != null) {
logger.setLevel(Level.toLevel(level));
}
}
}
This actuator allows us to both read the current log levels and change them on the fly. It's incredibly useful for debugging issues in production without having to restart the application.
Now, let's talk about security. Exposing all this information and allowing runtime changes can be risky if not properly secured. Spring Boot provides built-in security for actuators, but we can enhance it further.
First, let's ensure that our actuator endpoints are only accessible over HTTPS. We can do this by adding the following to our application.properties file:
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=mypassword
server.ssl.keyStoreType=PKCS12
server.ssl.keyAlias=tomcat
management.server.ssl.enabled=true
Next, let's add authentication to our actuator endpoints. We'll use Spring Security for this:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class ActuatorSecurity extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher(EndpointRequest.toAnyEndpoint()).authorizeRequests()
.anyRequest().hasRole("ACTUATOR")
.and()
.httpBasic();
}
}
This configuration ensures that only users with the ACTUATOR role can access our actuator endpoints, and they must authenticate using HTTP Basic Authentication.
Now that we've covered the basics of creating custom actuators and securing them, let's talk about some advanced techniques.
One powerful feature of custom actuators is the ability to integrate with other parts of your application. For example, let's say we have a caching system in our application, and we want to be able to monitor and manage it through an actuator.
Here's an example of how we might implement this:
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
@Component
@Endpoint(id = "cache")
public class CacheActuator {
private final CacheManager cacheManager;
public CacheActuator(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@ReadOperation
public Map<String, Object> getCacheStats() {
Map<String, Object> stats = new HashMap<>();
for (String name : cacheManager.getCacheNames()) {
Cache cache = cacheManager.getCache(name);
stats.put(name, cache.getNativeCache().stats());
}
return stats;
}
@WriteOperation
public void clearCache(String cacheName) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.clear();
}
}
}
This actuator allows us to view statistics for all our caches and clear specific caches as needed. It's a great example of how custom actuators can provide deep, application-specific insights and controls.
Another advanced technique is creating actuators that can affect the runtime behavior of your application. For example, let's create an actuator that allows us to dynamically add or remove features from our application:
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
@Component
@Endpoint(id = "features")
public class FeatureToggleActuator {
private Map<String, Boolean> features = new HashMap<>();
@ReadOperation
public Map<String, Boolean> getFeatures() {
return features;
}
@WriteOperation
public void setFeature(String featureName, boolean enabled) {
features.put(featureName, enabled);
}
public boolean isFeatureEnabled(String featureName) {
return features.getOrDefault(featureName, false);
}
}
This actuator allows us to dynamically enable or disable features in our application. We can use the isFeatureEnabled method throughout our code to check if a feature should be active.
Now, let's talk about how to make our custom actuators more robust and production-ready. One important aspect is proper error handling. We should ensure that our actuators gracefully handle and report errors, rather than just throwing exceptions.
Here's an example of how we might enhance our CacheActuator with better error handling:
@ReadOperation
public Map<String, Object> getCacheStats() {
Map<String, Object> stats = new HashMap<>();
for (String name : cacheManager.getCacheNames()) {
try {
Cache cache = cacheManager.getCache(name);
stats.put(name, cache.getNativeCache().stats());
} catch (Exception e) {
stats.put(name, "Error: " + e.getMessage());
}
}
return stats;
}
@WriteOperation
public Map<String, String> clearCache(String cacheName) {
Map<String, String> result = new HashMap<>();
try {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.clear();
result.put("status", "success");
result.put("message", "Cache cleared successfully");
} else {
result.put("status", "error");
result.put("message", "Cache not found");
}
} catch (Exception e) {
result.put("status", "error");
result.put("message", "Error clearing cache: " + e.getMessage());
}
return result;
}
This version of the actuator catches and reports any errors that occur while accessing or clearing the cache, providing more useful feedback to the user.
Another important consideration for production-ready actuators is performance. We need to ensure that our custom actuators don't negatively impact the performance of our application. For actuators that perform potentially expensive operations, we might want to consider caching the results or limiting how often the operation can be performed.
Here's an example of how we might implement caching for our ApplicationInsightsActuator:
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.TimeUnit;
@Component
@Endpoint(id = "appinsights")
public class ApplicationInsightsActuator {
private Map<String, Object> cachedInsights;
private long lastUpdateTime;
private static final long CACHE_DURATION = TimeUnit.MINUTES.toMillis(1);
@ReadOperation
public synchronized Map<String, Object> getAppInsights() {
long currentTime = System.currentTimeMillis();
if (cachedInsights == null || currentTime - lastUpdateTime > CACHE_DURATION) {
cachedInsights = generateAppInsights();
lastUpdateTime = currentTime;
}
return cachedInsights;
}
private Map<String, Object> generateAppInsights() {
Map<String, Object> insights = new HashMap<>();
// Memory usage
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
insights.put("heapMemoryUsage", memoryBean.getHeapMemoryUsage());
insights.put("nonHeapMemoryUsage", memoryBean.getNonHeapMemoryUsage());
// Thread information
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
insights.put("threadCount", threadBean.getThreadCount());
insights.put("peakThreadCount", threadBean.getPeakThreadCount());
// Uptime
long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
insights.put("uptime", String.format("%d days, %d hours, %d minutes",
uptime / (1000 * 60 * 60 * 24),
(uptime / (1000 * 60 * 60)) % 24,
(uptime / (1000 * 60)) % 60));
return insights;
}
}
This version of the actuator caches the results for one minute, reducing the load on the system when the actuator is called frequently.
Lastly, let's discuss how to integrate our custom actuators with monitoring and alerting systems. While Spring Boot provides built-in support for exposing metrics to systems like Prometheus, we can enhance this with our custom actuators.
For example, we could create a custom actuator that exposes application-specific metrics in a format that can be easily consumed by Prometheus:
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
@Component
@Endpoint(id = "custommetrics")
public class CustomMetricsActuator {
private final MeterRegistry meterRegistry;
public CustomMetricsActuator(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@ReadOperation(produces = "text/plain")
public String getMetrics() {
StringBuilder metrics = new StringBuilder();
meterRegistry.getMeters().forEach(meter -> {
meter.measure().forEach(measurement -> {
metrics.append(meter.getId().getName())
.append("{")
.append(meter.getId().getTags().stream()
.map(tag -> tag.getKey() + "=\"" + tag.getValue() + "\"")
.collect(Collectors.joining(",")))
.append("} ")
.append(measurement.getValue())
.append("\n");
});
});
return metrics.toString();
}
}
This actuator exposes all the metrics in the MeterRegistry in a format that Prometheus can scrape. We can then use these metrics to set up alerts and dashboards in our monitoring system.
In conclusion, custom Spring Boot actuators are a powerful tool for gaining deep insights into your application and providing fine-grained control over its behavior. By creating tailored actuators, you can expose exactly the information and controls that are most relevant to your specific application. Remember to consider security, performance, and integration with your broader monitoring ecosystem when designing your custom actuators. With these tools at your disposal, you'll be well-equipped to build robust, observable, and manageable Spring Boot applications.
Our Creations
Be sure to check out our creations:
Investor Central | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Posted on November 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
November 27, 2024
November 23, 2024
November 21, 2024