Micronaut Mastery: Add Custom Health Indicators
Let's see how we can set up custom app health metrics for our microservices applications in Micronaut quickly and easily.
Join the DZone community and get the full member experience.
Join For FreeWhen we add the io.micronaut:management
dependency to our Micronaut application, we get, among other things, a /health
endpoint. We must enable it in our application configuration, where we can also configure how much information is shown and if we want to secure the endpoint. Micronaut has some built-in health indicators, some of which are only available based on certain conditions. For example, there is a disk space health indicator that will return a status of DOWN when the free disk space is less than a (configurable) threshold. If we would have one or more DataSource
beans for database access in our application context, a health indicator is added to show if the database(s) are available or not.
We can also add our own health indicator that will show up in the /health
endpoint. We must write a class that implements the HealthIndicator
interface and add it to the application context. We could add some conditions to make sure the bean is loaded when needed. Micronaut also has the abstract AbstractHealthIndicator
class, which can be used as base class for writing custom health indicators.
First, we must add the io.micronaut:management
dependency to our compile classpath like in the following example Gradle build file:
// File: build.gradle
...
dependencies {
...
compile "io.micronaut:management"
...
}
...
Next, we write a class that implements HealthIndicator
or extends AbstractHealthIndicator
. In the following example, we implement HealthIndicator
and the method getResult
. This health indicator will try to access a remote URL and will return the status UP when the URL is reachable and DOWN when the status code is invalid or an exception occurs. We also use the @Requires
annotation to make sure the indicator is only loaded when the correct value is set for a configuration property and when the HealthPoint
bean is available.
package mrhaki.micronaut;
import io.micronaut.health.HealthStatus;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.client.Client;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.management.health.indicator.HealthIndicator;
import io.micronaut.management.health.indicator.HealthResult;
import org.reactivestreams.Publisher;
import javax.inject.Singleton;
import java.util.Collections;
@Singleton
// Only create bean when configuration property
// endpoints.health.url.enabled equals true,
// and HealthEndpoint bean to expose /health endpoint is available.
@Requires(property = HealthEndpoint.PREFIX + ".url.enabled", value = "true")
@Requires(beans = HealthEndpoint.class)
public class RemoteUrlHealthIndicator implements HealthIndicator {
/**
* Name for health indicator.
*/
private static final String NAME = "remote-url-health";
/**
* URL to check.
*/
private static final String URL = "http://www.mrhaki.com/";
/**
* We use {@link RxHttpClient} to check if
* URL is reachable.
*/
private RxHttpClient client;
/**
* Inject client with URL to check.
*
* @param client Client to check if URl is reachable.
*/
public RemoteUrlHealthIndicator(@Client(URL) final RxHttpClient client) {
this.client = client;
}
/**
* Implementaton of {@link HealthIndicator#getResult()} where we
* check if the url is reachable and return result based
* on the HTTP status code.
*
* @return Contains {@link HealthResult} with status UP or DOWN.
*/
@Override
public Publisher<HealthResult> getResult() {
return client.exchange(HttpRequest.HEAD("/"))
.map(this::checkStatusCode)
.onErrorReturn(this::statusException);
}
/**
* Check response status code and return status UP when code is
* between 200 and 399, otherwise return status DOWN.
*
* @param response Reponse with status code.
* @return Result with checked URL in the details and status UP or DOWN.
*/
private HealthResult checkStatusCode(HttpResponse<?> response) {
final int statusCode = response.getStatus().getCode();
final boolean statusOk = statusCode >= 200 && statusCode < 400;
final HealthStatus healthStatus = statusOk ? HealthStatus.UP : HealthStatus.DOWN;
// We use the builder API of HealthResult to create
// the health result with a details section containing
// the checked URL and the health status.
return HealthResult.builder(NAME, healthStatus)
.details(Collections.singletonMap("url", URL))
.build();
}
/**
* Set status is DOWN when exception occurs checking URL status.
*
* @param exception Exception thrown when checking status.
* @return Result with exception in details in status DOWN.
*/
private HealthResult statusException(Throwable exception) {
// We use the build API of HealthResult to include
// the original exception in the details and
// status is DOWN.
return HealthResult.builder(NAME, HealthStatus.DOWN)
.exception(exception)
.build();
}
}
Finally, we add configuration properties to our application.yml
file:
# File: src/main/resources/application.yml
...
endpoints:
health:
enabled: true
sensitive: false # non-secured endpoint
details-visible: ANONYMOUS # show details for everyone
url:
enabled: true
...
Let's run our Micronaut application and invoke the /health
endpoint. In the JSON response, we see the output of our custom health indicator:
...
"remote-url-health": {
"details": {
"url": "http://www.mrhaki.com/"
},
"name": "micronaut-sample",
"status": "UP"
},
...
When we use a URL that is not available, we get the following output:
...
"remote-url-health": {
"details": {
"error": "io.micronaut.http.client.exceptions.HttpClientResponseException: Not Found"
},
"name": "micronaut-sample",
"status": "DOWN"
},
...
Written with Micronaut 1.0.0.M4.
Published at DZone with permission of Hubert Klein Ikkink, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments