Metrics MP Guide

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

What you need

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 archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-mp \
    -DarchetypeVersion=1.4.12 \
    -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. Helidon automatically provides built-in base and vendor metrics. Applications can use these metrics without additional configuration or code changes. 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 following example will demonstrate how to use the 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 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: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": {
    "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": {
    "grpc.requests.count": 0,
    "grpc.requests.meter": {
      "count": 0,
      "meanRate": 0.0,
      "oneMinRate": 0.0,
      "fiveMinRate": 0.0,
      "fifteenMinRate": 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 grpc.requests.meter metric:
curl -H "Accept: application/json"  http://localhost:8080/metrics/vendor/grpc.requests.meter
Copied
JSON response:
{
  "grpc.requests.meter": {
    "count": 0,
    "meanRate": 0.0,
    "oneMinRate": 0.0,
    "fiveMinRate": 0.0,
    "fifteenMinRate": 0.0
  }
}
Copied

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

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, 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 three 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

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 javax.enterprise.context.RequestScoped;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.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).

Update the GreetApplication class as follows:
@Override
public Set<Class<?>> getClasses() {
    return CollectionsHelper.setOf(GreetResource.class, GreetingCards.class); 
}
Copied
  • Add the GreetingCards class to the set of classes managed by Helidon for this application.
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 and @Metered annotations can also be used with a method. For the following example. you can just annotate the same method with these metrics. When using multiple annoations 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 javax.enterprise.context.RequestScoped;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.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() throws InterruptedException {
    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.3664337145491488,
    "oneMinRate": 0.4,
    "fiveMinRate": 0.4,
    "fifteenMinRate": 0.4
  },
  "cardTimer": { 
    "count": 2,
    "meanRate": 0.36649792432150535,
    "oneMinRate": 0.4,
    "fiveMinRate": 0.4,
    "fifteenMinRate": 0.4,
    "min": 12944,
    "max": 2078856,
    "mean": 1045900.0,
    "stddev": 1032956.0,
    "p50": 2078856.0,
    "p75": 2078856.0,
    "p95": 2078856.0,
    "p98": 2078856.0,
    "p99": 2078856.0,
    "p999": 2078856.0
  }
}
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 by specifying the reusable field in the metric annotation as demonstrated below.

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

import java.util.Collections;
import javax.enterprise.context.RequestScoped;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.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, reusable = 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, reusable = 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 javax.enterprise.context.RequestScoped;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.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 javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.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 javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Initialized;
import javax.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 javax.enterprise.context.RequestScoped;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.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

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: extensions/v1beta1
metadata:
  name: helidon-metrics
spec:
  replicas: 1 
  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. In this exercise, you will 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: