Contents

Overview

Helidon MP metrics implements the MicroProfile Metrics specification, providing:

  • a unified way for MicroProfile servers to export monitoring data—​telemetry—​to management agents, and

  • a unified Java API which all application programmers can use to register and update metrics to expose telemetry data from their services.

  • support for metrics-related annotations.

Learn more about the MicroProfile Metrics specification.

Metrics is one of the Helidon observability features.

Note
Recommended Configuration Setting

Beginning with Helidon 4.1, strongly consider assigning the config setting

metrics.gc-time-type = gauge

so your service complies with the MicroProfile Metrics 5.1 specification. See the longer discussion below in the Configuration section.

Maven Coordinates

To enable metrics, add the following dependency to your project’s pom.xml (see Managing Dependencies).

<dependency>
    <groupId>io.helidon.microprofile.metrics</groupId>
    <artifactId>helidon-microprofile-metrics</artifactId>
</dependency>

Adding this dependency packages the full-featured metrics implementation with your service.

Usage

Instrumenting Your Service

You add metrics to your service in these ways:

  • Annotate bean methods—​typically your REST resource endpoint methods (the Java code that receives incoming REST requests); Helidon automatically registers these metrics and updates them when the annotated methods are invoked via CDI.

  • Write code which explicitly invokes the metrics API to register metrics, retrieve previously-registered metrics, and update metric values.

  • Configure some simple REST.request metrics which Helidon automatically registers and updates for all REST resource endpoints.

Later sections of this document describe how to do each of these.

Metric Types

Helidon supports meters described by the MicroProfile Metrics spec and summarized in the following table:

Table 1. Types of Metrics

Metric Type

Description

Related MicroProfile annotation

Counter

Monotonically-increasing long value.

@Counted

Histogram

Summary of samples each with a long value. Reports aggregate information over all samples (count, total, mean, max) as well as the distribution of sample values using percentiles and bucket counts.

(none)

Timer

Accumulation of short-duration (typically under a minute) intervals. Typically updated using a Java Duration or by recording the time taken by a method invocation or lambda. Reports the count, total time, max, and mean; provides a histogram of the samples.

@Timed

Gauge<? extends Number>

View of a value that is assignment-compatible with a subtype of Java Number. The underlying value is updated by code elsewhere in the system, not by invoking methods on the gauge itself.

@Gauge

Categorizing Types of Metrics

Helidon distinguishes among scopes, or categories, of metrics as described in the MP metrics specification.

Helidon includes metrics in the built-in scopes described below. Applications often register their own metrics in the application scope but can create their own scopes and register metrics within them.

Table 2. Built-in metric scopes
Built-in Scope Typical Usage

base

OS or Java runtime measurements (available heap, disk space, etc.). Mandated by the MP metrics specification

vendor

Implemented by vendors, including the REST.request metrics and other key performance indicator measurements (described in later sections).

application

Declared via annotations or programmatically registered by your service code.

When you add metrics annotations to your service code, Helidon registers the resulting metrics in the application scope.

Metric Registries

A metric registry collects registered metrics of a given scope. Helidon supports one metrics registry for each scope.

When you add code to your service to create a metric programmatically, the code first locates the appropriate registry and then registers the metric with that registry.

Retrieving Metrics Reports from your Service

When you add the metrics dependency to your project, Helidon automatically provides a built-in REST endpoint /metrics which responds with a report of the registered metrics and their values.

Clients can request a particular output format.

Table 3. Formats for /metrics output
Format Requested by

OpenMetrics (Prometheus)

default (text/plain)

JSON

Header Accept: application/json

Clients can also limit the report by specifying the scope as a query parameter in the request URL:

  • /metrics?scope=base

  • /metrics?scope=vendor

  • /metrics?scope=application

Further, clients can narrow down to a specific metric name by adding the name as another query parameter, such as /metrics?scope=application&name=myCount.

Example Reporting: Prometheus format
curl -s -H 'Accept: text/plain' -X GET http://localhost:8080/metrics
# HELP classloader_loadedClasses_count Displays the number of classes that are currently loaded in the Java virtual machine.
# TYPE classloader_loadedClasses_count gauge
classloader_loadedClasses_count{mp_scope="base",} 5297.0

See the summary of the OpenMetrics and Prometheus Format for more information.

Example Reporting: JSON format
curl -s -H 'Accept: application/json' -X GET http://localhost:8080/metrics
JSON response:
{
   "base" : {
      "memory.maxHeap" : 3817865216,
      "memory.committedHeap" : 335544320
    }
}

In addition to your application metrics, the reports contain other metrics of interest such as system and VM information.

OpenMetrics and Prometheus Format

The OpenMetrics format and the Prometheus exposition format are very similar in most important respects but are not identical. This brief summary treats them as the same.

The OpenMetrics/Prometheus format represents each metric using three lines of output as summarized in the following table.

Table 4. OpenMetrics/Prometheus format
Line prefix Purpose Format

# TYPE

Displays the scope, name, and type of the metric

TYPE <scope>:<output-name> <metric-type>

# HELP

Displays the scope, name, and description of the metric

HELP <scope>:<output-name> <registered description>

(none)

Displays the scope, metric ID, and current value of the metric

<scope>:<output-name> <current value>

The OpenMetrics/Prometheus output converts metric IDs in these ways:

  • Names in camel case are converted to "snake case" and dots are converted to underscores.

  • Names include any units specified for the metric.

  • For percentiles, the ID includes a tag identifying which percentile the line of output describes.

As the earlier example output showed, for a metric with multiple values, such as a timer or a histogram, (with, among others, max, mean, and count), the OpenMetrics/Prometheus output reports a "metric family" which includes a separate family member metric for each of the multiple values. The name for each member in the family is derived from the registered name for the metric plus a suffix indicating which one of the metric’s multiple values the line refers to.

The following table summarizes the naming for each metric type.

Table 5. OpenMetrics/Prometheus Metric Naming
Metric Type Example registered name Metric family member Name Suffix Example displayed name

Counter

requests.count

count

_total

requests_count_total

Histogram

nameLengths

count

_count

nameLengths_count

sum

_sum

nameLengths_sum

max

_max

nameLengths_max

percentile

none

nameLengths{mp_scope="base",quantile="0.5",}

Gauge

classloader.loadedClasses.count

value

none

classloader_loadedClasses_count

Timer 1

vthreads.recentPinned

count

_count

vthreads_recentPinned_seconds_count

sum

_sum

vthreads_recentPinned_seconds_sum

max

_max

vthreads_recentPinned_seconds_max

percentile

none

vthreads_recentPinned_seconds{mp_scope="base",quantile="0.5",}

1 The OpenMetrics/Prometheus output format reports a timer as a summary with units of seconds.

JSON Format

Unlike OpenMetrics/Prometheus output, which combines the data and the metadata in a single response, you use an HTTP GET request to retrieve metrics JSON data and an OPTIONS request to retrieve metadata in JSON format.

Helidon groups metrics in the same scope together in JSON output as shown in the following example.

JSON metrics output structured by scope (partial)
{
  "application": {  (1)
    "getTimer": {
      "type": "timer",
      "unit": "seconds",
      "description": "Timer for getting the default greeting"
    }
  },
  "vendor": {       (1)
    "requests.count": {
      "type": "counter",
      "description": "Each request (regardless of HTTP method) will increase this counter"
    }
  },
  "base": {         (1)
    "cpu.systemLoadAverage": {
      "type": "gauge",
      "description": "Displays the system load average for the last minute."
    },
    "classloader.loadedClasses.count": {
      "type": "gauge",
      "description": "Displays the number of classes that are currently loaded in the Java virtual machine."
    }
  }
}
  1. Note the application, vendor, and base sections.

If an HTTP request selects by scope, the output omits the extra level of structure that identifies the scope as shown in the following example.

JSON metrics output for the base scope (partial)
{
  "cpu.systemLoadAverage": {
    "type": "gauge",
     "description": "Displays the system load average for the last minute."
  },
  "classloader.loadedClasses.count": {
    "type": "gauge",
    "description": "Displays the number of classes that are currently loaded in the Java virtual machine."
  }
}
Understanding the JSON Metrics Data Format

