DEV Community

Cover image for Boost Your Spring Boot App: Custom Metrics and Health Checks Made Easy
Aarav Joshi
Aarav Joshi

Posted on

Boost Your Spring Boot App: Custom Metrics and Health Checks Made Easy

Spring Boot Actuator is a powerful tool for monitoring and managing your applications. But what if you want to go beyond the basics? Let's explore how to create custom metrics and health indicators that give you deeper insights into your app's behavior.

First, let's talk about custom metrics. Micrometer, the metrics library used by Spring Boot, makes it easy to create your own. Here's a simple example:

@Component
public class OrderMetrics {
    private final MeterRegistry meterRegistry;

    public OrderMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        meterRegistry.gauge("orders.pending", this, OrderMetrics::getPendingOrders);
    }

    private int getPendingOrders() {
        // Logic to get pending orders count
        return 10;
    }
}
Enter fullscreen mode Exit fullscreen mode

This code creates a gauge metric that tracks pending orders. You can now see this metric in your actuator endpoints or send it to your monitoring system.

But what if you need something more complex? Let's say you want to track the processing time of orders. You can use a Timer for this:

@Service
public class OrderService {
    private final Timer orderProcessingTimer;

    public OrderService(MeterRegistry meterRegistry) {
        this.orderProcessingTimer = meterRegistry.timer("order.processing");
    }

    public void processOrder(Order order) {
        orderProcessingTimer.record(() -> {
            // Order processing logic
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

This code will track how long it takes to process each order. You can then use this data to set up alerts or optimize your system.

Now, let's move on to health indicators. Spring Boot comes with several built-in health indicators, but you can create your own for specific needs. Here's an example of a custom health indicator that checks if a critical external service is up:

@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
    @Override
    public Health health() {
        if (isExternalServiceUp()) {
            return Health.up().build();
        }
        return Health.down().withDetail("reason", "External service is down").build();
    }

    private boolean isExternalServiceUp() {
        // Logic to check external service
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

This health indicator will show up in your /actuator/health endpoint, giving you a quick way to check if this critical service is available.

But what if you have multiple services to check? You can create a composite health indicator:

@Component
public class CompositeExternalServicesHealthIndicator implements CompositeHealthContributor {
    private final Map<String, HealthIndicator> indicators;

    public CompositeExternalServicesHealthIndicator(
            ServiceAHealthIndicator serviceA,
            ServiceBHealthIndicator serviceB) {
        indicators = Map.of(
            "serviceA", serviceA,
            "serviceB", serviceB
        );
    }

    @Override
    public HealthContributor getContributor(String name) {
        return indicators.get(name);
    }

    @Override
    public Iterator<NamedContributor<HealthContributor>> iterator() {
        return indicators.entrySet().stream()
                .map(entry -> NamedContributor.of(entry.getKey(), entry.getValue()))
                .iterator();
    }
}
Enter fullscreen mode Exit fullscreen mode

This composite indicator combines multiple health checks into one, giving you a comprehensive view of your system's health.

Now, let's talk about custom endpoints. While Spring Boot provides many useful endpoints out of the box, you might need something specific to your application. Here's how you can create a custom endpoint:

@Component
@Endpoint(id = "customInfo")
public class CustomInfoEndpoint {
    @ReadOperation
    public Map<String, Object> info() {
        Map<String, Object> info = new HashMap<>();
        info.put("appName", "MyAwesomeApp");
        info.put("version", "1.0.0");
        info.put("environment", "production");
        return info;
    }
}
Enter fullscreen mode Exit fullscreen mode

This endpoint will be available at /actuator/customInfo and return some basic information about your app.

But what about security? You don't want just anyone accessing your sensitive actuator endpoints. Spring Boot makes it easy to secure these endpoints:

@Configuration
public class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeRequests()
            .anyRequest().hasRole("ACTUATOR")
            .and()
            .httpBasic();
    }
}
Enter fullscreen mode Exit fullscreen mode

This configuration ensures that only users with the ACTUATOR role can access the actuator endpoints.

Let's dive deeper into metrics. Micrometer supports various monitoring systems like Prometheus, Graphite, and InfluxDB. Here's how you can set up Prometheus:

First, add the dependency:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Then, enable the Prometheus endpoint in your application.properties:

management.endpoints.web.exposure.include=prometheus
Enter fullscreen mode Exit fullscreen mode

Now you can scrape metrics from /actuator/prometheus.

But what if you want to track something specific to your business logic? Let's say you want to track how many users are currently logged in:

@Service
public class UserService {
    private final AtomicInteger loggedInUsers;

    public UserService(MeterRegistry meterRegistry) {
        this.loggedInUsers = meterRegistry.gauge("users.logged.in", new AtomicInteger(0));
    }

    public void userLoggedIn() {
        loggedInUsers.incrementAndGet();
    }

    public void userLoggedOut() {
        loggedInUsers.decrementAndGet();
    }
}
Enter fullscreen mode Exit fullscreen mode

This code creates a gauge that tracks the number of logged-in users. You can now monitor this metric and set up alerts if it drops below or exceeds certain thresholds.

Let's talk about dynamic logging configurations. Spring Boot Actuator allows you to change log levels at runtime. Here's how you can implement a custom endpoint to do this:

@Component
@Endpoint(id = "logging")
public class LoggingEndpoint {
    @WriteOperation
    public void configureLogger(@Selector String loggerName, @Nullable String logLevel) {
        Logger logger = LoggerFactory.getLogger(loggerName);
        if (logger instanceof ch.qos.logback.classic.Logger) {
            ch.qos.logback.classic.Logger logbackLogger = (ch.qos.logback.classic.Logger) logger;
            logbackLogger.setLevel(Level.toLevel(logLevel));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

With this endpoint, you can change log levels on the fly by sending a POST request to /actuator/logging.

Now, let's address performance. In high-load scenarios, you might need to optimize your actuator endpoints. One way to do this is by using caching:

@Component
@Endpoint(id = "expensiveOperation")
public class ExpensiveOperationEndpoint {
    private final Cache<String, Object> cache;

    public ExpensiveOperationEndpoint() {
        this.cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .build();
    }

    @ReadOperation
    public Object getExpensiveData(@Selector String key) {
        return cache.get(key, this::fetchExpensiveData);
    }

    private Object fetchExpensiveData(String key) {
        // Expensive operation to fetch data
        return "Expensive data for " + key;
    }
}
Enter fullscreen mode Exit fullscreen mode

This endpoint uses Caffeine cache to store the results of expensive operations for a minute, reducing the load on your system.

Let's wrap up with a complex example that brings together several concepts we've discussed. Imagine you're building a web store and want to monitor various aspects of it:

@Configuration
public class WebStoreMonitoring {
    private final MeterRegistry meterRegistry;

    public WebStoreMonitoring(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @Bean
    public Counter orderCounter() {
        return Counter.builder("orders.total")
                .description("Total number of orders")
                .register(meterRegistry);
    }

    @Bean
    public Timer orderProcessingTimer() {
        return Timer.builder("orders.processing.time")
                .description("Time taken to process orders")
                .register(meterRegistry);
    }

    @Bean
    public Gauge inventoryGauge(InventoryService inventoryService) {
        return Gauge.builder("inventory.total", inventoryService::getTotalItems)
                .description("Total items in inventory")
                .register(meterRegistry);
    }

    @Bean
    public HealthIndicator inventoryHealthIndicator(InventoryService inventoryService) {
        return () -> {
            if (inventoryService.getTotalItems() > 0) {
                return Health.up().build();
            }
            return Health.down().withDetail("reason", "Out of stock").build();
        };
    }

    @Bean
    public Timer paymentProcessingTimer() {
        return Timer.builder("payments.processing.time")
                .description("Time taken to process payments")
                .register(meterRegistry);
    }
}
Enter fullscreen mode Exit fullscreen mode

This configuration sets up various metrics and health indicators for a web store. It tracks the total number of orders, time taken to process orders, total items in inventory, and time taken to process payments. It also includes a health indicator that checks if there are items in stock.

You can use these metrics to monitor your web store's performance and health. For example, you could set up alerts if the order processing time exceeds a certain threshold, or if the inventory drops below a certain level.

Remember, the key to effective monitoring is to track metrics that are meaningful to your specific application. Don't just track everything because you can. Think about what data would be most useful for understanding your application's behavior and health.

In conclusion, Spring Boot Actuator and Micrometer provide powerful tools for monitoring and managing your applications. By creating custom metrics, health indicators, and endpoints, you can gain deep insights into your application's behavior and health. With the techniques we've discussed, you can create a comprehensive, application-specific monitoring and management solution that goes far beyond the basics provided out of the box.


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

Top comments (0)