- Metrics SE Guide
This guide describes how to create a sample Helidon SE project that can be used to run some basic examples using both built-in and custom metrics.
What you need
| About 30 minutes |
| Helidon Prerequisites |
| Helm |
Create a sample Helidon SE project
Use the Helidon SE Maven archetype to create a simple project that can be used for the examples in this guide.
mvn archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=io.helidon.archetypes \
-DarchetypeArtifactId=helidon-quickstart-se \
-DarchetypeVersion=1.4.12 \
-DgroupId=io.helidon.examples \
-DartifactId=helidon-quickstart-se \
-Dpackage=io.helidon.examples.quickstart.seUsing 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 following example will demonstrate how to use the built-in metrics. All examples are executed from the root directory of your project (helidon-quickstart-se). The generated source code is already configured for both metrics and health-checks, but the following example removes health-checks.
<dependency>
<groupId>io.helidon.metrics</groupId>
<artifactId>helidon-metrics</artifactId>
</dependency>Main.createRouting method with the following code: private static Routing createRouting(Config config) {
GreetService greetService = new GreetService(config);
return Routing.builder()
.register(JsonSupport.create())
.register(MetricsSupport.create())
.register("/greet", greetService)
.build();
}- Register the built-in base and vendor metrics.
mvn package -DskipTests=true
java -jar target/helidon-quickstart-se.jarMetrics 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.
curl http://localhost:8080/metrics# 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
...You can get the same data in JSON format.
curl -H "Accept: application/json" http://localhost:8080/metrics{
"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
}
}
}You can get a single metric by specifying the name in the URL path.
grpc.requests.meter metric:curl -H "Accept: application/json" http://localhost:8080/metrics/vendor/grpc.requests.meter{
"grpc.requests.meter": {
"count": 0,
"meanRate": 0.0,
"oneMinRate": 0.0,
"fiveMinRate": 0.0,
"fifteenMinRate": 0.0
}
}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:
- 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, 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
This section demonstrates how to use application-specific metrics and integrate them with Helidon. It is the application’s responsibility to create and update the metrics at runtime. The application has complete control over when and how each metric is used. For example, an application may use the same counter for multiple methods, or one counter per method. Helidon maintains an application MetricRegistry which is used to manage all of the application metrics. Helidon returns these metrics in response to a /metrics/application REST request.
In all of these examples, the scope and lifetime of the metric is at the application-level. Each metric, except Gauge, is updated in response to a REST request and the contents of the metric is cumulative.
Counter metric
The Counter metric is a monotonically increasing or decreasing number. The following example will demonstrate how to use a Counter to track the number of times the /cards endpoint is called.
GreetingCards with the following code:package io.helidon.examples.quickstart.se;
import io.helidon.metrics.RegistryFactory;
import io.helidon.webserver.Routing;
import io.helidon.webserver.ServerRequest;
import io.helidon.webserver.ServerResponse;
import io.helidon.webserver.Service;
import java.util.Collections;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import org.eclipse.microprofile.metrics.Counter;
import org.eclipse.microprofile.metrics.MetricRegistry;
public class GreetingCards implements Service {
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
private final Counter cardCounter;
GreetingCards() {
RegistryFactory metricsRegistry = RegistryFactory.getInstance();
MetricRegistry appRegistry = metricsRegistry.getRegistry(MetricRegistry.Type.APPLICATION);
cardCounter = appRegistry.counter("cardCount");
}
@Override
public void update(Routing.Rules rules) {
rules.get("/", this::getDefaultMessageHandler);
}
private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) {
cardCounter.inc();
sendResponse(response, "Here are some cards ...");
}
private void sendResponse(ServerResponse response, String msg) {
JsonObject returnObject = JSON.createObjectBuilder().add("message", msg).build();
response.send(returnObject);
}
}- Import metrics classes.
- Declare a
Countermember variable. - Create and register the
Countermetric in theMetricRegistry. ThisCounterwill exist for the lifetime of the application. - Increment the count.
Main.createRouting method as follows: private static Routing createRouting(Config config) {
MetricsSupport metrics = MetricsSupport.create();
GreetService greetService = new GreetService(config);
return Routing.builder()
.register(JsonSupport.create())
.register(metrics)
.register("/greet", greetService)
.register("/cards", new GreetingCards())
.build();
}- Add the
GreetingCardsservice to theRouting.builder. Helidon will route any REST requests with the/cardsroot path to theGreetingCardsservice.
curl http://localhost:8080/cards
curl -H "Accept: application/json" http://localhost:8080/metrics/application{
"cardCount": 1
}- The count value is one since the method was called once.
Meter metric
The Meter metric is used to measure throughput, the number of times an event occurs within a certain time period. When a Meter object is created, its internal clock starts running. That clock is used to calculate the various rates stored this metric. The Meter also includes the count field from the Counter metric. When you mark an event, the count is incremented.
The following example marks an event each time the /cards endpoint is called.
GreetingCards class with the following code:package io.helidon.examples.quickstart.se;
import io.helidon.metrics.RegistryFactory;
import io.helidon.webserver.Routing;
import io.helidon.webserver.ServerRequest;
import io.helidon.webserver.ServerResponse;
import io.helidon.webserver.Service;
import java.util.Collections;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import org.eclipse.microprofile.metrics.Meter;
import org.eclipse.microprofile.metrics.MetricRegistry;
public class GreetingCards implements Service {
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
private final Meter cardMeter;
GreetingCards() {
RegistryFactory metricsRegistry = RegistryFactory.getInstance();
MetricRegistry appRegistry = metricsRegistry.getRegistry(MetricRegistry.Type.APPLICATION);
cardMeter = appRegistry.meter("cardMeter");
}
@Override
public void update(Routing.Rules rules) {
rules.get("/", this::getDefaultMessageHandler);
}
private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) {
cardMeter.mark();
sendResponse(response, "Here are some cards ...");
}
private void sendResponse(ServerResponse response, String msg) {
JsonObject returnObject = JSON.createObjectBuilder().add("message", msg).build();
response.send(returnObject);
}
}- Import metrics classes.
- Declare a
Metermember variable. - Create and register the
Metermetric in theMetricRegistry. - Mark the occurrence of an event.
Note: you can specify a count parameter such as mark(100) to mark multiple events.
curl http://localhost:8080/cards
curl http://localhost:8080/cards
curl http://localhost:8080/cards
curl -H "Accept: application/json" http://localhost:8080/metrics/application{
"cardMeter": {
"count": 3,
"meanRate": 0.17566568722974535,
"oneMinRate": 0.04413761384322548,
"fiveMinRate": 0.009753212003766951,
"fifteenMinRate": 0.0033056752265846544
}
}- The
Metermetric has a set of fields to show various rates, along with the count. - The
/cardsendpoint was called three times.
Timer metric
The Timer metric aggregates durations, provides timing statistics, and includes throughput statistics using an internal Meter metric. The Timer measures duration in nanoseconds. In the following example, a Timer metric is used to measure the duration of a method’s execution. Whenever the REST /cards endpoint is called, the Timer will be updated with additional timing information.
GreetingCards class with the following code:package io.helidon.examples.quickstart.se;
import io.helidon.metrics.RegistryFactory;
import io.helidon.webserver.Routing;
import io.helidon.webserver.ServerRequest;
import io.helidon.webserver.ServerResponse;
import io.helidon.webserver.Service;
import java.util.Collections;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import org.eclipse.microprofile.metrics.MetricRegistry;
import org.eclipse.microprofile.metrics.Timer;
public class GreetingCards implements Service {
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
private final Timer cardTimer;
GreetingCards() {
RegistryFactory metricsRegistry = RegistryFactory.getInstance();
MetricRegistry appRegistry = metricsRegistry.getRegistry(MetricRegistry.Type.APPLICATION);
cardTimer = appRegistry.timer("cardTimer");
}
@Override
public void update(Routing.Rules rules) {
rules.get("/", this::getDefaultMessageHandler);
}
private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) {
Timer.Context timerContext = cardTimer.time();
sendResponse(response, "Here are some cards ...");
response.whenSent().thenAccept(res -> timerContext.stop());
}
private void sendResponse(ServerResponse response, String msg) {
JsonObject returnObject = JSON.createObjectBuilder().add("message", msg).build();
response.send(returnObject);
}
}- Import metrics classes.
- Declare a
Timermember variable. - Create and register the
Timermetric in theMetricRegistry. - Start the timer.
- Stop the timer.
curl http://localhost:8080/cards
curl -H "Accept: application/json" http://localhost:8080/metrics/application{
"cardTimer": {
"count": 1,
"meanRate": 0.03843465264149663,
"oneMinRate": 0.014712537947741825,
"fiveMinRate": 0.0032510706679223173,
"fifteenMinRate": 0.0011018917421948848,
"min": 40876527,
"max": 40876527,
"mean": 40876527,
"stddev": 0.0,
"p50": 40876527,
"p75": 40876527,
"p95": 40876527,
"p98": 40876527,
"p99": 40876527,
"p999": 40876527
}
}- These are the same fields used by
Meter. - These are the
Timerfields that measure the duration of thegetDefaultMessageHandlermethod. Some of these values will change each time you invoke the/cardsendpoint.
Histogram metric
The Histogram metric calculates the distribution of a set of values within ranges. This metric does not relate to time at all. The following example will record a set of random numbers in a Histogram metric when the /cards endpoint is invoked.
GreetingCards class with the following code:package io.helidon.examples.quickstart.se;
import io.helidon.metrics.RegistryFactory;
import io.helidon.webserver.Routing;
import io.helidon.webserver.ServerRequest;
import io.helidon.webserver.ServerResponse;
import io.helidon.webserver.Service;
import java.util.Collections;
import java.util.Random;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import org.eclipse.microprofile.metrics.Histogram;
import org.eclipse.microprofile.metrics.MetricRegistry;
public class GreetingCards implements Service {
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
private final Histogram cardHistogram;
GreetingCards() {
RegistryFactory metricsRegistry = RegistryFactory.getInstance();
MetricRegistry appRegistry = metricsRegistry.getRegistry(MetricRegistry.Type.APPLICATION);
cardHistogram = appRegistry.histogram("cardHistogram");
}
@Override
public void update(Routing.Rules rules) {
rules.get("/", this::getDefaultMessageHandler);
}
private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) {
Random r = new Random();
for (int i = 0; i < 1000; i++) {
cardHistogram.update(1 + r.nextInt(25));
}
sendResponse(response, "Here are some cards ...");
}
private void sendResponse(ServerResponse response, String msg) {
JsonObject returnObject = JSON.createObjectBuilder().add("message", msg).build();
response.send(returnObject);
}
}- Import metrics classes.
- Declare a
Histogrammember variable. - Create and register the
Histogrammetric in theMetricRegistry. - Loop, loading the histogram with numbers.
- Update the
Histogrammetric with a random number.
curl http://localhost:8080/cards
curl -H "Accept: application/json" http://localhost:8080/metrics/application{
"cardHistogram": {
"count": 1000,
"min": 1,
"max": 25,
"mean": 12.743999999999915,
"stddev": 7.308793607702962,
"p50": 13.0,
"p75": 19.0,
"p95": 24.0,
"p98": 25.0,
"p99": 25.0,
"p999": 25.0
}
}- This is the histogram data. Some of these values will change each time you invoke the
/cardsendpoint.
Gauge metric
The Gauge metric measures a discreet value at a point in time, such as a temperature. The metric is not normally tied to a REST endpoint, rather it should be registered during application startup. When the /metrics/application endpoint is invoked, Helidon will call the getValue method of each registered Gauge. The following example demonstrates how a Gauge is used to get the current temperature.
Main.java and replace the Main.createRouting method with the following code:import io.helidon.metrics.RegistryFactory;
import java.util.Random;
import org.eclipse.microprofile.metrics.Gauge;
import org.eclipse.microprofile.metrics.MetricRegistry;
...
private static Routing createRouting(Config config) {
MetricsSupport metrics = MetricsSupport.create();
RegistryFactory metricsRegistry = RegistryFactory.getInstance();
MetricRegistry appRegistry = metricsRegistry.getRegistry(MetricRegistry.Type.APPLICATION);
appRegistry.register("temperature", (Gauge<Integer>)() -> new Random().nextInt(100));
GreetService greetService = new GreetService(config);
return Routing.builder()
.register(JsonSupport.create())
.register(metrics) // Metrics at "/metrics"
.register("/greet", greetService)
.register("/cards", new GreetingCards())
.build();
}- Register the
Gauge, providing a lambda function that will return a random temperature.
GreetingCards class with the following code to use the Counter metric which will simplify the JSON output:package io.helidon.examples.quickstart.se;
import io.helidon.metrics.RegistryFactory;
import io.helidon.webserver.Routing;
import io.helidon.webserver.ServerRequest;
import io.helidon.webserver.ServerResponse;
import io.helidon.webserver.Service;
import java.util.Collections;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import org.eclipse.microprofile.metrics.Counter;
import org.eclipse.microprofile.metrics.MetricRegistry;
public class GreetingCards implements Service {
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
private final Counter cardCounter;
GreetingCards() {
RegistryFactory metricsRegistry = RegistryFactory.getInstance();
MetricRegistry appRegistry = metricsRegistry.getRegistry(MetricRegistry.Type.APPLICATION);
cardCounter = appRegistry.counter("cardCount");
}
@Override
public void update(Routing.Rules rules) {
rules.get("/", this::getDefaultMessageHandler);
}
private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) {
cardCounter.inc();
sendResponse(response, "Here are some cards ...");
}
private void sendResponse(ServerResponse response, String msg) {
JsonObject returnObject = JSON.createObjectBuilder().add("message", msg).build();
response.send(returnObject);
}
}curl http://localhost:8080/cards
curl -H "Accept: application/json" http://localhost:8080/metrics/application/metrics/application:{
"cardCount": 1,
"temperature": 11
}- The current temperature is returned. Invoke the
/metrics/applicationendpoint again and you should get a different value.
Integration with Kubernetes and Prometheus
The following example shows how to integrate the Helidon SE application with Kubernetes.
docker build -t helidon-metrics-se .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-se
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
31143, 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. In this exercise, you will 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 SE application using various combinations of metrics and scopes.
Access metrics for all three scopes: base, vendor, and application
Configure metrics that are updated by the application when an application REST endpoint is invoked
Configure a
GaugemetricIntegrate Helidon metrics with Kubernetes and Prometheus
Refer to the following references for additional information:
MicroProfile Metrics specification at https://github.com/eclipse/microprofile-metrics/releases/tag/1.1
MicroProfile Metrics Javadoc at https://javadoc.io/doc/org.eclipse.microprofile.metrics/microprofile-metrics-api/1.1.1
Helidon Javadoc at https://helidon.io/docs/latest/apidocs/index.html?overview-summary.html