Create a Micronaut Application to Collect Metrics

This guide describes how to create a Micronaut application that collects standard and custom metrics.

The application stores book information in a database 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. However, you can also download the completed example in Java:

A note regarding your development environment

Consider using Visual Studio Code that provides native support for developing applications with the Graal Cloud Native Tools extension.

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

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

1. Create the Application #

Create an application using the GCN Launcher.

  1. Open the GCN Launcher in advanced mode.

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

Alternatively, use the GCN CLI as follows:

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

For more information, see Using the GCN CLI.

When creating the application with GCN, 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 GCN 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, the following properties were added to the configuration file, src/main/resources/application.properties, as follows:

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 configuration. In this case all metrics are enabled. To disable, change to false, for example, per-environment.

1.2. Database Migration with Flyway #

To create the database schema, the application uses the Micronaut integration with Flyway. Flyway automates schema changes, significantly simplifying schema management tasks, such as migrating, rolling back, and reproducing in multiple environments.

  1. You specified Flyway as a project feature in the GCN Launcher, so the build file includes it as a dependency:

    build.gradle

    implementation("io.micronaut.flyway:micronaut-flyway")
    runtimeOnly("org.flywaydb:flyway-mysql")

    pom.xml

    <dependency>
        <groupId>io.micronaut.flyway</groupId>
        <artifactId>micronaut-flyway</artifactId>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.flywaydb</groupId>
        <artifactId>flyway-mysql</artifactId>
        <scope>runtime</scope>
    </dependency>
  2. The file src/main/resources/application.properties was modified to enable Flyway to perform migrations on the default datasources, by adding the following:

     flyway.datasources.default.enabled=true
    

    Configuring multiple datasources is as simple. You can also specify directories that will be used for migrating each datasource. For more information, see Micronaut integration with Flyway.

  3. The GCN Launcher created a migration file in the src/main/resources/db/migration directory named V1__schema.sql to contain the database schema, as follows:

    DROP TABLE IF EXISTS book;
    
    CREATE TABLE book (
        id   BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,
       name  VARCHAR(255) NOT NULL UNIQUE,
       isbn  CHAR(13) NOT NULL UNIQUE
    );
    
    INSERT INTO book (isbn, name)
    VALUES ("9781491950357", "Building Microservices"),
           ("9781680502398", "Release It!"),
           ("9780321601919", "Continuous Delivery"),
           ("9781617294549", "Microservices Patterns");
    

Flyway migration is automatically triggered before your application starts and Flyway reads migration files from the src/main/resources/db/migration/ directory. During application startup, Flyway runs the commands in the SQL file and creates the schema needed for the application.

Using the SQL statements in V1__schema.sql, Flyway creates a table with the name book and populates it with four records.

1.3. Domain Entity #

The GCN Launcher created a Book domain class that uses Micronaut Data JDBC in a file named src/main/java/com/example/Book.java, as follows:

package com.example;

import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;

import static io.micronaut.data.annotation.GeneratedValue.Type.AUTO;

import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.Size;

@Serdeable
@MappedEntity // <1>
public class Book {

    @Id
    @GeneratedValue(AUTO)
    private Long id;

    private String name;

    @Size(min=13, max=13)
    private String isbn;

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }
}

1 The annotation @MappedEntity maps the class to the table defined in the schema.

1.4. Repository Interface #

A repository interface defines the operations to access the database. Micronaut Data implements the interface at compilation time. The GCN Launcher created a BookRepository interface in a file named src/main/java/com/example/BookRepository.java:

package com.example;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.repository.CrudRepository;
import jakarta.validation.constraints.NotBlank;

import java.util.Optional;

import static io.micronaut.data.model.query.builder.sql.Dialect.MYSQL;

@JdbcRepository(dialect = MYSQL) // <1>
public interface BookRepository extends CrudRepository<Book, Long> { // <2>

    @NonNull
    Optional<Book> findByIsbn(@NotBlank String isbn);
}

1 A database dialect is specified with the @JdbcRepository annotation.

2 The Book is the root entity, and the primary key type is Long.

1.5. Controller Class #

The GCN Launcher created a controller to access Book instances (and to trigger the JDBC metric data) in a file named 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.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micrometer.core.annotation.Timed;
import io.micrometer.core.annotation.Counted;

import java.util.Optional;

@Controller("/books") // <1>
@ExecuteOn(TaskExecutors.IO)
class BookController {

    private final BookRepository bookRepository;

    BookController(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

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

    @Get("/{isbn}") // <3>
    @Counted("books.find") // <4>
    Optional<Book> findBook(String isbn) {
        return bookRepository.findByIsbn(isbn);
    }
}

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

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

3 The controller maps a GET request to /books, which returns a list of Book.

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

2. Create Tests #

Note: You require a running Docker container to run the tests.

  1. For tests to run correctly, the GCN Launcher created a test configuration file named src/test/resources/application-test.properties with the following contents:

     customMetrics.initialDelay=10h
     datasources.default.db-type=mysql
     datasources.default.dialect=MYSQL
     datasources.default.driverClassName=com.mysql.cj.jdbc.Driver
     flyway.datasources.default.enabled=true
     micronaut.metrics.enabled=true
     micronaut.metrics.export.cloudwatch.enabled=false
     micronaut.metrics.export.oraclecloud.enabled=false
    

    The configuration enables the metrics and specifies a MySQL datasource. Since it does not specify a URL for the source, Micronaut Test Resources automatically creates a test container for the database. The configuration also enables Flyway to create the books tables the same way as in a production environment.