The Helidon JSON format expresses each metric as either a single value (for example, a counter) or a structure with multiple values (for example, a timer).

JSON output for a single-valued metric (for example, Counter)
"requests.count": 5
JSON output for a multi-valued metric (for example, Timer)
"getTimer": {
  "count": 3,
  "max": 0.0030455,
  "mean": 0.0011060836666666666,
  "elapsedTime": 0.003318251,
  "p0.5": 0.000151552,
  "p0.75": 0.003141632,
  "p0.95": 0.003141632,
  "p0.98": 0.003141632,
  "p0.99": 0.003141632,
  "p0.999": 0.003141632
}

By default, Helidon formats time values contained in JSON output as seconds. You can change this behavior as described below.

Understanding the JSON Metrics Metadata Format

Access the metrics endpoint with an HTTP OPTIONS request and the Accept: application/json header to retrieve metadata in JSON format.

Example Counter metadata
"requests.count": {
  "type": "counter",
  "description": "Each request (regardless of HTTP method) will increase this counter"
    }
Example Timer metadata
"getTimer": {
  "type": "timer",
  "unit": "seconds",
  "description": "Timer for getting the default greeting"
}

Generally, the output for a given metric reflects only the metadata that the application or Helidon code explicitly set on that metric.

One exception is that metadata for a timer always includes the unit field. By default, Helidon formats timer data in JSON output as seconds, regardless of any explicit baseUnit setting applied to the timers. But as described below you can change this behavior which can lead to different timers being formatted using different units. Checking the metadata is the only way to know for sure what units Helidon used to express a given timer, so Helidon always includes unit in timer metadata.

Controlling JSON Timer Output

By default, Helidon expresses timer data as seconds.

You can change this using configuration:

Setting default timer units for JSON in META-INF/microprofile-config.properties
metrics.timers.json-units-default=units # (1)
  1. For units specify any valid name for a TimeUnit value (SECONDS, MILLISECONDS, etc.)

If you have configured json-units-default, Helidon formats each timer’s data as follows:

  1. If code set baseUnit on the timer, Helidon uses those units for that timer.

  2. Otherwise, Helidon uses the default units you configured.

To enable the JSON output behavior from Helidon 3, specify json-units-default as NANOSECONDS.

API

The MicroProfile Metrics API prescribes all the standard interfaces related to metrics. This section summarizes a few key points about using that API and explains some Helidon-specific interfaces.

Metrics Annotations

You can very easily instrument your service and refer to registered metrics by annotating methods to be measured and injecting metrics which your code needs to observe.

Metric-defining Annotations

The MicroProfile Metrics specification describes several metric types you can create using annotations, summarized in the following table:

Table 6. Metrics Annotations
Annotation Usage

@Counted

Automatically registers a monotonically-increasing Counter and increments it with each invocation of the annotated constructor or method. 1

@Gauge

Automatically registers a Gauge whose value is provided by the annotated method. Code elsewhere in the system updates the underlying value.

@Timed

Automatically registers a Timer and updates it with each invocation of the annotated constructor or method. 1

1 Place annotations on constructors or methods to measure those specific executables. If you annotate the class instead, Helidon applies that annotation to all constructors and methods which the class declares.

Metric-referencing Annotations

To get a reference to a specific metric, use a metric-referencing annotation in any bean, including your REST resource classes.

You can @Inject a field of the correct type. Helidon uses the MicroProfile Metrics naming conventions to select which specific metric to inject. Use the @Metric annotation to control that selection.

You can also add @Metric on a constructor or method parameter to trigger injection there.

Helidon automatically looks up the metric referenced from any injection site and provides a reference to the metric. Your code then simply invokes methods on the injected metric.

The MetricRegistry API

To register or look up metrics programmatically, your service code uses the MetricRegistry instance for the scope of interest: base, vendor, application, or a custom scope.

Either of the following techniques gets a MetricRegistry reference. Remember that injection works only if the class is a bean so CDI can inject into it.

  • @Inject MetricRegistry, optionally using @RegistryScope to indicate the registry scope.

    Injecting the default MetricRegistry (for the application scope)
    class Example {
    
        @Inject
        private MetricRegistry applicationRegistry;
    }
    Injecting a non-default MetricRegistry
    class Example {
    
        @RegistryScope(scope = "myCustomScope")
        @Inject
        private MetricRegistry myCustomRegistry;
    }
  • Get a Helidon RegistryFactory instance and invoke its getRegistry method.

    Obtain the RegistryFactory using either of the following techniques:

    • @Inject RegistryFactory.

      Getting the RegistryFactory using injection
      class InjectExample {
      
          @Inject
          private RegistryFactory registryFactory;
      
          private MetricRegistry findRegistry(String scope) {
              return registryFactory.getRegistry(scope);
          }
      }
    • Invoke the static getInstance() method on the RegistryFactory class.

      Getting the RegistryFactory programmatically
      class Example {
      
          private MetricRegistry findRegistry(String scope) {
              return RegistryFactory.getInstance().getRegistry(scope);
          }
      }

Once it has a reference to a MetricRegistry your code can use the reference to register new metrics, look up previously-registered metrics, and remove metrics.

Working with Metrics in CDI Extensions

You can work with metrics inside your own CDI extensions, but be careful to do so at the correct point in the CDI lifecycle. Configuration can influence how the metrics system behaves, as the configuration section below explains. Your code should work with metrics only after the Helidon metrics system has initialized itself using configuration. One way to accomplish this is to deal with metrics in a method that observes the Helidon RuntimeStart CDI event, which the extension example below illustrates.

Configuration

To control how the Helidon metrics subsystem behaves, add a metrics section to your META-INF/microprofile-config.properties file.

Certain default configuration values depend on the fact that you are using Helidon MP as described in the second table below.

This is a standalone configuration type, prefix from configuration root: metrics

This type provides the following service implementations:

  • io.helidon.webserver.observe.spi.ObserveProvider

Configuration options

Table 7. Optional configuration options
key type default value description

app-name

string

 

Value for the application tag to be added to each meter ID.

app-tag-name

string

 

Name for the application tag to be added to each meter ID.

built-in-meter-name-format

BuiltInMeterNameFormat (SNAKE, CAMEL)

BuiltInMeterNameFormat.CAMEL

Output format for built-in meter names.

BuiltInMeterNameFormat.SNAKE selects "snake_case" which does not conform to the MicroProfile
Metrics specification.

Allowed values:

  • SNAKE: Snake-case.

  • CAMEL: Camel-case (which is compatible with the MicroProfile Metrics spec).

enabled

boolean

true

Whether metrics functionality is enabled.

endpoint

string

metrics

gc-time-type

GcTimeType (GAUGE, COUNTER)

GcTimeType.COUNTER

Deprecated Whether the gc.time meter should be registered as a gauge (vs. a counter). The gc.time meter is inspired by the MicroProfile Metrics spec, in which the meter was originally checked to be a counter but starting in 5.1 was checked be a gauge. For the duration of Helidon 4.x users can choose which type of meter Helidon registers for gc.time. @deprecated Provided for backward compatibility only; no replacement

Allowed values:

  • GAUGE: Implement the meter as a gauge. This is backward-incompatible with Helidon 4.0.x releases but complies with MicroProfile 5.1.

  • COUNTER: Implement the meter as a counter. This is backward-compatible with Helidon 4.0.x releases but does not comply with MicroProfile 5.1.

key-performance-indicators

 

Key performance indicator metrics settings.

permit-all

boolean

true

Whether to allow anybody to access the endpoint.

See roles()

rest-request-enabled

boolean

 

Deprecated Whether automatic REST request metrics should be measured (as indicated by the deprecated config key rest-request-enabled, the config key using a hyphen instead of a dot separator).

@deprecated Use rest-request.enabled instead.

rest-request.enabled

boolean

false

Whether automatic REST request metrics should be measured.

roles

string[]

observe

Hints for role names the user is expected to be in.

scoping

 

Settings related to scoping management.

tags

 

Global tags.

timers.json-units-default

TimeUnit (NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS)

 

Default units for timer output in JSON if not specified on a given timer.

If the configuration key is absent, the Helidon JSON output uses java.util.concurrent.TimeUnit.SECONDS. If the configuration key is present, Helidon formats each timer using that timer’s specific units (if set) and the config value otherwise.

virtual-threads.enabled

boolean

false

Whether Helidon should expose meters related to virtual threads.

virtual-threads.pinned.threshold

Duration

PT0.020S

Threshold for sampling pinned virtual threads to include in the pinned threads meter.

Table 8. Default Values Specific to Helidon MP
Key Default Value

app-tag-name

mp_app

scoping.tag-name

mp_scope

scoping.default

application

Controlling the Metric Type for gc.time

To date Helidon 4 releases have implemented the system-provided metric gc.time as a counter. In fact, a gauge is more suitable for the approximate time the JVM has spent doing garbage collection, and beginning with MicroProfile Metrics 5.1 the TCK relies on gc.time being a gauge.

Helidon 4.3.0-SNAPSHOT continues to use a counter by default to preserve backward compatibility, but you can choose to use a gauge by setting the configuration property metrics.gc-time-type to gauge. You can also set the config property to counter which is the default.

Why should you care? In fact, this distinction might not make a difference for many users. But for others the differences between the programmatic APIs for Counter and Gauge would affect application code that works directly with the gc-time metric. Further, the difference in output—​particularly in the OpenMetrics/Prometheus format—​might affect their application or downstream monitoring tools.

The ability to choose the metric type for gc.time is deprecated and is planned for removal in a future major release of Helidon at which time Helidon will always use a gauge.

Examples

Helidon MP includes a pre-written example application illustrating enabling/disabling metrics using configuration.

The rest of this section contains other examples of working with metrics:

Example Application Code

Adding Method-level Annotations

The following example adds a new resource class, GreetingCards, to the Helidon MP QuickStart example. It shows 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:
@Path("/cards") //(1)
@RequestScoped // (2)
public class GreetingCards {

    private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Map.of());

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

    private JsonObject createResponse(String msg) {
        return JSON.createObjectBuilder().add("message", msg).build();
    }
}
  1. This class is annotated with Path which sets the path for this resource as /cards.

  2. 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.

  3. 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.

Build and run the application
mvn package
java -jar target/helidon-quickstart-mp.jar
Access the application endpoints
curl http://localhost:8080/cards
curl http://localhost:8080/cards
curl -H "Accept: application/json"  'http://localhost:8080/metrics?scope=application'
JSON response:
{
  "io.helidon.examples.quickstart.mp.GreetingCards.any-card": 2, //  (1)
  "personalizedGets": 0,
  "allGets": {
    "count": 0,
    "elapsedTime": 0,
    "max": 0,
    "mean": 0
  }
}
  1. The any-card count is two, since you invoked the endpoint twice. The other metrics are from the SimpleGreetResource class.

Note
Notice the counter name is fully qualified with the class and method names. You can remove the prefix by using the absolute=true field in the @Counted annotation. You must use absolute=false (the default) for class-level annotations.

Additional Method-level Metrics

You can also use the @Timed` annotation with a method. For the following example. you can just annotate the same method with @Timed. Timers significant information about the measured methods, but at a cost of some overhead and more complicated output.

Note that when using multiple annotations on a method, you must give the metrics different names as shown below, although they do not have to be absolute.

Update the GreetingCards class with the following code:
@Path("/cards")
@RequestScoped
public class GreetingCards {

    private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Map.of());

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

    private JsonObject createResponse(String msg) {
        return JSON.createObjectBuilder().add("message", msg).build();
    }
}
  1. Specify a custom name for the Counter metric and set absolute=true to remove the path prefix from the name. <2>Add the @Timed annotation to get a Timer metric.

Build and run the application
mvn package
java -jar target/helidon-quickstart-mp.jar
Access the application endpoints
curl http://localhost:8080/cards
curl http://localhost:8080/cards
curl -H "Accept: application/json"  'http://localhost:8080/metrics?scope=application'
JSON response:
{
  "cardTimer": {
    "count": 2,
    "elapsedTime": 0.002941925,
    "max": 0.002919973,
    "mean": 0.0014709625
  },
  "personalizedGets": 0,
  "allGets": {
    "count": 0,
    "elapsedTime": 0,
    "max": 0,
    "mean": 0
  },
  "cardCount": 2
}

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:
@Path("/cards")
@RequestScoped
@Counted(name = "totalCards") // (1)
public class GreetingCards {

    private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Map.of());

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

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

    private JsonObject createResponse(String msg) {
        return JSON.createObjectBuilder().add("message", msg).build();
    }
}
  1. This class is now annotated with @Counted, which aggregates count data from all the method that have a Count annotation.

  2. Use absolute=true to remove path prefix for method-level annotations.

  3. Add a method with a Counter metric to get birthday cards.

Build and run the application
mvn package
java -jar target/helidon-quickstart-mp.jar
Access the application endpoints
curl http://localhost:8080/cards
curl http://localhost:8080/cards/birthday
curl -H "Accept: application/json"  'http://localhost:8080/metrics?scope=application'
JSON response from /metrics?scope=application:
{
  "birthdayCard": 1,
  "personalizedGets": 0,
  "allGets": {
    "count": 0,
    "elapsedTime": 0,
    "max": 0,
    "mean": 0
  },
  "anyCard": 1,
  "io.helidon.examples.quickstart.mp.totalCards.GreetingCards": 2 // (1)
}
  1. The totalCards.GreetingCards 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 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:
@Path("/cards")
@RequestScoped
@Counted(name = "totalCards")
public class GreetingCards {

    private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Map.of());

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

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

    @Path("/birthday")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Counted(absolute = true)
    public JsonObject birthdayCard() throws InterruptedException {
        updateStats();  // (3)
        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(); // (4)
        }
    }
}
  1. A Counter metric field, cacheHits, is automatically injected by Helidon.

  2. Call updateStats() to update the cache hits.

  3. Call updateStats() to update the cache hits.

  4. 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?scope=application'
JSON response from /metrics/application:
{
  "birthdayCard": 3,
  "personalizedGets": 0,
  "allGets": {
    "count": 0,
    "elapsedTime": 0,
    "max": 0,
    "mean": 0
  },
  "anyCard": 2,
  "cacheHits": 2, // (1)
  "io.helidon.examples.quickstart.mp.totalCards.GreetingCards": 5
}
  1. 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?scope=application is invoked, Helidon will return the current value of the metric stored in the MetricRegistry.

The Gauge annotation is different from the other metric annotations. The application must provide a method to return the gauge value in an application-scoped class. When GET /metrics?scope=application is invoked, Helidon will call the Gauge method, using the returned value as the value of the gauge as part of the metrics response.

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

Create a new GreetingCardsAppMetrics class with the following code:
@ApplicationScoped // (1)
public class GreetingCardsAppMetrics {

    private AtomicLong startTime = new AtomicLong(0); // (2)

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

    @Gauge(unit = "TimeSeconds")
    public long appUpTimeSeconds() {
        return Duration.ofMillis(System.currentTimeMillis() - startTime.get()).getSeconds();  // (4)
    }
}
  1. This managed object must be application scoped to properly register and use the annotated Gauge metric.

  2. Declare an AtomicLong field to hold the start time of the application.

  3. Initialize the application start time.

  4. 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:
@Path("/cards")
@RequestScoped
public class GreetingCards {

    private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Map.of());

    @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();
    }
}
Build and run the application, then invoke the application metrics endpoint:
curl -H "Accept: application/json"  'http://localhost:8080/metrics?scope=application'
JSON response from /metrics/application:
{
  "personalizedGets": 0,
  "allGets": {
    "count": 0,
    "elapsedTime": 0,
    "max": 0,
    "mean": 0
  },
  "io.helidon.examples.quickstart.mp.GreetingCardsAppMetrics.appUpTimeSeconds": 23, // (1)
  "cardCount": 0
}
  1. The application has been running for 23 seconds.

Working with Metrics in CDI Extensions

You can work with metrics from your own CDI extension by observing the RuntimeStart event.

CDI Extension that works correctly with metrics
public class MyExtension implements Extension {
    void startup(@Observes @RuntimeStart Object event,  // (1)
                 MetricRegistry metricRegistry) {       // (2)
        metricRegistry.counter("myCounter");         // (3)
    }
}
  1. Declares that your observer method responds to the RuntimeStart event. By this time, Helidon has initialized the metrics system.

  2. Injects a MetricRegistry (the application registry by default).

  3. Uses the injected registry to register a metric (a counter in this case).

Helidon does not prevent you from working with metrics earlier than the RuntimeStart event, but, if you do so, then Helidon might ignore certain configuration settings that would otherwise control how metrics behaves. Instead, consider writing your extension to use earlier lifecycle events (such as ProcessAnnotatedType) to gather and store information about metrics that you want to register. Then your extension’s RuntimeStart observer method would use that stored information to register the metrics you need.

Example Configuration

Metrics configuration is quite extensive and powerful and, therefore, a bit complicated. The rest of this section illustrates some of the most common scenarios:

Disable Metrics Subsystem

Disabling metrics entirely
metrics.enabled=false

Helidon does not update metrics, and the /metrics endpoints respond with 404.

Configuring Virtual Threads Metrics

Enabling Virtual Threads Metrics

Gathering data to compute the metrics for virtual threads is designed to be as efficient as possible, but doing so still imposes a load on the server and by default Helidon does not report metrics related to virtual threads.

To enable the metrics describing virtual threads include a config setting as shown in the following example.

Enabling virtual thread metrics
metrics.virtual-threads.enabled = true
Controlling Measurements of Pinned Virtual Threads

Helidon measures pinned virtual threads only when the thread is pinned for a length of time at or above a threshold. Control the threshold as shown in the example below.

Setting virtual thread pinning threshold to 100 ms
metrics.virtual-threads.pinned.threshold=PT0.100S

The threshold value is a Duration string, such as PT0.100S for 100 milliseconds.

Collecting Basic and Extended Key Performance Indicator (KPI) Metrics

Any time you include the Helidon metrics module in your application, Helidon tracks a basic performance indicator metric: a Counter of all requests received (requests.count)

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

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

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

  • load - a Counter (requests.load) measuring the number of requests worked on (as opposed to received)

  • deferred - a Gauge (requests.deferred) measuring delayed request processing (work on a request was delayed after Helidon received the request)

You can enable and control these metrics using configuration:

Controlling extended KPI metrics
metrics.key-performance-indicators.extended = true
metrics.key-performance-indicators.long-running.threshold-ms = 2000

Enable REST.request Metrics

Controlling REST request metrics
metrics.rest-request.enabled=true

Helidon automatically registers and updates Timer metrics for every REST endpoint in your service.

Additional Information

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 .
Create the Kubernetes YAML specification, named metrics.yaml, with the following content:
kind: Service
apiVersion: v1
metadata:
  name: helidon-metrics # (1)
  labels:
    app: helidon-metrics
  annotations:
    prometheus.io/scrape: "true" # (2)
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 # (3)
  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
  1. A service of type NodePort that serves the default routes on port 8080.

  2. An annotation that will allow Prometheus to discover and scrape the application pod.

  3. A deployment with one replica of a pod.

Create and deploy the application into Kubernetes:
kubectl apply -f ./metrics.yaml
Get the service information:
kubectl get service/helidon-metrics
NAME             TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
helidon-metrics   NodePort   10.99.159.2   <none>        8080:31143/TCP   8s # (1)
  1. 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
Note
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

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
Create a port-forward, so you can access the server URL:
kubectl --namespace default port-forward $POD_NAME 7090:9090

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
Delete the application Kubernetes resources:
kubectl delete -f ./metrics.yaml