Helidon MP Metrics Guide

This guide describes how to create a sample Helidon MicroProfile (MP) project that can be used to run some basic examples using both built-in and custom metrics with Helidon.

What You Need

For this 30 minute tutorial, you will need the following:

A Helidon MP ApplicationYou can use your own application or use the Helidon MP Quickstart to create a sample application.
Java SE 17 (Open JDK 17)Helidon requires Java 17+.
Maven 3.6.1+Helidon requires Maven 3.6.1+.
Docker 18.09+You need Docker if you want to build and deploy Docker containers.
Kubectl 1.16.5+If you want to deploy to Kubernetes, you need kubectl and a Kubernetes cluster (you can install one on your desktop.
HelmTo manage Kubernetes applications.
Verify Prerequisites
java -version
mvn --version
docker --version
kubectl version
Copied
Setting JAVA_HOME
# On Mac
export JAVA_HOME=`/usr/libexec/java_home -v 17`

# On Linux
# Use the appropriate path to your JDK
export JAVA_HOME=/usr/lib/jvm/jdk-17
Copied

Create a Sample Helidon MP Project

Use the Helidon MP Maven archetype to create a simple project that can be used for the examples in this guide.

Run the Maven archetype
mvn -U archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-mp \
    -DarchetypeVersion=3.2.16 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-mp \
    -Dpackage=io.helidon.examples.quickstart.mp
Copied

Using the Built-In Metrics

Helidon provides three scopes of metrics: base, vendor, and application. Here are the metric endpoints:

  1. /metrics/base - Base metrics data as specified by the MicroProfile Metrics specification.
  2. /metrics/vendor - Helidon-specific metrics data.
  3. /metrics/application - Application-specific metrics data.

The /metrics endpoint will return data for all scopes.

The built-in metrics fall into three categories:

  1. JVM behavior (in the base registry),
  2. basic key performance indicators for request handling (in the vendor registry), and
  3. thread pool utilization (also in the vendor registry).

A later section describes the key performance indicator metrics in detail.

The following example demonstrates how to use the other built-in metrics. All examples are executed from the root directory of your project (helidon-quickstart-mp).

Build the application, skipping unit tests, then run it:
mvn package -DskipTests=true
java -jar target/helidon-quickstart-mp.jar
Copied

Metrics can be returned in either text format (the default), or JSON. The text format uses OpenMetrics (Prometheus) Text Format, see https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-details.

Verify the metrics endpoint in a new terminal window:
curl http://localhost:8080/metrics
Copied
Text response:
# TYPE base_REST_request_total counter
# HELP base_REST_request_total The number of invocations and total response time of this RESTful resource method since the start of the server. The metric will not record the elapsed time nor count of a REST request if it resulted in an unmapped exception. Also tracks the highest recorded time duration within the previous completed full minute and lowest recorded time duration within the previous completed full minute.
base_REST_request_total{class="io.helidon.examples.quickstart.mp.GreetResource",method="getDefaultMessage"} 0
# TYPE base_REST_request_elapsedTime_seconds gauge
base_REST_request_elapsedTime_seconds{class="io.helidon.examples.quickstart.mp.GreetResource",method="getDefaultMessage"} 0.0
# TYPE base_REST_request_maxTimeDuration_seconds gauge
base_REST_request_maxTimeDuration_seconds{class="io.helidon.examples.quickstart.mp.GreetResource",method="getDefaultMessage"} NaN
# TYPE base_REST_request_minTimeDuration_seconds gauge
base_REST_request_minTimeDuration_seconds{class="io.helidon.examples.quickstart.mp.GreetResource",method="getDefaultMessage"} NaN
base_REST_request_total{class="io.helidon.examples.quickstart.mp.GreetResource",method="getMessage_java.lang.String"} 0
base_REST_request_elapsedTime_seconds{class="io.helidon.examples.quickstart.mp.GreetResource",method="getMessage_java.lang.String"} 0.0
base_REST_request_maxTimeDuration_seconds{class="io.helidon.examples.quickstart.mp.GreetResource",method="getMessage_java.lang.String"} NaN
base_REST_request_minTimeDuration_seconds{class="io.helidon.examples.quickstart.mp.GreetResource",method="getMessage_java.lang.String"} NaN

# TYPE base_REST_request_unmappedException_total counter
# HELP base_REST_request_unmappedException_total The total number of unmapped exceptions that occur from this RESTful resouce method since the start of the server.
base_REST_request_unmappedException_total{class="io.helidon.examples.quickstart.mp.GreetResource",method="getDefaultMessage"} 0
base_REST_request_unmappedException_total{class="io.helidon.examples.quickstart.mp.GreetResource",method="getMessage_java.lang.String"} 0

# 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 7511
# TYPE base:classloader_total_loaded_class_count counter
# HELP base:classloader_total_loaded_class_count Displays the total number of classes that have been loaded since the Java virtual machine has started execution.
base:classloader_total_loaded_class_count 7512
Copied

You can get the same data in JSON format.

Verify the metrics endpoint with an HTTP accept header:
curl -H "Accept: application/json"  http://localhost:8080/metrics
Copied
JSON response:
{
  "base": {
    "REST.request":
      {
      "count;class=io.helidon.examples.quickstart.mp.GreetResource;method=getDefaultMessage": 0,
      "elapsedTime;class=io.helidon.examples.quickstart.mp.GreetResource;method=getDefaultMessage": 0,
      "maxTimeDuration;class=io.helidon.examples.quickstart.mp.GreetResource;method=getDefaultMessage": null,
      "minTimeDuration;class=io.helidon.examples.quickstart.mp.GreetResource;method=getDefaultMessage": null,
      "count;class=io.helidon.examples.quickstart.mp.GreetResource;method=getMessage_java.lang.String": 0,
      "elapsedTime;class=io.helidon.examples.quickstart.mp.GreetResource;method=getMessage_java.lang.String": 0,
      "maxTimeDuration;class=io.helidon.examples.quickstart.mp.GreetResource;method=getMessage_java.lang.String": null,
      "minTimeDuration;class=io.helidon.examples.quickstart.mp.GreetResource;method=getMessage_java.lang.String": null,
      },
    "classloader.currentLoadedClass.count": 7534,
    "classloader.totalLoadedClass.count": 7538,
    "classloader.totalUnloadedClass.count": 1,
    "cpu.availableProcessors": 4,
    "cpu.systemLoadAverage": 2.83349609375,
    "gc.PS MarkSweep.count": 2,
    "gc.PS MarkSweep.time": 77,
    "gc.PS Scavenge.count": 5,
    "gc.PS Scavenge.time": 37,
    "jvm.uptime": 727588,
    "memory.committedHeap": 284164096,
    "memory.maxHeap": 3817865216,
    "memory.usedHeap": 53283088,
    "thread.count": 44,
    "thread.daemon.count": 35,
    "thread.max.count": 44
  },
  "vendor": {
    "executor-service.active-count;poolIndex=0;supplierCategory=helidon-thread-pool-2;supplierIndex=0": 0,
    "executor-service.completed-task-count;poolIndex=0;supplierCategory=helidon-thread-pool-2;supplierIndex=0": 0,
    "executor-service.largest-pool-size;poolIndex=0;supplierCategory=helidon-thread-pool-2;supplierIndex=0": 5,
    "executor-service.pool-size;poolIndex=0;supplierCategory=helidon-thread-pool-2;supplierIndex=0": 5,
    "executor-service.queue.remaining-capacity;poolIndex=0;supplierCategory=helidon-thread-pool-2;supplierIndex=0": 10000,
    "executor-service.queue.size;poolIndex=0;supplierCategory=helidon-thread-pool-2;supplierIndex=0": 0,
    "executor-service.task-count;poolIndex=0;supplierCategory=helidon-thread-pool-2;supplierIndex=0": 0,
    "requests.count": 6,
    "requests.meter": {
      "count": 6,
      "meanRate": 0.008275992296704147,
      "oneMinRate": 0.01576418632772332,
      "fiveMinRate": 0.006695060022357365,
      "fifteenMinRate": 0.0036382699664488415
    }
  }
}
Copied

You can get a single metric by specifying the name in the URL path.

Get the Helidon requests.meter metric:
curl -H "Accept: application/json"  http://localhost:8080/metrics/vendor/requests.meter
Copied
JSON response:
{
  "requests.meter": {
    "count": 6,
    "meanRate": 0.008275992296704147,
    "oneMinRate": 0.01576418632772332,
    "fiveMinRate": 0.006695060022357365,
    "fifteenMinRate": 0.0036382699664488415
  }
}
Copied

You cannot get the individual fields of a metric. For example, you cannot target http://localhost:8080/metrics/vendor/requests.meter.count.

The base metrics illustrated above provide some insight into the behavior of the JVM in which the server runs.

The vendor metrics shown above appear in two groups:

  • Helidon thread pools

    Helidon uses these thread pools for its own internal work, and your application can also use Helidon-managed thread pools if it needs to do work asynchronously. (See this example.) The metrics in this group show information about the thread pools which can help you assess how efficiently they are utilized. Helidon uses tags to distinguish the metrics which describe different thread pools. In some cases the specific metrics exposed depend on the particular type of thread pool.

  • basic key performance indicators

    These metrics give an idea of the request traffic the server is handling. See the later section for more information on the basic and extended key performance indicator metrics.

Controlling Metrics Behavior

By adding a metrics section to your application configuration you can control how the Helidon metrics subsystem behaves in any of several ways.

Disabling Metrics Subsystem Entirely

By default, if your application depends on the helidon-metrics Maven module then full-featured metrics are enabled. You can disable the metrics subsystem entirely using configuration:

Configuration properties file disabling metrics
metrics.enabled=false
Copied

With metrics processing disabled, Helidon never updates any metrics and the /metrics endpoints respond with 404 plus a message that the metrics subsystem is disabled.

Collecting Basic and Extended Key Performance Indicator (KPI) Metrics

Any time you include the Helidon metrics module in your application, Helidon tracks two basic performance indicator metrics:

  • a Counter of all requests received (requests.count), and

  • a Meter of all requests received (requests.meter).

Helidon MP also includes additional, extended KPI metrics which are disabled by default:

  • current number of requests in-flight - a ConcurrentGauge (requests.inFlight) of requests currently being processed

  • long-running requests - a Meter (requests.longRunning) measuring the rate at which Helidon processes requests which take at least a given amount of time to complete; configurable, defaults to 10000 milliseconds (10 seconds)

  • load - a Meter (requests.load) measuring the rate at which requests are worked on (as opposed to received)

  • deferred - a Meter (requests.deferred) measuring the rate at which a request’s processing is delayed after Helidon receives the request

You can enable and control these metrics using configuration:

Configuration properties file controlling extended KPI metrics
metrics.key-performance-indicators.extended = true
metrics.key-performance-indicators.long-running.threshold-ms = 2000
Copied

Controlling REST.request Metrics

Helidon implements the optional family of metrics, all with the name REST.request, as described in the MicroProfile Metrics specification. Each instance is a SimpleTimer with tags class and method identifying exactly which REST endpoint Java method that instance measures.

By default, Helidon MP does not enable this feature. Enable it by editing your application configuration to set metrics.rest-request.enabled to true.

Note that the applications you generate using the full Helidon archetype do enable this feature in the generated config file. You can see the results in the sample output shown in earlier example runs.

Enabling and Disabling Metrics Usage by a Component

Helidon contains several components and integrations which register and update metrics. Depending on how the component is written, you might be able to disable just that component’s use of metrics:

Configuration properties file disabling a component’s use of metrics
some-component.metrics.enabled=false
Copied

Check the documentation for a specific component to find out whether that component uses metrics and whether it allows you to disable that use. If you disable a component’s use of metrics, Helidon does not register the component’s metrics in the visible metrics registries nor do those metrics ever update their values. The response from the /metrics endpoint excludes that component’s metrics.

Note that if you disable metrics processing entirely, no component updates its metrics regardless of any component-level metrics settings.

Controlling Metrics By Registry Type and Metric Name

You can control the collection and reporting of metrics by registry type and metric name within registry type.

Disabling All Metrics of a Given Registry Type

To disable all metrics in a given registry type (application, vendor, or base), add one or more groups to the configuration:

Disabling base and vendor metrics (properties format)
metrics.registries.0.type = base
metrics.registries.0.enabled = false
metrics.registries.1.type = vendor
metrics.registries.1.enabled = false
Copied
Disabling base and vendor metrics (YAML format)
metrics:
  registries:
    - type: base
      enabled: false
    - type: vendor
      enabled: false
Copied
Controlling Metrics by Metric Name

You can be even more selective. Within a registry type you can configure up to two regular expression patterns:

  • one matching metric names to exclude, and

  • one matching metric names to include.

Helidon updates and reports a metric only if two conditions hold:

  • the metric name does not match the exclude regex pattern (if you define one), and

  • either

    • there is no include regex pattern, or

    • the metric name matches the include pattern.

Caution

Make sure any include regex pattern you specify matches all the metric names you want to capture.

Suppose your application creates and updates a group of metrics with names such as myapp.xxx.queries, myapp.xxx.creates, myapp.xxx.updates, and myapp.xxx.deletes where xxx can be either supplier or customer.

The following example gathers all metrics except those from your application regarding suppliers:

Disabling metrics by name (properties format)
metrics.registries.0.type = application
metrics.registries.0.filter.exclude = myapp\.supplier\..*
Copied

The following settings select the particular subset of the metrics created in your application code representing updates of customers and suppliers:

Enabling metrics by name (properties format)
metrics.registries.0.type = application
metrics.registries.0.filter.include = myapp\..*\.updates
Copied

If you use the YAML configuration format, enclose the regex patterns in single-quote marks:

Enabling metrics by name (YAML format)
metrics:
  registries:
    - type: application
      filter:
        include: 'myapp\..*\.updates'
Copied

The next example selects only your application’s metrics while excluding those which refer to deletions:

Combining include and exclude
metrics.registries.0.type = application
metrics.registries.0.filter.include = myapp\..*
metrics.registries.0.filter.exclude = myapp\..*/deletes
Copied

Helidon would not update or report the metric myapp.supplier.queries, for example. To include metrics from your application for both updates and queries (but not for other operations), you could change the settings in the previous example to this:

metrics.registries.0.type = application
metrics.registries.0.filter.include = myapp\..*\.updates|myapp\..*\.queries
metrics.registries.0.filter.exclude = myapp\..*/deletes
Copied

Metrics Metadata

Each metric has associated metadata that describes:

  1. name: The name of the metric.
  2. units: The unit of the metric such as time (seconds, millisecond), size (bytes, megabytes), etc.
  3. type: The type of metric: Counter, Timer, Meter, Histogram, SimpleTimer, or Gauge.

You can get the metadata for any scope, such as /metrics/base, as shown below:

Get the metrics metadata using HTTP OPTIONS method:
 curl -X OPTIONS -H "Accept: application/json"  http://localhost:8080/metrics/base
Copied
JSON response (truncated):
{
  "classloader.currentLoadedClass.count": {
    "unit": "none",
    "type": "counter",
    "description": "Displays the number of classes that are currently loaded in the Java virtual machine.",
    "displayName": "Current Loaded Class Count"
  },
  "jvm.uptime": {
    "unit": "milliseconds",
    "type": "gauge",
    "description": "Displays the start time of the Java virtual machine in milliseconds. This attribute displays the approximate time when the Java virtual machine started.",
    "displayName": "JVM Uptime"
  },
  "memory.usedHeap": {
    "unit": "bytes",
    "type": "gauge",
    "description": "Displays the amount of used heap memory in bytes.",
    "displayName": "Used Heap Memory"
  }
}
Copied

Application-Specific Metrics Data

You can create application-specific metrics and integrate them with Helidon using CDI. To add a new metric, simply annotate the JAX-RS resource with one of the metric annotations. Metrics can be injected at the class, method, and field-levels. This document shows examples of all three.

Helidon will automatically create and register annotated application metrics and store them in the application MetricRegistry, which also contains the metric metadata. The metrics will exist for the lifetime of the application. Each metric annotation has mandatory and optional fields. The name field, for example, is optional.

Method Level Metrics

There are four metrics that you can use by annotating a method:

  1. @Counted - Register a Counter metric
  2. @Timed - Register a Timer metric
  3. @Metered - Register a Meter metric
  4. @SimplyTimed - Register a SimpleTimer metric

The following example will demonstrate how to use the @Counted annotation to track the number of times the /cards endpoint is called.

Create a new class GreetingCards with the following code:
package io.helidon.examples.quickstart.mp;

import java.util.Collections;
import jakarta.enterprise.context.RequestScoped;
import jakarta.json.Json;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.metrics.annotation.Counted;

@Path("/cards") 
@RequestScoped 
public class GreetingCards {

  private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Counted(name = "any-card")  
  public JsonObject anyCard() throws InterruptedException {
    return createResponse("Here are some random cards ...");
  }

  private JsonObject createResponse(String msg) {
    return JSON.createObjectBuilder().add("message", msg).build();
  }
}
Copied
  • This class is annotated with Path which sets the path for this resource as /cards.
  • The @RequestScoped annotation defines that this bean is request scoped. The request scope is active only for the duration of one web service invocation and it is destroyed at the end of that invocation.
  • The annotation @Counted will register a Counter metric for this method, creating it if needed. The counter is incremented each time the anyCards method is called. The name attribute is optional.

For Metrics 1.1, you must set monotonic field to true to force the count to increment when entering the method. The default behavior is to decrement when exiting the method. Here is an example: @Counted(name = "any-card", monotonic = true).

Build and run the application, then invoke the application endpoints below:
curl http://localhost:8080/cards
curl http://localhost:8080/cards
curl -H "Accept: application/json"  http://localhost:8080/metrics/application
Copied
JSON response:
{
  "io.helidon.examples.quickstart.mp.GreetingCards.any-card":2 
}
Copied
  • The any-card count is two, since you invoked the endpoint twice.

Notice the counter is fully qualified. You can remove the package prefix by using the absolute=true field in the @Counted annotation. You must use absolute=false for class-level annotations.

Additional Method Level Metrics

The @Timed, @Metered, and @SimplyTimed annotations can also be used with a method. For the following example. you can just annotate the same method with @Metered and @Timed. These metrics collect significant information about the measured methods, but at a cost of some overhead and more complicated output. Use @SimplyTimed in cases where capturing the invocation count and the total elapsed time spent in a block of code is sufficient.

Note that when using multiple annotations on a method, you must give the metrics different names as shown below.

Update the GreetingCards class with the following code:
package io.helidon.examples.quickstart.mp;

import java.util.Collections;
import jakarta.enterprise.context.RequestScoped;
import jakarta.json.Json;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.metrics.MetricUnits;
import org.eclipse.microprofile.metrics.annotation.Counted;
import org.eclipse.microprofile.metrics.annotation.Metered;
import org.eclipse.microprofile.metrics.annotation.Timed;

@Path("/cards")
@RequestScoped
public class GreetingCards {

  private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Counted(name = "cardCount", absolute = true) 
  @Metered(name = "cardMeter", absolute = true, unit = MetricUnits.MILLISECONDS) 
  @Timed(name = "cardTimer", absolute = true, unit = MetricUnits.MILLISECONDS) 
  public JsonObject anyCard() {
    return createResponse("Here are some random cards ...");
  }

  private JsonObject createResponse(String msg) {
    return JSON.createObjectBuilder().add("message", msg).build();
  }
}
Copied
  • Specify a custom name for the Counter metric and set absolute=true to remove the path prefix from the name.
  • Add the @Metered annotation to get a Meter metric.
  • Add the @Timed annotation to get a Timer metric.
Build and run the application, then invoke the application endpoints below:
curl http://localhost:8080/cards
curl http://localhost:8080/cards
curl -H "Accept: application/json"  http://localhost:8080/metrics/application
Copied
JSON response:
{
  "cardCount": 2,
  "cardMeter": { 
    "count": 2,
    "meanRate": 0.15653506570241812,
    "oneMinRate": 0,
    "fiveMinRate": 0,
    "fifteenMinRate": 0
  },
  "cardTimer": { 
    "count": 2,
    "elapsedTime": 2,
    "meanRate": 0.15651866263362785,
    "oneMinRate": 0,
    "fiveMinRate": 0,
    "fifteenMinRate": 0,
    "min": 0,
    "max": 2,
    "mean": 1.0506565,
    "stddev": 1.0405735,
    "p50": 2.09123,
    "p75": 2.09123,
    "p95": 2.09123,
    "p98": 2.09123,
    "p99": 2.09123,
    "p999": 2.09123
  }
}
Copied
  • The Meter metric includes the count field (it is a superset of Counter).
  • The Timer metric includes the Meter fields (it is a superset of Meter).

Reusing Metrics

You can share a metric across multiple endpoints simply by specifying the same metric annotation as demonstrated below.

Update the GreetingCards class with the following code:
package io.helidon.examples.quickstart.mp;

import java.util.Collections;
import jakarta.enterprise.context.RequestScoped;
import jakarta.json.Json;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.metrics.annotation.Counted;

@Path("/cards")
@RequestScoped
public class GreetingCards {

  private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Counted(name = "anyCard", absolute = true)
  public JsonObject anyCard() throws InterruptedException {
    return createResponse("Here are some cards ...");
  }

  @GET
  @Path("/birthday")
  @Produces(MediaType.APPLICATION_JSON)
  @Counted(name = "specialEventCard", absolute = true)  
  public JsonObject birthdayCard() throws InterruptedException {
    return createResponse("Here are some birthday cards ...");
  }

  @GET
  @Path("/wedding")
  @Produces(MediaType.APPLICATION_JSON)
  @Counted(name = "specialEventCard", absolute = true)  
  public JsonObject weddingCard() throws InterruptedException {
    return createResponse("Here are some wedding cards ...");
  }

  private JsonObject createResponse(String msg) {
    return JSON.createObjectBuilder().add("message", msg).build();
  }
}
Copied
  • The /birthday endpoint uses a Counter metric, named specialEventCard.
  • The /wedding endpoint uses the same Counter metric, named specialEventCard.
Build and run the application, then invoke the following endpoints:
curl  http://localhost:8080/cards/wedding
curl  http://localhost:8080/cards/birthday
curl  http://localhost:8080/cards
curl -H "Accept: application/json"  http://localhost:8080/metrics/application
Copied
JSON response from /metrics/application:
{
"anyCard": 1,
"specialEventCard": 2  
}
Copied
  • Notice that specialEventCard count is two, since you accessed /cards/wedding and /cards/birthday.

Class Level Metrics

You can collect metrics at the class-level to aggregate data from all methods in that class using the same metric. The following example introduces a metric to count all card queries. In the following example, the method-level metrics are not needed to aggregate the counts, but they are left in the example to demonstrate the combined output of all three metrics.

Update the GreetingCards class with the following code:
package io.helidon.examples.quickstart.mp;

import java.util.Collections;
import jakarta.enterprise.context.RequestScoped;
import jakarta.json.Json;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.metrics.annotation.Counted;

@Path("/cards")
@RequestScoped
@Counted(name = "totalCards") 
public class GreetingCards {

  private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Counted(absolute = true) 
  public JsonObject anyCard() throws InterruptedException {
    return createResponse("Here are some random cards ...");
  }

  @Path("/birthday")
  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Counted(absolute = true) 
  public JsonObject birthdayCard() throws InterruptedException {
    return createResponse("Here are some birthday cards ...");
  }

  private JsonObject createResponse(String msg) {
    return JSON.createObjectBuilder().add("message", msg).build();
  }
}
Copied
  • This class is annotated with @Counted, which aggregates count data from all the method that have a Count annotation.
  • Use absolute=true to remove path prefix for method-level annotations.
  • Add a method with a Counter metric to get birthday cards.
Build and run the application, then invoke the following endpoints:
curl http://localhost:8080/cards
curl http://localhost:8080/cards/birthday
curl -H "Accept: application/json"  http://localhost:8080/metrics/application
Copied
JSON response from /metrics/application:
{
  "anyCard": 1,
  "birthdayCard": 1,
  "io.helidon.examples.quickstart.mp.totalCards.GreetingCards": 2  
}
Copied
  • The totalCards count is a total of all the method-level Counter metrics. Class level metric names are always fully qualified.

Field Level Metrics

Field level metrics can be injected into managed objects, but they need to be updated by the application code. This annotation can be used on fields of type Meter, Timer, Counter, and Histogram.

The following example shows how to use a field-level Counter metric to track cache hits.

Update the GreetingCards class with the following code:
package io.helidon.examples.quickstart.mp;

import java.util.Collections;
import java.util.Random;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.json.Json;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.metrics.Counter;
import org.eclipse.microprofile.metrics.annotation.Counted;
import org.eclipse.microprofile.metrics.annotation.Metric;

@Path("/cards")
@RequestScoped
@Counted(name = "totalCards")
public class GreetingCards {

  private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());

  @Inject
  @Metric(name = "cacheHits", absolute = true) 
  private Counter cacheHits;

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Counted(absolute = true)
  public JsonObject anyCard() throws InterruptedException {
    updateStats(); 
    return createResponse("Here are some random cards ...");
  }

  @Path("/birthday")
  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Counted(absolute = true)
  public JsonObject birthdayCard() throws InterruptedException {
    updateStats();  
    return createResponse("Here are some birthday cards ...");
  }

  private JsonObject createResponse(String msg) {
    return JSON.createObjectBuilder().add("message", msg).build();
  }

  private void updateStats() {
    if (new Random().nextInt(3) == 1) {
      cacheHits.inc(); 
    }
  }
}
Copied
  • A Counter metric field, cacheHits, is automatically injected by Helidon.
  • Call updateStats() to update the cache hits.
  • Call updateStats() to update the cache hits.
  • Randomly increment the cacheHits counter.
Build and run the application, then invoke the following endpoints:
curl http://localhost:8080/cards
curl http://localhost:8080/cards
curl http://localhost:8080/cards/birthday
curl http://localhost:8080/cards/birthday
curl http://localhost:8080/cards/birthday
curl -H "Accept: application/json"  http://localhost:8080/metrics/application
Copied
JSON response from /metrics/application:
{
  "anyCard": 2,
  "birthdayCard": 3,
  "cacheHits": 2, 
  "io.helidon.examples.quickstart.mp.totalCards.GreetingCards": 5
}
Copied
  • The cache was hit two times out of five queries.

Gauge Metric

The metrics you have tested so far are updated in response to an application REST request, i.e GET /cards. These metrics can be declared in a request scoped class and Helidon will store the metric in the MetricRegistry, so the value persists across requests. When GET /metrics/application is invoked, Helidon will return the current value of the metric stored in the MetricRegistry. The Gauge metric is different from all the other metrics. The application must provide a getter to return the gauge value in an application scoped class. When GET /metrics/application is invoked, Helidon will call the Gauge getter, store that value in the MetricsRegistry, and return it as part of the metrics response payload. So, the Gauge metric value is updated real-time, in response to the get metrics request.

The following example demonstrates how to use a Gauge to track application up-time.

Create a new GreetingCardsAppMetrics class with the following code:
package io.helidon.examples.quickstart.mp;

import java.time.Duration;
import java.util.concurrent.atomic.AtomicLong;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;
import org.eclipse.microprofile.metrics.annotation.Gauge;

@ApplicationScoped 
public class GreetingCardsAppMetrics {

  private AtomicLong startTime = new AtomicLong(0); 

  public void onStartUp(@Observes @Initialized(ApplicationScoped.class) Object init) {
    startTime = new AtomicLong(System.currentTimeMillis()); 
  }

  @Gauge(unit = "TimeSeconds")
  public long appUpTimeSeconds() {
    return Duration.ofMillis(System.currentTimeMillis() - startTime.get()).getSeconds();  
  }
}
Copied
  • This managed object must be application scoped to properly register and use the Gauge metric.
  • Declare an AtomicLong field to hold the start time of the application.
  • Initialize the application start time.
  • Return the application appUpTimeSeconds metric, which will be included in the application metrics.
Update the GreetingCards class with the following code to simplify the metrics output:
package io.helidon.examples.quickstart.mp;

import java.util.Collections;
import jakarta.enterprise.context.RequestScoped;
import jakarta.json.Json;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.metrics.annotation.Counted;

@Path("/cards")
@RequestScoped
public class GreetingCards {

  private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Counted(name = "cardCount", absolute = true)
  public JsonObject anyCard() throws InterruptedException {
    return createResponse("Here are some random cards ...");
  }

  private JsonObject createResponse(String msg) {
    return JSON.createObjectBuilder().add("message", msg).build();
  }
}
Copied
Build and run the application, then invoke the application metrics endpoint:
curl -H "Accept: application/json"  http://localhost:8080/metrics/application
Copied
JSON response from /metrics/application:
{
  "cardCount": 0,
  "io.helidon.examples.quickstart.mp.GreetingCardsAppMetrics.appUpTimeSeconds": 6 
}
Copied
  • The application has been running for 6 seconds.

Integration with Kubernetes and Prometheus

Kubernetes Integration

The following example shows how to integrate the Helidon MP application with Kubernetes.

Stop the application and build the docker image:
docker build -t helidon-metrics-mp .
Copied
Create the Kubernetes YAML specification, named metrics.yaml, with the following content:
kind: Service
apiVersion: v1
metadata:
  name: helidon-metrics 
  labels:
    app: helidon-metrics
  annotations:
    prometheus.io/scrape: true 
spec:
  type: NodePort
  selector:
    app: helidon-metrics
  ports:
    - port: 8080
      targetPort: 8080
      name: http
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: helidon-metrics
spec:
  replicas: 1 
  selector:
    matchLabels:
      app: helidon-metrics
  template:
    metadata:
      labels:
        app: helidon-metrics
        version: v1
    spec:
      containers:
        - name: helidon-metrics
          image: helidon-metrics-mp
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
Copied
  • A service of type NodePort that serves the default routes on port 8080.
  • An annotation that will allow Prometheus to discover and scrape the application pod.
  • A deployment with one replica of a pod.
Create and deploy the application into Kubernetes:
kubectl apply -f ./metrics.yaml
Copied
Get the service information:
kubectl get service/helidon-metrics
Copied
NAME             TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
helidon-metrics   NodePort   10.99.159.2   <none>        8080:31143/TCP   8s 
Copied
  • A service of type NodePort that serves the default routes on port 31143.
Verify the metrics endpoint using port 30116, your port will likely be different:
curl http://localhost:31143/metrics
Copied

Leave the application running in Kubernetes since it will be used for Prometheus integration.

Prometheus Integration

The metrics service that you just deployed into Kubernetes is already annotated with prometheus.io/scrape:. This will allow Prometheus to discover the service and scrape the metrics. This example shows how to install Prometheus into Kubernetes, then verify that it discovered the Helidon metrics in your application.

Install Prometheus and wait until the pod is ready:
helm install stable/prometheus --name metrics
export POD_NAME=$(kubectl get pods --namespace default -l "app=prometheus,component=server" -o jsonpath="{.items[0].metadata.name}")
kubectl get pod $POD_NAME
Copied

You will see output similar to the following. Repeat the kubectl get pod command until you see 2/2 and Running. This may take up to one minute.

metrics-prometheus-server-5fc5dc86cb-79lk4   2/2     Running   0          46s
Copied
Create a port-forward so you can access the server URL:
kubectl --namespace default port-forward $POD_NAME 7090:9090
Copied

Now open your browser and navigate to http://localhost:7090/targets. Search for helidon on the page and you will see your Helidon application as one of the Prometheus targets.

Final Cleanup

You can now delete the Kubernetes resources that were just created during this example.

Delete the Prometheus Kubernetes resources:
helm delete --purge metrics
Copied
Delete the application Kubernetes resources:
kubectl delete -f ./metrics.yaml
Copied

Summary

This guide demonstrated how to use metrics in a Helidon MP application using various combinations of metrics and scopes.

  • Access metrics for all three scopes: base, vendor, and application

  • Configure application metrics at the class, method, and field-level

  • Integrate Helidon metrics with Kubernetes and Prometheus

Refer to the following references for additional information: