- 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 Application | You 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. |
| Helm | To manage Kubernetes applications. |
java -version
mvn --version
docker --version
kubectl version# 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-17Create 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.
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.mpUsing the Built-In Metrics
Helidon provides three scopes of metrics: base, vendor, and application. Here are the metric endpoints:
/metrics/base- Base metrics data as specified by the MicroProfile Metrics specification./metrics/vendor- Helidon-specific metrics data./metrics/application- Application-specific metrics data.
The /metrics endpoint will return data for all scopes.
The built-in metrics fall into three categories:
- JVM behavior (in the base registry),
- basic key performance indicators for request handling (in the vendor registry), and
- 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).
mvn package -DskipTests=true
java -jar target/helidon-quickstart-mp.jarMetrics 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.
curl http://localhost:8080/metrics# 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 7512You can get the same data in JSON format.
curl -H "Accept: application/json" http://localhost:8080/metrics{
"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
}
}
}You can get a single metric by specifying the name in the URL path.
requests.meter metric:curl -H "Accept: application/json" http://localhost:8080/metrics/vendor/requests.meter{
"requests.meter": {
"count": 6,
"meanRate": 0.008275992296704147,
"oneMinRate": 0.01576418632772332,
"fiveMinRate": 0.006695060022357365,
"fifteenMinRate": 0.0036382699664488415
}
}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.
Identify groups of metrics to control:
by metric registry (application, vendor, and base) and within a registry by metric names which match patterns you provide.
Select whether to collect extended key performance indicator metrics.
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:
metrics.enabled=falseWith 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
Counterof all requests received (requests.count), anda
Meterof 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 processedlong-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:
metrics.key-performance-indicators.extended = true
metrics.key-performance-indicators.long-running.threshold-ms = 2000Controlling 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:
some-component.metrics.enabled=falseCheck 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:
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 = falsebase and vendor metrics (YAML format)metrics:
registries:
- type: base
enabled: false
- type: vendor
enabled: falseControlling 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
excluderegex pattern (if you define one), andeither
there is no
includeregex pattern, orthe metric name matches the
includepattern.
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:
metrics.registries.0.type = application
metrics.registries.0.filter.exclude = myapp\.supplier\..*The following settings select the particular subset of the metrics created in your application code representing updates of customers and suppliers:
metrics.registries.0.type = application
metrics.registries.0.filter.include = myapp\..*\.updatesIf you use the YAML configuration format, enclose the regex patterns in single-quote marks:
metrics:
registries:
- type: application
filter:
include: 'myapp\..*\.updates'The next example selects only your application’s metrics while excluding those which refer to deletions:
include and excludemetrics.registries.0.type = application
metrics.registries.0.filter.include = myapp\..*
metrics.registries.0.filter.exclude = myapp\..*/deletesHelidon 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\..*/deletesMetrics Metadata
Each metric has associated metadata that describes:
- name: The name of the metric.
- units: The unit of the metric such as time (seconds, millisecond), size (bytes, megabytes), etc.
- type: The type of metric:
Counter,Timer,Meter,Histogram,SimpleTimer, orGauge.
You can get the metadata for any scope, such as /metrics/base, as shown below:
curl -X OPTIONS -H "Accept: application/json" http://localhost:8080/metrics/base{
"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"
}
}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:
@Counted- Register aCountermetric@Timed- Register aTimermetric@Metered- Register aMetermetric@SimplyTimed- Register aSimpleTimermetric
The following example will demonstrate how to use the @Counted annotation to track the number of times the /cards endpoint is called.
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();
}
}- This class is annotated with
Pathwhich sets the path for this resource as/cards. - The
@RequestScopedannotation 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
@Countedwill register aCountermetric for this method, creating it if needed. The counter is incremented each time the anyCards method is called. Thenameattribute 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).
curl http://localhost:8080/cards
curl http://localhost:8080/cards
curl -H "Accept: application/json" http://localhost:8080/metrics/application{
"io.helidon.examples.quickstart.mp.GreetingCards.any-card":2
}- 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.
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();
}
}- Specify a custom name for the
Countermetric and setabsolute=trueto remove the path prefix from the name. - Add the
@Meteredannotation to get aMetermetric. - Add the
@Timedannotation to get aTimermetric.
curl http://localhost:8080/cards
curl http://localhost:8080/cards
curl -H "Accept: application/json" http://localhost:8080/metrics/application{
"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
}
}- The
Metermetric includes the count field (it is a superset ofCounter). - The
Timermetric includes theMeterfields (it is a superset ofMeter).
Reusing Metrics
You can share a metric across multiple endpoints simply by specifying the same metric annotation as demonstrated below.
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();
}
}- The
/birthdayendpoint uses aCountermetric, namedspecialEventCard. - The
/weddingendpoint uses the sameCountermetric, namedspecialEventCard.
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/metrics/application:{
"anyCard": 1,
"specialEventCard": 2
}- Notice that
specialEventCardcount is two, since you accessed/cards/weddingand/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.
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();
}
}- This class is annotated with
@Counted, which aggregates count data from all the method that have aCountannotation. - Use
absolute=trueto remove path prefix for method-level annotations. - Add a method with a
Countermetric to get birthday cards.
curl http://localhost:8080/cards
curl http://localhost:8080/cards/birthday
curl -H "Accept: application/json" http://localhost:8080/metrics/application/metrics/application:{
"anyCard": 1,
"birthdayCard": 1,
"io.helidon.examples.quickstart.mp.totalCards.GreetingCards": 2
}- The
totalCardscount is a total of all the method-levelCountermetrics. 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.
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();
}
}
}- A
Countermetric field,cacheHits, is automatically injected by Helidon. - Call
updateStats()to update the cache hits. - Call
updateStats()to update the cache hits. - Randomly increment the
cacheHitscounter.
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/metrics/application:{
"anyCard": 2,
"birthdayCard": 3,
"cacheHits": 2,
"io.helidon.examples.quickstart.mp.totalCards.GreetingCards": 5
}- 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.
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();
}
}- This managed object must be application scoped to properly register and use the
Gaugemetric. - Declare an
AtomicLongfield to hold the start time of the application. - Initialize the application start time.
- Return the application
appUpTimeSecondsmetric, which will be included in the application metrics.
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();
}
}curl -H "Accept: application/json" http://localhost:8080/metrics/application/metrics/application:{
"cardCount": 0,
"io.helidon.examples.quickstart.mp.GreetingCardsAppMetrics.appUpTimeSeconds": 6
}- 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.
docker build -t helidon-metrics-mp .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- A service of type
NodePortthat serves the default routes on port8080. - An annotation that will allow Prometheus to discover and scrape the application pod.
- A deployment with one replica of a pod.
kubectl apply -f ./metrics.yamlkubectl get service/helidon-metricsNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
helidon-metrics NodePort 10.99.159.2 <none> 8080:31143/TCP 8s - A service of type
NodePortthat serves the default routes on port31143.
30116, your port will likely be different:curl http://localhost:31143/metricsLeave 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.
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_NAMEYou 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 46skubectl --namespace default port-forward $POD_NAME 7090:9090Now 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.
helm delete --purge metricskubectl delete -f ./metrics.yamlSummary
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:
Helidon Javadoc at ./apidocs/index.html?overview-summary.html