  2. The GCN Launcher created a test class, named BookControllerMetricsTest, to verify metrics functionality in a file named 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"));
             assertTrue(names.contains("hikaricp.connections.max"));
    
             // 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"));
             assertTrue(names.contains("hikaricp.connections.max"));
         }
    
         @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. The GCN Launcher created a service that retrieves information from the database and publishes custom metrics based on it. The custom metrics provide the number of books about microservices. The service is in a file named 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 BookRepository bookRepository;
         private final Counter checks;
         private final Timer time;
         private final AtomicInteger microserviceBooksNumber = new AtomicInteger(0);
    
         private static final String SEARCH_KEY = "microservice";
    
         MicroserviceBooksNumberService(BookRepository bookRepository,
                                        MeterRegistry meterRegistry) { // <1>
             this.bookRepository = bookRepository;
             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 = bookRepository.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 database, 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. The GCN Launcher created MicroserviceBooksNumberTest to test the custom metrics in a file named 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 Flyway schema 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 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 test

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

./mvnw test

5. Run the Application #

The GCN Launcher configured an initial datasource in src/main/resources/application.properties with the following properties:

datasources.default.db-type=mysql
datasources.default.dialect=MYSQL
datasources.default.driverClassName=com.mysql.cj.jdbc.Driver

You can set values for the missing datasources.default.url, datasources.default.username, and datasources.default.password properties with these environment variables:

export DATASOURCES_DEFAULT_URL=jdbc:mysql://<Datasource URL, IP, or localhost>:3306/gcn
export DATASOURCES_DEFAULT_USERNAME=<Username>
export DATASOURCES_DEFAULT_PASSWORD=<Password>
set DATASOURCES_DEFAULT_URL=jdbc:mysql://<Datasource URL, IP, or localhost>:3306/gcn
set DATASOURCES_DEFAULT_USERNAME=<Username>
set DATASOURCES_DEFAULT_PASSWORD=<Password>
$ENV:DATASOURCES_DEFAULT_URL = "jdbc:mysql://<Datasource URL, IP, or localhost>:3306/gcn"
$ENV:DATASOURCES_DEFAULT_USERNAME = "<Username>"
$ENV:DATASOURCES_DEFAULT_PASSWORD = "<Password>"

This approach requires an existing database, or requires you to manually start a server in a Docker container. Instead, to simplify things, do not set the environment variables: if you do not specify a datasource URL, Micronaut Test Resources automatically starts a MySQL server in a Docker container when running the application locally or running tests.

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

./gradlew 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 run --args="-customMetrics.updateFrequency=5s"
./mvnw mn: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:

./mvnw mn:run -Dmn.appArgs="-customMetrics.updateFrequency=5s"

Send a few test requests with curl, as follows:

  • Get all the books:
    curl localhost:8080/books
    
    [{"id": 1, "name": "Building Microservices", "isbn": "9781491950357"},
    {"id": 2, "name": "Release It!", "isbn": "9781680502398"},
    {"id": 3, "name": "Continuous Delivery", "isbn": "9780321601919"}]
    
  • Get a book by its ISBN:
    curl localhost:8080/books/9781680502398
    
    {"id": 2, "name": "Release It!", "isbn": "9781680502398"}
    
  • Get a list of all the available metrics:
    curl localhost:8080/metrics
    
    {"names": [
      "books.find",
      "books.index",
      ...,
      "microserviceBooksNumber.latest",
      "microserviceBooksNumber.time",
      ...,
      "http.server.requests",
      "process.uptime",
      ...
    ]}
    
  • Get the value of a particular metric:
    curl localhost:8080/metrics/http.server.requests
    
    {"name": "http.server.requests",
    "measurements": [
        {"statistic": "COUNT", "value": 3.0},
        {"statistic": "TOTAL_TIME", "value": 0.6045995000000001},
        {"statistic": "MAX", "value": 0.0343463}
    ], ...
    }
    
  • 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": 1.0},
        {"statistic": "TOTAL_TIME", "value": 0.3218636},
        {"statistic":"MAX","value":0.0}
    ], ...
    }
    
  • 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.0}]
    }
    

6. Generate a Native Executable using GraalVM #

GCN supports compiling Java applications ahead-of-time into native executables using GraalVM Native Image and Native Build Tools. Packaged as a native executable, it significantly reduces the application startup time and memory footprint.

6.1. Build the Native Executable #

To generate a native executable, run the following command:

./gradlew nativeCompile
./mvnw package -Dpackaging=native-image

6.2. Run the Native Executable #

To start the native executable, run a Docker container with a MySQL database:

docker run -it --rm \
    --name "mysql.8" \
    -p 3306:3306 \
    -e MYSQL_DATABASE=gcn \
    -e MYSQL_USER=guide_user \
    -e MYSQL_PASSWORD=User123User! \
    -e MYSQL_ALLOW_EMPTY_PASSWORD=true \
    mysql:8

Set environment variables to provide values for the missing datasource url, username, and password properties:

export DATASOURCES_DEFAULT_URL=jdbc:mysql://localhost:3306/gcn
export DATASOURCES_DEFAULT_USERNAME=guide_user
export DATASOURCES_DEFAULT_PASSWORD=User123User!

Note: On Windows use set instead of the export keyword in the commands above.

Start the generated native executable:

./build/native/nativeCompile/metrics-demo
./target/metrics-demo

Note: With Gradle you can run ./gradlew nativeRun to start the native executable in development mode, which uses Micronaut Test Resources and therefore doesn’t require you to start a MySQL server.

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

To stop your container run the following command:

docker stop mysql.8

Summary #

This guide demonstrated how to create an application that collects standard and custom metrics, and to package and run this application as a native executable.