Create a Micronaut Application to Collect Metrics and Monitor Them on Oracle Cloud Infrastructure

This guide describes how to use the Graal Development Kit for Micronaut (GDK) to create a Micronaut® application that collects standard and custom metrics, then publishes and monitors them on Oracle Cloud Infrastructure Monitoring.

Oracle Cloud Infrastructure Monitoring enables you to actively and passively monitor your application and cloud resources using the Metrics and Alarms features.

The application stores book information in memory and provides endpoints to query books. The application collects metrics for the whole application and measures total computation time for a particular endpoint. The application uses Micronaut Micrometer to expose application metric data with Micrometer.

Prerequisites #

Follow the steps below to create the application from scratch.

A note regarding your development environment

Consider using Visual Studio Code, which provides native support for developing applications with the Graal Development Kit extension.

Note: If you use IntelliJ IDEA, enable annotation processing.

Windows platform: The GDK guides are compatible with Gradle only. Maven support is coming soon.

1. Create the Application #

Create an application using the GDK Launcher.

  1. Open the GDK Launcher in advanced mode.

  2. Create a new project using the following selections.
    • Project Type: Application (Default)
    • Project Name: oci-metrics-demo
    • Base Package: com.example (Default)
    • Clouds: OCI
    • Build Tool: Gradle (Groovy) or Maven
    • Language: Java (Default)
    • Test Framework: JUnit (Default)
    • Java Version: 17 (Default)
    • Micronaut Version: (Default)
    • Cloud Services: Metrics
    • Features: GraalVM Native Image, and Micrometer Annotation
    • Sample Code: No
  3. Click Generate Project, then click Download Zip. The GDK Launcher creates a Java project with the default package com.example in a directory named oci-metrics-demo. The application ZIP file will be downloaded to your default downloads directory. Unzip it, open it in your code editor, and proceed to the next steps.

Alternatively, use the GDK CLI as follows:

gdk create-app com.example.oci-metrics-demo \
    --clouds=oci \
    --services=metrics \
    --features=graalvm,micrometer-annotation \
    --build=gradle \
    --jdk=17 \
    --lang=java \
    --example-code=false
gdk create-app com.example.oci-metrics-demo \
    --clouds=oci \
    --services=metrics \
    --features=graalvm,micrometer-annotation \
    --build=maven \
    --jdk=17 \
    --lang=java \
    --example-code=false

For more information, see Using the GDK CLI.

The GDK Launcher creates a multi-module project with two subprojects: oci, and lib. The common application logic is in the lib subproject, and the oci subproject contain logic specific to the Oracle Cloud Infrastructure platform. When creating the application with the GDK, the necessary features such as the Logback framework, Micronaut Micrometer, and others were added for you. These features will be used in the guide.

1.1. Configure Metrics Collection #

The project generated by the GDK Launcher has Micrometer as a dependency.

Micrometer provides a simple facade over the instrumentation clients for a number of popular monitoring systems.

To configure Micrometer, add the following properties to the application configuration file, oci/src/main/resources/application.properties:

micronaut.metrics.enabled=true
micronaut.metrics.binders.files.enabled=true
micronaut.metrics.binders.jdbc.enabled=true
micronaut.metrics.binders.jvm.enabled=true
micronaut.metrics.binders.logback.enabled=true
micronaut.metrics.binders.processor.enabled=true
micronaut.metrics.binders.uptime.enabled=true
micronaut.metrics.binders.web.enabled=true

Several groups of metrics are enabled by default: these include system metrics (such as JVM information and uptime), as well as metrics tracking web requests, datasources activity, and others. Overall metrics can be enabled or disabled, and groups can be individually enabled or disabled in this configuration file. In this case all metrics are enabled. To disable, change to false, for example, per-environment.

1.2. Book Domain Class #

Create a Book domain class in a file named lib/src/main/java/com/example/Book.java, as follows:

package com.example;

import io.micronaut.core.annotation.Creator;
import io.micronaut.serde.annotation.Serdeable;

import java.util.Objects;

@Serdeable
public class Book {

    private final String isbn;
    private final String name;

    @Creator
    public Book(String isbn, String name) {
        this.isbn = isbn;
        this.name = name;
    }

    public String getIsbn() {
        return isbn;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Book{" +
                "isbn='" + isbn + '\'' +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book other = (Book) o;
        return Objects.equals(isbn, other.isbn) &&
                Objects.equals(name, other.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(isbn, name);
    }
}

1.3. BookService #

To keep this guide simple there is no database persistence: the Books microservice keeps the list of books in memory. Create a class named BookService in lib/src/main/java/com/example/BookService.java with the following contents:

package com.example;

import jakarta.annotation.PostConstruct;
import jakarta.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Singleton
public class BookService {

    private final List<Book> bookStore = new ArrayList<>();

    @PostConstruct
    void init() {
        bookStore.add(new Book("9781491950357", "Building Microservices"));
        bookStore.add(new Book("9781680502398", "Release It!"));
        bookStore.add(new Book("9780321601919", "Continuous Delivery"));
        bookStore.add(new Book("9781617294549", "Microservices Patterns"));
    }

    public List<Book> findAll() {
        return bookStore;
    }

    public Optional<Book> findByIsbn(String isbn) {
        return bookStore.stream()
                .filter(b -> b.getIsbn().equals(isbn))
                .findFirst();
    }
}

1.4. BookController #

Create a controller to access Book instances (and to trigger the metric data) in a file named lib/src/main/java/com/example/BookController.java with the following contents:

package com.example;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micrometer.core.annotation.Timed;
import io.micrometer.core.annotation.Counted;

import java.util.Optional;

@Controller("/books") // <1>
class BookController {

    private final BookService bookService;

    BookController(BookService bookService) { // <2>
        this.bookService = bookService;
    }

    @Get // <3>
    @Timed("books.index") // <4>
    Iterable<Book> index() {
        return bookService.findAll();
    }

    @Get("/{isbn}") // <5>
    @Counted("books.find") // <6>
    Optional<Book> findBook(String isbn) {
        return bookService.findByIsbn(isbn);
    }
}

1 The class is defined as a controller with the @Controller annotation mapped to the path /books.

2 Use constructor injection to inject a bean of type BookService.

3 The @Get annotation maps the findAll method to an HTTP GET request on /books.

4 Use a Micrometer @Timed annotation, with the value “books.index”, to create a timer metric.

5 The @Get annotation maps the findBook method to an HTTP GET request on /books/{isbn}.

6 Use a Micrometer @Counted annotation, with the value “books.find”, to create a counter metric.

2. Create Tests #

  1. For tests to run correctly, edit the test configuration file named oci/src/test/resources/application-test.properties so that it contains the following:

     customMetrics.initialDelay=1h
     micronaut.metrics.enabled=true
     micronaut.metrics.export.cloudwatch.enabled=false
     micronaut.metrics.export.oraclecloud.enabled=false
    
  2. Create a test class, named BookControllerMetricsTest, to verify metrics functionality in a file named oci/src/test/java/com/example/BookControllerMetricsTest.java with the following contents:

     package com.example;
    
     import io.micrometer.core.instrument.Counter;
     import io.micrometer.core.instrument.MeterRegistry;
     import io.micrometer.core.instrument.Tags;
     import io.micrometer.core.instrument.Timer;
     import io.micronaut.core.type.Argument;
     import io.micronaut.http.HttpRequest;
     import io.micronaut.http.client.HttpClient;
     import io.micronaut.http.client.annotation.Client;
     import io.micronaut.logging.LoggingSystem;
     import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
     import jakarta.inject.Inject;
     import org.junit.jupiter.api.Test;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
    
     import java.util.List;
     import java.util.Map;
     import java.util.Set;
     import java.util.stream.Collectors;
     import java.util.concurrent.TimeUnit;
    
     import static io.micronaut.logging.LogLevel.ALL;
     import static org.junit.jupiter.api.Assertions.assertEquals;
     import static org.junit.jupiter.api.Assertions.assertFalse;
     import static org.junit.jupiter.api.Assertions.assertTrue;
    
     @MicronautTest
     class BookControllerMetricsTest {
    
         @Inject
         MeterRegistry meterRegistry;
    
         @Inject
         LoggingSystem loggingSystem;
    
         @Inject
         @Client("/")
         HttpClient httpClient;
    
         @Test
         void testExpectedMeters() {
    
             Set<String> names = meterRegistry.getMeters().stream()
                     .map(meter -> meter.getId().getName())
                     .collect(Collectors.toSet());
    
             // check that a subset of expected meters exist
             assertTrue(names.contains("jvm.memory.max"));
             assertTrue(names.contains("process.uptime"));
             assertTrue(names.contains("system.cpu.usage"));
             assertTrue(names.contains("logback.events"));
    
             // these will be lazily created
             assertFalse(names.contains("http.client.requests"));
             assertFalse(names.contains("http.server.requests"));
         }
    
         @Test
         void testHttp() {
    
             Timer timer = meterRegistry.timer("http.server.requests", Tags.of(
                     "exception", "none",
                     "method", "GET",
                     "status", "200",
                     "uri", "/books"));
             assertEquals(0, timer.count());
    
             Timer bookIndexTimer = meterRegistry.timer("books.index",
                     Tags.of("exception", "none"));
             assertEquals(0, bookIndexTimer.count());
    
             httpClient.toBlocking().retrieve(
                     HttpRequest.GET("/books"),
                     Argument.listOf(Book.class));
    
             assertEquals(1, timer.count());
             assertEquals(1, bookIndexTimer.count());
             assertTrue(0.0 < bookIndexTimer.totalTime(TimeUnit.MILLISECONDS));
             assertTrue(0.0 < bookIndexTimer.max(TimeUnit.MILLISECONDS));
    
             Counter bookFindCounter = meterRegistry.counter("books.find",
                     Tags.of("result", "success",
                             "exception", "none"));
             assertEquals(0, bookFindCounter.count());
    
             httpClient.toBlocking().retrieve(
                     HttpRequest.GET("/books/9781491950357"),
                     Argument.of(Book.class));
    
             assertEquals(1, bookFindCounter.count());
         }
    
         @Test
         void testLogback() {
    
             Counter counter = meterRegistry.counter("logback.events", Tags.of("level", "info"));
             double initial = counter.count();
    
             Logger logger = LoggerFactory.getLogger("testing.testing");
             loggingSystem.setLogLevel("testing.testing", ALL);
    
             logger.trace("trace");
             logger.debug("debug");
             logger.info("info");
             logger.warn("warn");
             logger.error("error");
    
             assertEquals(initial + 1, counter.count(), 0.000001);
         }
    
         @Test
         void testMetricsEndpoint() {
    
             Map<String, Object> response = httpClient.toBlocking().retrieve(
                     HttpRequest.GET("/metrics"),
                     Argument.mapOf(String.class, Object.class));
    
             assertTrue(response.containsKey("names"));
             assertTrue(response.get("names") instanceof List);
    
             List<String> names = (List<String>) response.get("names");
    
             // check that a subset of expected meters exist
             assertTrue(names.contains("jvm.memory.max"));
             assertTrue(names.contains("process.uptime"));
             assertTrue(names.contains("system.cpu.usage"));
             assertTrue(names.contains("logback.events"));
         }
    
         @Test
         void testOneMetricEndpoint() {
    
             Map<String, Object> response = httpClient.toBlocking().retrieve(
                     HttpRequest.GET("/metrics/jvm.memory.used"),
                     Argument.mapOf(String.class, Object.class));
    
             String name = (String) response.get("name");
             assertEquals("jvm.memory.used", name);
    
             List<Map<String, Object>> measurements = (List<Map<String, Object>>) response.get("measurements");
             assertEquals(1, measurements.size());
    
             double value = (double) measurements.get(0).get("value");
             assertTrue(value > 0);
         }
     }
    

    The tests verify that certain metrics are present, including the ones that you enabled in the application configuration file, and the ones that you collected thanks to the @Counter and @Timer annotations.

    Note that, since the @MicronautTest annotation is used, Micronaut initializes the application context and the embedded server with the endpoints you created earlier. (For more information, see the Micronaut Test guide.)

3. Create Custom Metrics #

  1. Create a service that retrieves information from the BookService and publishes custom metrics based on it. The custom metrics provide the number of books about microservices. Create the custom metrics service in a file named lib/src/main/java/com/example/MicroserviceBooksNumberService.java with the following contents:

     package com.example;
    
     import io.micrometer.core.instrument.Counter;
     import io.micrometer.core.instrument.MeterRegistry;
     import io.micrometer.core.instrument.Timer;
     import io.micronaut.scheduling.annotation.Scheduled;
     import jakarta.inject.Singleton;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
    
     import java.util.concurrent.atomic.AtomicInteger;
     import java.util.stream.StreamSupport;
    
     @Singleton
     public class MicroserviceBooksNumberService {
    
         private final Logger log = LoggerFactory.getLogger(getClass().getName());
    
         private final BookService bookService;
         private final Counter checks;
         private final Timer time;
         private final AtomicInteger microserviceBooksNumber = new AtomicInteger(0);
    
         private static final String SEARCH_KEY = "microservice";
    
         MicroserviceBooksNumberService(BookService bookService,
                                        MeterRegistry meterRegistry) { // <1>
             this.bookService = bookService;
             checks = meterRegistry.counter("microserviceBooksNumber.checks");
             time = meterRegistry.timer("microserviceBooksNumber.time");
             meterRegistry.gauge("microserviceBooksNumber.latest", microserviceBooksNumber);
         }
    
         @Scheduled(fixedRate = "${customMetrics.updateFrequency:1h}",
                    initialDelay = "${customMetrics.initialDelay:0s}") // <2>
         public void updateNumber() {
             time.record(() -> {
                 try {
                     Iterable<Book> allBooks = bookService.findAll();
                     long booksNumber = StreamSupport.stream(allBooks.spliterator(), false)
                             .filter(b -> b.getName().toLowerCase().contains(SEARCH_KEY))
                             .count();
    
                     checks.increment();
                     microserviceBooksNumber.set((int) booksNumber);
                 } catch (Exception e) {
                     log.error("Problem setting the number of microservice books", e);
                 }
             });
         }
     }
    

    1 The code registers custom meters, queries the BookService, counts books containing microservice in the name and updates the microserviceBooksNumber.latest meter with the value. microserviceBooksNumber.checks stores the number of updates performed, and microserviceBooksNumber.time stores the total time spent on the updates for the metric.

    2 The metric update is scheduled. The customMetrics.updateFrequency parameter corresponds to the update rate and has the default value of one hour. The customMetrics.initialDelay parameter corresponds to a delay after application startup before metrics calculation and has a default value of zero seconds.

  2. Create a test for the custom metrics in a class named MicroserviceBooksNumberTest. Create a new file named oci/src/test/java/com/example/MicroserviceBooksNumberTest.java with the following contents:

     package com.example;
    
     import io.micrometer.core.instrument.Counter;
     import io.micrometer.core.instrument.Gauge;
     import io.micrometer.core.instrument.MeterRegistry;
     import io.micrometer.core.instrument.Timer;
     import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
     import jakarta.inject.Inject;
     import org.junit.jupiter.api.Test;
    
     import static java.util.concurrent.TimeUnit.MILLISECONDS;
     import static org.junit.jupiter.api.Assertions.assertEquals;
     import static org.junit.jupiter.api.Assertions.assertTrue;
    
     @MicronautTest
     class MicroserviceBooksNumberTest {
    
         @Inject
         MeterRegistry meterRegistry;
    
         @Inject
         MicroserviceBooksNumberService service;
    
         @Test
         void testMicroserviceBooksNumberUpdates() {
             Counter counter = meterRegistry.counter("microserviceBooksNumber.checks");
             Timer timer = meterRegistry.timer("microserviceBooksNumber.time");
             Gauge gauge = meterRegistry.get("microserviceBooksNumber.latest").gauge();
    
             assertEquals(0.0, counter.count());
             assertEquals(0.0, timer.totalTime(MILLISECONDS));
             assertEquals(0.0, gauge.value());
    
             int checks = 3;
             for (int i = 0; i < checks; i++) { // <1>
                 service.updateNumber();
             }
    
             assertEquals((double) checks, counter.count());
             assertTrue(timer.totalTime(MILLISECONDS) > 0);
             assertEquals(2.0, gauge.value());
         }
     }
    

    1 The test calls the service three times and verifies that the metrics are collected correctly. Because the BookService added two books with titles containing the word “microservices”, the value of microserviceBooksNumber.latest is 2.0.

    To make sure that the test works as expected and only three updates of the metric are performed, change the test configuration (in the file oci/src/test/resources/application-test.properties) so that it does not perform scheduled updates for the custom metric. To achieve this, change the customMetrics.initialDelay parameter to a large value, such as 10 hours. For example:

     customMetrics.initialDelay=10h
    

4. Run the Tests #

To run the tests, use the following command:

./gradlew :oci:test

Then open the file oci/build/reports/tests/test/index.html in a browser to view the results.

./mvnw install -pl lib -am && ./mvnw test -pl oci

The tests could be run locally without any cloud credentials, as metrics are not exported to the cloud.

5. Set Up Oracle Cloud Infrastructure #

  1. Configure Oracle Cloud Infrastructure Authentication.

    If you haven’t created an API-key and configuration file, run the following command:

     oci setup bootstrap
    

    The command will open your browser for authentication. When asked, enter your region (for example, ca-toronto-1 for Toronto, Canada) and a name for the profile (for example, oci_metrics_demo_profile).

    Note: For more information, see Oracle Cloud Infrastructure SDK Authentication Methods and Setting up the Configuration File.

    Use the credentials in the configuration file oci/src/main/resources/application.properties, as follows:

     oci.config.profile=oci_metrics_demo_profile
    

    Provide the profile name. If you didn’t create a custom profile name, use DEFAULT. Use the oci.config.path property if you need to specify the path to the configuration file.

  2. To export metrics to Oracle Cloud Infrastructure, enable Oracle Cloud Infrastructure as an export location for Micrometer in the application configuration file. In addition, set the namespace property for your metrics to something meaningful, for example, an application name such as ocimetricsdemo. The namespace groups the metrics in Oracle Cloud Infrastructure Metrics.

Edit the file oci/src/main/resources/application.properties so that it includes the following:

micronaut.metrics.enabled=true
micronaut.metrics.export.oraclecloud.enabled=true
micronaut.metrics.export.oraclecloud.namespace=ocimetricsdemo

By default, metrics are published in your root compartment (also known as your “tenancy”). If you want to specify in which compartment metrics should be published, you can insert a compartmentId property inside your configuration file. The full key is micronaut.metrics.export.oraclecloud.compartmentId.

6. Run the Application #

  1. To run the application, use the following command, which starts the application on port 8080.

    ./gradlew :oci:run

    Alternatively, to cause the custom metric update to occur more frequently (to see the effects on metrics), start the application with a configuration override to update every five seconds, as follows:

    ./gradlew :oci:run --args="-customMetrics.updateFrequency=5s"
    ./mvnw install -pl lib -am
    ./mvnw mn:run -pl oci

    Alternatively, to cause the custom metric update to occur more frequently (to see the effects on metrics), start the application with a configuration override to update every five seconds, as follows:

    ./mvnw install -pl lib -am
    ./mvnw mn:run -pl oci -Dmn.appArgs="-customMetrics.updateFrequency=5s"
  2. Send a few test requests with curl, as follows:

    • Get all the books:
      curl localhost:8080/books
      
      [
        {
          "isbn": "9781491950357",
          "name": "Building Microservices"
        },
        {
          "isbn": "9781680502398",
          "name": "Release It!"
        },
        {
          "isbn": "9780321601919",
          "name": "Continuous Delivery"
        },
        {
          "isbn": "9781617294549",
          "name": "Microservices Patterns"
        }
      ]
      
    • Get the value of the metric that you created on the books endpoint:
      curl localhost:8080/metrics/books.index
      
      {
        "name": "books.index",
        "measurements": [
          {
            "statistic": "COUNT",
            "value": 0
          },
          {
            "statistic": "TOTAL_TIME",
            "value": 0
          },
          {
            "statistic": "MAX",
            "value": 0.765111
          }
        ]
        ...
      
    • Get the value of the custom metric calculating the number of books with microservice in their title:
      curl localhost:8080/metrics/microserviceBooksNumber.latest
      
      {
        "name": "microserviceBooksNumber.latest",
        "measurements": [
          {
            "statistic": "VALUE",
            "value": 2
          }
        ]
      }
      

7. Monitor Metrics in Oracle Cloud Infrastructure Metrics Explorer #

Use the Oracle Cloud Infrastructure Monitoring Service’s Metrics Explorer UI to view the collected metrics.

  1. In the Oracle Cloud Console, open the navigation menu, click Observability & Management. Under Monitoring, click Metrics Explorer:

  2. In the query editor, create a query that you want to monitor. Specify the following properties:

    • Compartment: Select the root compartment from the drop-down list.
    • Metric namespace: From the drop-down list, select the namespace you provided in your application configuration file (for example, “ocimetricsdemo”).
    • Metric name: From the drop-down list, select a metric, such as “microserviceBooksNumber.latest”.
  3. Click Update Chart and the values of your selected metric are displayed in the Metrics Explorer:

    Metrics Explorer

8. Generate a Native Executable using GraalVM #

The GDK supports compiling Java applications ahead-of-time into native executables using GraalVM Native Image. You can use the Gradle plugin for GraalVM Native Image building/Maven plugin for GraalVM Native Image building. Packaged as a native executable, it significantly reduces application startup time and memory footprint.

Prerequisites: Make sure you have installed a GraalVM JDK. The easiest way to get started is with SDKMAN!. For other installation options, visit the Downloads section.

To generate a native executable, run the following command:

./gradlew :oci:nativeCompile

The native executable is created in the oci/build/native/nativeCompile/ directory and can be run with the following command:

oci/build/native/nativeCompile/oci-metrics-demo-oci
./mvnw install -pl lib -am
./mvnw clean package -pl oci -Dpackaging=native-image

The native executable is created in the oci/target/ directory and can be run with the following command:

oci/target/oci-metrics-demo-oci

9. Run and Test the Native Executable #

Run the native executable, and then perform the same curl commands from above to confirm that the application works the same way as before, but with faster startup and response times.

Then return to the Oracle Cloud Console to review the metrics in the Metrics Explorer.

Summary #

This guide demonstrated how to create a Micronaut application that collects standard and custom metrics, then publishes and monitors them on Oracle Cloud Infrastructure Monitoring. The application uses Micronaut Micrometer to expose application metric data with Micrometer.