Providing metrics from a microservice

duration 15 minutes

You’ll explore how to provide system and application metrics from a microservice with MicroProfile Metrics.

What you’ll learn

You will learn how to use MicroProfile Metrics to provide metrics from a microservice. You can monitor metrics to determine the performance and health of a service. You can also use them to pinpoint issues, collect data for capacity planning, or to decide when to scale a service to run with more or fewer resources.

The application that you will work with is an inventory service that stores information about various systems. The inventory service communicates with the system service on a particular host to retrieve its system properties when necessary.

You will use annotations provided by MicroProfile Metrics to instrument the inventory service to provide application-level metrics data. You will add counter, gauge, and timer metrics to the service.

You will also check well-known REST endpoints that are defined by MicroProfile Metrics to review the metrics data collected. Monitoring agents can access these endpoints to collect metrics.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone https://github.com/openliberty/guide-microprofile-metrics.git
cd guide-microprofile-metrics

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project, which is what you will build.

Try what you’ll build

The finish directory contains the finished implementation for the application. You can try it out before you build your own.

To try the application, navigate to the finish directory and run the following command:

mvn install liberty:start-server

Maven builds the application and runs it inside Open Liberty.

Point your browser to the http://localhost:9080/inventory/systems URL to access the inventory service. Because you just started the application, the inventory is currently empty. Then, access the http://localhost:9080/inventory/systems/localhost URL to add the localhost into the inventory.

Access the inventory service at the http://localhost:9080/inventory/systems URL at least once for application metrics to be collected. Otherwise, the metrics will not appear.

Next, point your browser to the https://localhost:9443/metrics MicroProfile Metrics endpoint. Log in as the admin user with adminpwd as the password. You can see both the system and application metrics in a text format.

To see only the application metrics, point your browser to https://localhost:9443/metrics/application.

See the following sample outputs for the @Timed, @Gauge, and @Counted metrics:

# TYPE application:inventory_properties_request_time_rate_per_second gauge
application:inventory_properties_request_time_rate_per_second 0.012564862395324394
# HELP application:inventory_properties_request_time_seconds Time needed to get the properties of a system from the given hostname
# TYPE application:inventory_size_guage gauge
# HELP application:inventory_size_guage Number of systems in the inventory
application:inventory_size_guage 1
# TYPE application:inventory_access_count counter
# HELP application:inventory_access_count Number of times the list of systems method is requested
application:inventory_access_count 1

To see only the system metrics, point your browser to https://localhost:9443/metrics/base.

See the following sample output for the @Gauge and @Counted metrics:

# TYPE base:jvm_uptime_seconds gauge
# HELP base:jvm_uptime_seconds Displays the start time of the Java virtual machine in milliseconds. This attribute displays the approximate time when the Java virtual machine started.
base:jvm_uptime_seconds 39.025
# TYPE base:classloader_current_loaded_class_count counter
# HELP base:classloader_current_loaded_class_count Displays the number of classes that are currently loaded in the Java virtual machine.
base:classloader_current_loaded_class_count 9144

When you’re done with the application, stop the Open Liberty server with the following command:

mvn liberty:stop-server

Adding MicroProfile Metrics to the inventory service

Navigate to the start directory to begin.

The MicroProfile Metrics API was added as a dependency to your pom.xml file. Look for the dependency with the mpMetrics artifact ID. Add this dependency to use MicroProfile Metrics API in your code to provide metrics from your microservices.

Next, create a src/main/liberty/config/server.xml file:

<server description="Sample Liberty server">

  <featureManager>
    <feature>jaxrs-2.1</feature>
    <feature>jsonp-1.1</feature>
    <feature>cdi-2.0</feature>
    <feature>mpMetrics-1.1</feature>
    <feature>mpRestClient-1.1</feature>
  </featureManager>

  <applicationManager autoExpand="true" />
  <quickStartSecurity userName="admin" userPassword="adminpwd"/>
  <keyStore id="defaultKeyStore" password="mpKeystore"/>
  <httpEndpoint host="*" httpPort="${default.http.port}" httpsPort="${default.https.port}" id="defaultHttpEndpoint"/>
  <webApplication location="microprofile-metrics.war" contextRoot="/"/>
</server>

The mpMetrics feature enables MicroProfile Metrics support in Open Liberty. Note that this feature requires SSL and the configuration has been provided for you.

The quickStartSecurity and keyStore configuration elements provide basic security to secure the server. When you visit the /metrics endpoint, use the credentials defined in the server configuration to log in to view the data.

Adding the annotations

Create the InventoryManager class in the src/main/java/io/openliberty/guides/inventory/InventoryManager.java file:

package io.openliberty.guides.inventory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

import io.openliberty.guides.inventory.model.InventoryList;
import io.openliberty.guides.inventory.model.SystemData;

import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.metrics.annotation.Counted;
import org.eclipse.microprofile.metrics.annotation.Timed;
import org.eclipse.microprofile.metrics.annotation.Gauge;
import org.eclipse.microprofile.metrics.MetricUnits;

@ApplicationScoped
public class InventoryManager {

  private List<SystemData> systems = Collections.synchronizedList(new ArrayList<>());
  private InventoryUtils invUtils = new InventoryUtils();

  @Timed(name = "inventoryPropertiesRequestTime", absolute = true,
    description = "Time needed to get the properties of a system from the given hostname")
  public Properties get(String hostname) {
    return invUtils.getProperties(hostname);
  }

  public void add(String hostname, Properties systemProps) {
    Properties props = new Properties();
    props.setProperty("os.name", systemProps.getProperty("os.name"));
    props.setProperty("user.name", systemProps.getProperty("user.name"));

    SystemData host = new SystemData(hostname, props);
    if (!systems.contains(host))
      systems.add(host);
  }


  @Counted(name = "inventoryAccessCount", absolute = true, monotonic = true,
    description = "Number of times the list of systems method is requested")
  public InventoryList list() {
    return new InventoryList(systems);
  }

  @Gauge(unit = MetricUnits.NONE, name = "inventorySizeGuage", absolute = true,
    description = "Number of systems in the inventory")
  public int getTotal() {
    return systems.size();
  }

}

Apply the @Timed annotation to the get() method to track how frequently the method is invoked and also how long it takes for the invocation to complete. This annotation has these metadata fields:

name

Optional. Use this field to name of the metric.

description

Optional. Use this field to describe the purpose of the metric.

absolute

Optional. Use this field to determine whether the metric name is the exact name that is specified in the name field or that is specified with the package prefix.

Apply the @Counted annotation to the list() method to count how many times the http://localhost:9080/inventory/systems URL is accessed. Note the additional metadata field:

monotonic

Set this annotation to true to count the total number of invocations of the annotated method, which is the total number of times the inventory is accessed.

Apply the @Gauge annotation to the getTotal() method to track the number of systems that are stored in the inventory. Note that when the value of the gauge is retrieved, the underlying getTotal() method is called to return the size of the inventory. Note the additional metadata field:

unit

Set the unit of the metric. If it is MetricUnits.NONE, the metric name is used as is without appending the unit name, and no scaling is applied.

Additional information about these annotations, relevant metadata fields, and more are available at the MicroProfile website.

Building and running the application

To build the application, run the Maven install phase from the command line in the start directory:

mvn install

This command builds the application and creates a .war file in the target directory. It also configures and installs Open Liberty into the target/liberty/wlp directory.

Next, run the Maven liberty:start-server goal:

mvn liberty:start-server

This goal starts an Open Liberty server instance. Your Maven pom.xml is already configured to start the application in this server instance.

Point your browser to the https://localhost:9443/metrics URL to review the all available metrics that have been enabled through MicroProfile Metrics. Log in with admin as your username and adminpwd as your password. You see only the system metrics because the server just started, and the inventory service has not been accessed.

Next, point your browser to the http://localhost:9080/inventory/systems URL. Reload the https://localhost:9443/metrics URL, or access only the application metrics at the https://localhost:9443/metrics/application URL.

If you make changes to the code, use the Maven compile goal to rebuild the application and have the running Open Liberty server pick them up automatically:

mvn compile

To stop the Open Liberty server, run the Maven liberty:stop-server goal:

mvn liberty:stop-server

Testing the metrics

You can test your application manually, but automated tests ensure code quality because they trigger a failure whenever a code change introduces a defect. JUnit and the JAX-RS Client API provide a simple environment for you to write tests.

First, create a test class in the src/test/java/it/io/openliberty/guides/metrics/MetricsTest.java file:

package it.io.openliberty.guides.metrics;

import static org.junit.Assert.*;
import java.io.*;
import java.util.*;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class MetricsTest {
  private static String httpPort;
  private static String httpsPort;
  private static String baseHttpUrl;
  private static String baseHttpsUrl;

  private List<String> metrics;
  private Client client;

  private final String INVENTORY_HOSTS = "inventory/systems";
  private final String INVENTORY_HOSTNAME = "inventory/systems/localhost";
  private final String METRICS_APPLICATION = "metrics/application";

  @BeforeClass
  public static void oneTimeSetup() {
    httpPort = System.getProperty("liberty.test.port");
    httpsPort = System.getProperty("liberty.https.port");
    baseHttpUrl = "http://localhost:" + httpPort + "/";
    baseHttpsUrl = "https://localhost:" + httpsPort + "/";
  }

  @Before
  public void setup() {
    client = ClientBuilder.newClient();
    client.register(JsrJsonpProvider.class);
  }

  @After
  public void teardown() {
    client.close();
  }

  @Test
  public void testSuite() {
    this.testPropertiesRequestTimeMetric();
    this.testInventoryAccessCountMetric();
    this.testInventorySizeGaugeMetric();
  }

  public void testPropertiesRequestTimeMetric() {
    connectToEndpoint(baseHttpUrl + INVENTORY_HOSTNAME);
    metrics = getMetrics();
    for (String metric : metrics) {
      if (metric.startsWith(
          "application:inventory_properties_request_time_rate_per_second")) {
        float seconds = Float.parseFloat(metric.split(" ")[1]);
        assertTrue(4 > seconds);
      }
    }
  }

  public void testInventoryAccessCountMetric() {
    connectToEndpoint(baseHttpUrl + INVENTORY_HOSTS);
    metrics = getMetrics();
    for (String metric : metrics) {
      if (metric.startsWith("application:inventory_access_count")) {
        assertTrue(
            1 == Character.getNumericValue(metric.charAt(metric.length() - 1)));
      }
    }
  }

  public void testInventorySizeGaugeMetric() {
    metrics = getMetrics();
    for (String metric : metrics) {
      if (metric.startsWith("application:inventory_size_guage")) {
        assertTrue(
            1 == Character.getNumericValue(metric.charAt(metric.length() - 1)));
      }
    }

  }

  public void connectToEndpoint(String url) {
    Response response = this.getResponse(url);
    this.assertResponse(url, response);
    response.close();
  }

  private List<String> getMetrics() {
    String usernameAndPassword = "admin" + ":" + "adminpwd";
    String authorizationHeaderValue = "Basic "
        + java.util.Base64.getEncoder()
                          .encodeToString(usernameAndPassword.getBytes());
    Response metricsResponse = client.target(baseHttpsUrl + METRICS_APPLICATION)
                                     .request(MediaType.TEXT_PLAIN)
                                     .header("Authorization",
                                         authorizationHeaderValue)
                                     .get();

    BufferedReader br = new BufferedReader(new InputStreamReader((InputStream) metricsResponse.getEntity()));
    List<String> result = new ArrayList<String>();
    try {
      String input;
      while ((input = br.readLine()) != null) {
        result.add(input);
      }
      br.close();
    } catch (IOException e) {
      e.printStackTrace();
      fail();
    }

    metricsResponse.close();
    return result;
  }

  private Response getResponse(String url) {
    return client.target(url).request().get();
  }

  private void assertResponse(String url, Response response) {
    assertEquals("Incorrect response code from " + url, 200, response.getStatus());
  }
}
  • The testPropertiesRequestTimeMetric() test case validates the @Timed metric. It sends a request to the http://localhost:9080/inventory/systems/localhost URL to access the inventory service, which adds the localhost host to the inventory. Next, the test case makes a connection to the https://localhost:9443/metrics/application URL to retrieve application metrics as plain text. Then, it asserts whether the time that is needed to retrieve the system properties for localhost is less than 4 seconds.

  • The testInventoryAccessCountMetric() test case validates the @Counted metric. The test case sends a request to the http://localhost:9080/inventory/systems URL to retrieve the whole inventory, and then it asserts that this endpoint is accessed only once.

  • The testInventorySizeGaugeMetric() test case validates the @Gauge metric. The test case first ensures that the localhost is in the inventory. It then looks for the @Gauge metric and asserts that the inventory size is equal to 1.

The oneTimeSetup() method retrieves the port number for the server and builds a base URL string to set up the tests. Apply the @BeforeClass annotation to this method to execute it before any of the test cases.

The setup() method creates a JAX-RS client that makes HTTP requests to the inventory service. Register this client with a JsrJsonpProvider JSON-P provider to process JSON resources. The teardown() method destroys this client instance. Apply the @Before annotation so that a method executes before a test case, and apply the @After annotation so that a method executes after a test case. Apply these annotations to methods that are generally used to perform any setup and teardown tasks before and after a test.

To execute the test cases in a particular order, put them in a testSuite() method. Label the method with the @Test annotation, which automatically executes when your test class runs.

In addition, a few endpoint tests src/test/java/it/io/openliberty/guides/inventory/InventoryEndpointTest.java and src/test/java/it/io/openliberty/guides/system/SystemEndpointTest.java are provided for you to test the basic functionality of the inventory and system services. If a test failure occurs, then you might have introduced a bug into the code.

Running the tests

If the server is still running from the previous steps, stop it using the Maven liberty:stop-server goal from command line in the start directory:

mvn liberty:stop-server

Then, verify that the tests pass using the Maven verify goal:

mvn verify

It may take some time before build is complete. If the tests pass, you will see a similar output to the following:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.system.SystemEndpointTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.4 sec - in it.io.openliberty.guides.system.SystemEndpointTest
Running it.io.openliberty.guides.metrics.MetricsTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.697 sec - in it.io.openliberty.guides.metrics.MetricsTest
Running it.io.openliberty.guides.inventory.InventoryEndpointTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.264 sec - in it.io.openliberty.guides.inventory.InventoryEndpointTest

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

To see whether the tests detect a failure, go to the MetricsTest.java file and change any of the assertions in the validateMetric() method. Rerun the Maven build. A test failure occurs.

Great work! You’re done!

You learned how to enable system and application metrics for microservices by using MicroProfile Metrics and wrote tests to validate them.

Contribute to this guide

Is something missing or broken? Raise an issue, or send us a pull request.