Search

Dark theme | Light theme
Showing posts with label MicronautMastery:Management. Show all posts
Showing posts with label MicronautMastery:Management. Show all posts

August 29, 2018

Micronaut Mastery: Adding Custom Info To Info Endpoint

In a previous post we learned how to add build information to the /info endpoint in our application. We can add custom information to the /info endpoint via our application configuration or via a bean by implementing the InfoSource interface. Remember we need to add the io.micronaut:management dependency to our application for this feature.

Any configuration property that is prefixed with info will be exposed via the /info endpoint. We can define the properties in configuration files, using Java system properties or system environment variables. In the following example configuration file we add the property info.sample.message:

// File: src/main/resources/application.yml
...
info:
  sample:
    message: Micronaut is awesome

Another option is to create a new class that implements the InfoSource interface. We need to override the getSource method to return a Publisher with a PropertySource. In our example application we have a ConferenceRepository bean that can access a database with data about conferences. In the following example InfoSource implementation we want to return the number of conferences that are stored in the database:

package mrhaki.micronaut;

import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.MapPropertySource;
import io.micronaut.context.env.PropertySource;
import io.micronaut.core.async.SupplierUtil;
import io.micronaut.management.endpoint.info.InfoEndpoint;
import io.micronaut.management.endpoint.info.InfoSource;
import io.micronaut.runtime.context.scope.Refreshable;
import io.reactivex.Flowable;
import org.reactivestreams.Publisher;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

// Component is refreshable via a refresh event
@Refreshable
// Only create this component when the following 
// requirements are met:
@Requires(beans = InfoEndpoint.class)
@Requires(property = "endpoints.info.config.enabled", notEquals = "false")
public class DatabaseInfoSource implements InfoSource {

    /**
     * Repository to fetch count of conferences in the database.
     */
    private final ConferenceRepository conferenceRepository;

    /**
     * Supplier for returning result.
     */
    private final Supplier<PropertySource> supplier;

    public DatabaseInfoSource(final ConferenceRepository conferenceRepository) {
        this.conferenceRepository = conferenceRepository;
        
        // Cache result from database.
        // This component is refreshable via 
        // a refresh event to get the latest value
        // from the database.
        this.supplier = SupplierUtil.memoized(this::retrieveDatabaseInfo);
    }

    /**
     * Return information.
     * 
     * @return Information about the database.
     */
    @Override
    public Publisher<PropertySource> getSource() {
        return Flowable.just(supplier.get());
    }

    /**
     * Get information from the database via the {@link #conferenceRepository} instance.
     * 
     * @return Number of conferences in the database.
     */
    private MapPropertySource retrieveDatabaseInfo() {
        final Map<String, Long> databaseInfo = new HashMap<>();
        databaseInfo.put("db.conferences.count", conferenceRepository.count());
        return new MapPropertySource("database", databaseInfo);
    }

}

Let's run our Micronaut application and invoke the /info endpoint. We see the following response if we have 42 conference records in our database:

{
    "db": {
        "conferences": {
            "count": 42
        }
    },
    "sample": {
        "message": "Micronaut is awesome"
    }
}

Written with Micronaut 1.0.0.M4.

August 20, 2018

Micronaut Mastery: Add Custom Health Indicator

When 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 as well 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 that can be used as base class for writing custom health indicators.

First we must add the io.micronaut:management dependency to our compile class path, 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 a 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.

August 17, 2018

Micronaut Mastery: Add Build Info To Info Endpoint

Micronaut has some built-in management endpoints to get information, a list of beans, health checks and more. To enable the endpoints we must add the dependency io.micronaut:management to our application. Then we can add configuration properties to enable the different endpoints. The /info endpoint gathers information from several sources with properties. If we want to add build information we must create a file build-info.properties with information and Micronaut will automatically add the properties from the file to the /info endpoint.

We can choose how we want to create the build-info.properties file. The location is configurable via Micronaut application configuration properties, but the default location is on the classpath at META-INF/build-info.properties. To make life easy for us we reuse the BuildInfo Gradle task from the Spring Boot Gradle plugin to create the build-info.properties file.

In the following example build file we add a build script classpath dependency on the Spring Boot Gradle plugin. This will add the BuildInfo class to our Gradle build file. Next we create a new task buildInfo using the BuildInfo type and we set the destination directory to the resources/main/META-INF directory in the project build directory. This is the default location that Micronaut uses to read the properties for the /info endpoint.

// File: build.gradle
buildscript {
   ...
   dependencies {
       ...
       classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.4.RELEASE"
       ...
    }
}
...
dependencies {
    ...
    // Add management endpoint support.
    runtime "io.micronaut:management"
    ...
}
...
import org.springframework.boot.gradle.tasks.buildinfo.BuildInfo

task buildInfo(type: BuildInfo) {
    description = 'Generates build-info.properties file.'

    group = BasePlugin.BUILD_GROUP

    destinationDir = new File(sourceSets.main.output.resourcesDir, 'META-INF')

    properties {
        time = null // Otherwise task is never up-to-date
        artifact = shadowJar.baseName
        
        // Extra properties that will be added to build-info.properties.
        additionalProperties = [
                by: System.properties['user.name'],
                operatingSystem: "${System.properties['os.name']} (${System.properties['os.version']})",
                continuousIntegration: System.getenv('CI') ? true: false,
                machine: InetAddress.localHost.hostName,
        ]
    }
}
classes.dependsOn buildInfo
...

In our application configuration we enable the /info endpoint:

# File: src/main/resources/application.yml
...
endpoints:
  info:
    enabled: true
    sensitive: false
...

We start our Micronaut application and invoke the /info endpoint and we get the following data:

{
    "build": {
        "artifact": "micronaut-sample",
        "by": "mrhaki",
        "continuous-integration": "false",
        "group": "mrhaki.micronaut",
        "machine": "mrhaki.fritz.box",
        "name": "micronaut-sample",
        "operating-system": "Mac OS X (10.13.6)",
        "version": "1.0.1"
    }
}

Written with Micronaut 1.0.0.M4.