From 386385c432ed4704773c6ef92d79674506d01c36 Mon Sep 17 00:00:00 2001 From: xstefank Date: Tue, 4 Feb 2020 09:42:53 +0100 Subject: [PATCH] [WFLY-12809] Quickstart for MP Metrics 2.3 --- microprofile-metrics/README.adoc | 987 ++++++++++++++++++ microprofile-metrics/grafana.json | 418 ++++++++ microprofile-metrics/pom.xml | 81 ++ microprofile-metrics/prometheus.yml | 13 + .../metrics/JaxRsApplication.java | 8 + .../metrics/PrimeNumberChecker.java | 132 +++ .../metrics/MicroProfileMetricsIT.java | 236 +++++ .../src/test/resources/arquillian.xml | 38 + pom.xml | 1 + 9 files changed, 1914 insertions(+) create mode 100644 microprofile-metrics/README.adoc create mode 100644 microprofile-metrics/grafana.json create mode 100644 microprofile-metrics/pom.xml create mode 100644 microprofile-metrics/prometheus.yml create mode 100644 microprofile-metrics/src/main/java/org/wildfly/quickstarts/microprofile/metrics/JaxRsApplication.java create mode 100644 microprofile-metrics/src/main/java/org/wildfly/quickstarts/microprofile/metrics/PrimeNumberChecker.java create mode 100644 microprofile-metrics/src/test/java/org/wildfly/quickstarts/microprofile/metrics/MicroProfileMetricsIT.java create mode 100644 microprofile-metrics/src/test/resources/arquillian.xml diff --git a/microprofile-metrics/README.adoc b/microprofile-metrics/README.adoc new file mode 100644 index 0000000000..b39de9f186 --- /dev/null +++ b/microprofile-metrics/README.adoc @@ -0,0 +1,987 @@ +include::../shared-doc/attributes.adoc[] + += microprofile-metrics: MicroProfile Metrics QuickStart +:author: Martin Stefanko +:level: Beginner +:technologies: MicroProfile Metrics + +[abstract] +The `microprofile-metrics` quickstart demonstrates the use of the MicroProfile +Metrics specification use in {productName}. + +:standalone-server-type: default +:archiveType: jar +:archiveName: {artifactId} + +== What is it? + +MicroProfile Metrics allows applications to expose different metrics from their +execution which is necessary to provide monitoring of essential system parameters. +The metrics are provided by default in the well-known Prometheus metric format which +allows applications utilizing MicroProfile Metrics to be easily integrated in the +common monitoring solutions. + +== Architecture + +In this quickstart, we build a collection of REST resources that expose +functionalities of the MicroProfile Metrics specification. The metrics are exposed +through the management interface at `/metrics` path according to the specification. +Metrics will be collected as a part of the REST invocations. + +== Solution + +We recommend that you follow the instructions in the next sections and create the +application step by step. However, you can go right to the completed example which +is available in this directory. + +// Start the {productName} Standalone Server +include::../shared-doc/start-the-standalone-server.adoc[leveloffset=+1] + +Compile the application code and deploy it to {productName} +[source,options="nowrap"] +---- +$ mvn clean package wildfly:deploy +---- + +== Creating the Maven Project + +[source,options="nowrap"] +---- +mvn archetype:generate \ + -DgroupId=org.wildfly.quickstarts \ + -DartifactId=microprofile-metrics \ + -DinteractiveMode=false \ + -DarchetypeGroupId=org.apache.maven.archetypes \ + -DarchetypeArtifactId=maven-archetype-webapp +cd microprofile-metrics +---- + +Open the project in your favourite IDE. + +Open the generated `pom.xml`. + +The first thing to do is to setup our dependencies. Add the following section to your +`pom.xml`: + +[source,xml] +---- + + + + + org.wildfly.bom + wildfly-microprofile + ${version.server.bom} + pom + import + + + +---- + +where `${version.server.bom}` is the version of your {productName}, in our case +"{productVersion}". + +Now we need to add the following dependencies: + +[source,xml] +---- + + + org.eclipse.microprofile.metrics + microprofile-metrics-api + provided + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + + org.jboss.spec.javax.ws.rs + jboss-jaxrs-api_2.1_spec + provided + +---- + +NOTE: Because the MicroProfile Metrics specifications uses CDI injection to expose +some of its functionality in the user application we need to also include the CDI API +dependency. + +All dependencies can have provided scope. + +As we are going to be deploying this application to the {productName} server, let's +also add a maven plugin that will simplify the deployment operations (you can replace +the generated build section): + +[source,xml] +---- + + + ${project.artifactId} + + + + org.wildfly.plugins + wildfly-maven-plugin + + + +---- + +As this is a Jakarta REST application we need to also create an application class. +Create `org.wildfly.quickstarts.microprofile.metrics.JaxRsApplication` with the following +content: + +NOTE: The new file should be created in +`src/main/java/org/quickstarts/microprofile/metrics/JaxRsApplication.java`. + +[source,java] +---- +package org.wildfly.quickstarts.microprofile.metrics; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +@ApplicationPath("/") +public class JaxRsApplication extends Application { +} +---- + +Now we are ready to start working with MicroProfile Metrics. + +== Accessing the metrics + +The MicroProfile Metrics exposes all collected metrics at a single REST endpoint +`/metrics`. Even without any direct interaction, {productName} server already collects +and exposes some metrics. You can check the metrics anytime by accessing the +`http://localhost:9990/metrics` endpoint using your browser or +`curl http://localhost:9990/metrics`. + +NOTE: Don't forget to have {productName} server started. + +You will get back a text file similar to this: + +[source] +---- +# HELP base_cpu_processCpuLoad Displays the "recent cpu usage" for the Java Virtual Machine process. +# TYPE base_cpu_processCpuLoad gauge +base_cpu_processCpuLoad 0.005753138075313807 +# HELP base_memory_committedNonHeap_bytes Displays the amount of memory that is committed for the Java virtual machine to use. +# TYPE base_memory_committedNonHeap_bytes gauge +base_memory_committedNonHeap_bytes 8.9915392E7 +---- + +This is a Prometheus format which is directly consumable by +the https://prometheus.io/[Prometheus monitoring system]. This is a default +format. If you prefer for your testing to use rather the JSON format you need to +specify the HTTP header `Accepted: application/json` with for +instance `curl -H "Accept: application/json" http://localhost:9990/metrics`. + +== Different scopes of metrics + +The MicroProfile Metrics specification defines three different scopes of metrics: + +* Base scope (`/metrics/base`)- the metrics that all MicroProfile vendors have to +provide +* Vendor scope (`/metrics/vendor`) - vendor specific metrics +* Application scope (`/metrics/application`) - application specific metrics + +Base scope metrics cover the metrics that are generally useful in any environment +like `memory.usedHeap` or `thread.count`. For the full list of required metrics +please access the +`http://localhost:9990/metrics/base` endpoint using your browser or +`curl http://localhost:9990/metrics/base`. + +Vendor metrics represent custom metrics that are provided by the implementation of +the MicroProfile Metrics specification which are in our case the custom metrics +provided by the {productName} server. To see the full list please access +the `http://localhost:9990/metrics/vendor` endpoint using your browser or +`curl http://localhost:9990/metrics/vendor`. You can note that there are custom +metrics exposed by the individual {productName} subsystems which are prefixed by + `wildfly_` or `jboss_`. + +The application scope covers the metrics defined by your application. We will +demonstrate how to define application metrics in the following sections. + +== OPTIONS requests for metrics metadata + +So far we've accessed the metrics only with the HTTP GET requests which will always +return the individual values of the requested metrics. The specification also provides +an OPTIONS method requests for users to request the metrics metadata: + +Perform a request +`curl -X OPTIONS -H "Accept: application/json" http://localhost:9990/metrics` and you +can see that the returned JSON now contains metadata about individual metrics like +unit, type, description, display name and tags. We will see later on how you can +change these values for your custom applications scoped metrics. + +== Application specific metrics + +So far we've covered how we can access the metrics which are already predefined in +the {productName} server. Let's now define some custom application metrics. + +The MicroProfile Metrics specification defines several types of application metrics: + +* `@Counted` - counts the invocations of the annotated object +* `@Timed` - tracks the duration of the annotated object +* `@Gauge` - samples the value of the annotated object +* `@ConcurrentGauge` - gauge that counts parallel invocations of the annotated object +* `@Metered` - tracks the frequency of invocation of the annotated object +* `@Metric` - injection of the metric object + +Let's see how we can use these annotations in our application. Create a new class +`org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker`: + +[source,java] +---- +package org.wildfly.quickstarts.microprofile.metrics; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/") +@ApplicationScoped +public class PrimeNumberChecker { + + @GET + @Path("/prime/{number}") + @Produces(MediaType.TEXT_PLAIN) + public String checkIfPrime(@PathParam("number") long number) { + if (number < 1) { + return "Only natural numbers can be prime numbers."; + } + + if (number == 1) { + return "1 is not prime."; + } + + if (number == 2) { + return "2 is prime."; + } + + if (number % 2 == 0) { + return number + " is not prime, it is divisible by 2."; + } + + for (int i = 3; i < Math.floor(Math.sqrt(number)) + 1; i = i + 2) { + if (number % i == 0) { + return number + " is not prime, is divisible by " + i + "."; + } + } + + return number + " is prime."; + } +} +---- + +This class represents a simple REST endpoint that is able to determine whether the +number passed as a path parameter is a prime number. + +Build and redeploy the application + +[source,options="nowrap"] +---- +$ mvn clean package wildfly:deploy +---- + +Now you can access `http://localhost:8080/microprofile-metrics/prime/350` +endpoint using your browser or +`curl http://localhost:8080/microprofile-metrics/prime/350` to check whether the +number `350` is a prime number. + +NOTE: You can of course try this endpoint with different numbers. + +Now we are ready to add our custom metrics to this REST resource. If you try to access +the application metrics now (`http://localhost:9990/metrics/application`) it should be +empty. + +=== Counted + +Update the `PrimeNumberChecker`: + +[source,java] +---- +package org.wildfly.quickstarts.microprofile.metrics; + +import org.eclipse.microprofile.metrics.annotation.Counted; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/") +@ApplicationScoped +public class PrimeNumberChecker { + + @GET + @Path("/prime/{number}") + @Produces(MediaType.TEXT_PLAIN) + @Counted + public String checkIfPrime(@PathParam("number") long number) { + if (number < 1) { + return "Only natural numbers can be prime numbers."; + } + + if (number == 1) { + return "1 is not prime."; + } + + if (number == 2) { + return "2 is prime."; + } + + if (number % 2 == 0) { + return number + " is not prime, it is divisible by 2."; + } + + for (int i = 3; i < Math.floor(Math.sqrt(number)) + 1; i = i + 2) { + if (number % i == 0) { + return number + " is not prime, is divisible by " + i + "."; + } + } + + return number + " is prime."; + } +} +---- + +Just by adding this single annotation we are already exposing the counter metric +that will be counting the invocations of the annotated method. If you now redeploy +your application (`mvn clean package wildfly:deploy`) and try to access your +application metrics again (`http://localhost:9990/metrics/application`) you will see +something like this: + +[source,json] +---- +{ + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.checkIfPrime": 0 +} +---- + +Try now to invoke the REST endpoint several times and you will see the counter works +as expected. + +If you perform an OPTIONS request for this metrics, it's values should be empty. + +[source,options="nowrap"] +---- +$ curl -X OPTIONS -H "Accept: application/json" http://localhost:9990/metrics/application +---- + +[source,json] +---- +{ + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.checkIfPrime": { + "unit": "none", + "type": "counter", + "description": "", + "displayName": "", + "tags": [ + [ + ] + ] + } +} +---- + +To change these values you can simply edit the parameters in the `@Counted` +annotation (similarly for other annotations). + +[source,java] +---- +@Counted(name = "performedChecks", displayName="Performed Checks", description = "How many prime checks have been performed.") +---- + +And rebuild, redeploy your application to the {productName} server and access the +application metrics metadata again: + +[source,options="nowrap"] +---- +mvn clean package wildfly:deploy +curl -X OPTIONS -H "Accept: application/json" http://localhost:9990/metrics/application +---- + +[source,json] +---- +{ + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.performedChecks": { + "unit": "none", + "type": "counter", + "description": "How many prime checks have been performed.", + "displayName": "Performed Checks", + "tags": [ + [ + ] + ] + } +} +---- + +=== Timed + +Timed annotation declares a timer which will time the duration of how long our +method is executing. Add the following annotation to our `checkIfPrime` method: + +[source,java] +---- +@GET +@Path("/prime/{number}") +@Produces(MediaType.TEXT_PLAIN) +@Counted(name = "performedChecks", displayName="Performed Checks", description = "How many prime checks have been performed.") +@Timed(name = "checksTimer", absolute = true, description = "A measure of how long it takes to perform the primality test.", unit = MetricUnits.MILLISECONDS) +public String checkIfPrime(@PathParam("number") long number) { +---- + +NOTE: Note the parameter `absolute = true` this will make our metric name to be +not prefixed by the fully qualified package of the class where our check is located. + +And rebuild, redeploy your application to the {productName} server, invoke the +method several times, and access the application metrics data again: + +[source,options="nowrap"] +---- +mvn clean package wildfly:deploy +curl http://localhost:8080/microprofile-metrics/prime/350 +curl http://localhost:8080/microprofile-metrics/prime/7 +curl http://localhost:8080/microprofile-metrics/prime/29 +curl -H "Accept: application/json" http://localhost:9990/metrics/application +---- + +[source,json] +---- +{ + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.performedChecks": 3, + "checksTimer": { + "p99": 0.136153, + "min": 0.01075, + "max": 0.136153, + "mean": 0.07816790480018097, + "p50": 0.088464, + "p999": 0.136153, + "stddev": 0.05164982729810127, + "p95": 0.136153, + "p98": 0.136153, + "p75": 0.136153, + "fiveMinRate": 0.0, + "fifteenMinRate": 0.0, + "meanRate": 5.254614407952331, + "count": 3, + "oneMinRate": 0.0 + } +} +---- + +Note the name of our timer check is only what we defined in our annotation because of +the `absolute = true`. You can also access particular check directly: + +[source,options="nowrap"] +---- +curl -H "Accept: application/json" http://localhost:9990/metrics/application/checksTimer +---- + +[source,json] +---- +{ + "checksTimer": { + "p99": 0.136153, + "min": 0.01075, + "max": 0.136153, + "mean": 0.07816790480018097, + "p50": 0.088464, + "p999": 0.136153, + "stddev": 0.05164982729810127, + "p95": 0.136153, + "p98": 0.136153, + "p75": 0.136153, + "fiveMinRate": 0.4595570030187892, + "fifteenMinRate": 0.5489683372380184, + "meanRate": 0.03506919414327886, + "count": 3, + "oneMinRate": 0.1581582828694362 + } +} +---- + +== Gauge + +Gauge is the most variable metric which is fully up to the application. It is +required to define the metric type for this kind of metric. Let's update +the `PrimeNumberChecker`: + +[source,java] +---- +package org.wildfly.quickstarts.microprofile.metrics; + +import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.annotation.Counted; +import org.eclipse.microprofile.metrics.annotation.Gauge; +import org.eclipse.microprofile.metrics.annotation.Timed; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/") +@ApplicationScoped +public class PrimeNumberChecker { + + private long highestPrimeNumberSoFar = 2; + + @GET + @Path("/prime/{number}") + @Produces(MediaType.TEXT_PLAIN) + @Counted(name = "performedChecks", displayName="Performed Checks", description = "How many prime checks have been performed.") + @Timed(name = "checksTimer", absolute = true, description = "A measure of how long it takes to perform the primality test.", unit = MetricUnits.MILLISECONDS) + public String checkIfPrime(@PathParam("number") long number) { + if (number < 1) { + return "Only natural numbers can be prime numbers."; + } + + if (number == 1) { + return "1 is not prime."; + } + + if (number == 2) { + return "2 is prime."; + } + + if (number % 2 == 0) { + return number + " is not prime, it is divisible by 2."; + } + + for (int i = 3; i < Math.floor(Math.sqrt(number)) + 1; i = i + 2) { + if (number % i == 0) { + return number + " is not prime, is divisible by " + i + "."; + } + } + + if (number > highestPrimeNumberSoFar) { + highestPrimeNumberSoFar = number; + } + + return number + " is prime."; + } + + @Gauge(name = "highestPrimeNumberSoFar", unit = MetricUnits.NONE, description = "Highest prime number so far.") + public Long highestPrimeNumberSoFar() { + return highestPrimeNumberSoFar; + } +} +---- + +WARNING: The metric unit for gauges is required to be defined in the annotation. + +And rebuild, redeploy your application to the {productName} server, invoke the method +several times, and access the application metrics data again: + +[source,options="nowrap"] +---- +mvn clean package wildfly:deploy +curl http://localhost:8080/microprofile-metrics/prime/350 +curl http://localhost:8080/microprofile-metrics/prime/7 +curl http://localhost:8080/microprofile-metrics/prime/29 +curl -H "Accept: application/json" http://localhost:9990/metrics/application +---- + +[source,json] +---- +{ + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.highestPrimeNumberSoFar": 29, + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.performedChecks": 3, + "checksTimer": { + "p99": 0.187263, + "min": 0.02916, + "max": 0.187263, + "mean": 0.121405, + "p50": 0.147792, + "p999": 0.187263, + "stddev": 0.06718801966124616, + "p95": 0.187263, + "p98": 0.187263, + "p75": 0.187263, + "fiveMinRate": 0.0, + "fifteenMinRate": 0.0, + "meanRate": 5.571895665830193, + "count": 3, + "oneMinRate": 0.0 + } +} +---- + +Of course, gauges can be any value your application need. This is a fully customizable +metric type. + +== Concurrent gauge + +As stated above, concurrent gauge is a gauge that count the number of parallel +accesses to a particular object. Let's add a new method to `PrimeNumbersChecker`: + +[source,java] +---- +private CountDownLatch countDownLatch = new CountDownLatch(1); + +@GET +@Path("/parallel") +@ConcurrentGauge(name = "parallelAccess", description = "Number of parallel accesses") +public void parallelAccess() throws InterruptedException { + countDownLatch.await(); + System.out.println("DONE"); +} + +@GET +@Path("/parallel-finish") +public void parallelFinish() { + countDownLatch.countDown(); + System.out.println("Finished parallel execution"); +} +---- + +Now let's try to exercise it: + +[source,options="nowrap"] +---- +mvn clean package wildfly:deploy +for i in {1..3}; do curl http://localhost:8080/microprofile-metrics/parallel & done +sleep 1 +curl -H "Accept: application/json" http://localhost:9990/metrics/application +---- + +[source,json] +---- +{ + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.highestPrimeNumberSoFar": 2, + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.performedChecks": 0, + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.parallelAccess": { + "current": 3, + "min": 0, + "max": 0 + }, + "checksTimer": { + "p99": 0.0, + "min": 0.0, + "max": 0.0, + "mean": 0.0, + "p50": 0.0, + "p999": 0.0, + "stddev": 0.0, + "p95": 0.0, + "p98": 0.0, + "p75": 0.0, + "fiveMinRate": 0.0, + "fifteenMinRate": 0.0, + "meanRate": 0.0, + "count": 0, + "oneMinRate": 0.0 + } +} +---- + +As you can see the 3 started requests are currently executed in parallel. You can +finish them by a call to `http://localhost:8080/microprofile-metrics/parallel-finish`. + + +=== Metered + +A meter is a metric type that tracks the frequency of incovations. Let's update our +`checkIfPrime` method: + +[source,java] +---- +@GET +@Path("/prime/{number}") +@Produces(MediaType.TEXT_PLAIN) +@Counted(name = "performedChecks", displayName="Performed Checks", description = "How many prime checks have been performed.") +@Timed(name = "checksTimer", absolute = true, description = "A measure of how long it takes to perform the primality test.", unit = MetricUnits.MILLISECONDS) +@Metered(name = "checkIfPrimeFrequency", absolute = true) +public String checkIfPrime(@PathParam("number") long number) { +---- + +And rebuid, redeploy your application to the {productName} server, invoke the method +several times, and access the application metrics data again: + +[source,options="nowrap"] +---- +mvn clean package wildfly:deploy +curl http://localhost:8080/microprofile-metrics/prime/350 +curl http://localhost:8080/microprofile-metrics/prime/7 +curl http://localhost:8080/microprofile-metrics/prime/29 +curl -H "Accept: application/json" http://localhost:9990/metrics/application +---- + +[source,json] +---- +{ + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.highestPrimeNumberSoFar": 7, + "checkIfPrimeFrequency": { + "fiveMinRate": 0.0, + "fifteenMinRate": 0.0, + "meanRate": 6.804175994067375, + "count": 3, + "oneMinRate": 0.0 + }, + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.performedChecks": 3, + "org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.parallelAccess": { + "current": 0, + "min": 0, + "max": 0 + }, + "checksTimer": { + "p99": 0.063733, + "min": 0.00611, + "max": 0.063733, + "mean": 0.04252133333333333, + "p50": 0.057721, + "p999": 0.063733, + "stddev": 0.0258634224640815, + "p95": 0.063733, + "p98": 0.063733, + "p75": 0.063733, + "fiveMinRate": 0.0, + "fifteenMinRate": 0.0, + "meanRate": 6.794024967566174, + "count": 3, + "oneMinRate": 0.0 + } +} +---- + + +=== Metric + +The last annotation is the `@Metric` which is used to inject or produce a metric. +Let's add the following code into `PrimeNumberChecker`: + +[source,java] +---- +@Inject +@Metric(name = "injectedCounter", absolute = true) +private Counter injectedCounter; + +@GET +@Path("/injected-metric") +public String injectedMetric() { + injectedCounter.inc(); + return "counter invoked"; +} +---- + +And rebuid, redeploy your application to the {productName} server, invoke the method +several times, and access the application metrics data again: + +[source,options="nowrap"] +---- +mvn clean package wildfly:deploy +curl http://localhost:8080/microprofile-metrics/injected-metric +curl http://localhost:8080/microprofile-metrics/injected-metric +curl http://localhost:8080/microprofile-metrics/injected-metric +curl -H "Accept: application/json" http://localhost:9990/metrics/application/injectedCounter +---- + +[source,json] +---- +{ + "injectedCounter": 3 +} +---- + +The types that can be injected and produced are `Counter`, `Timer`, `Meter`, +`Histogram`. `Gauge` can be only produced. + + +== Accessing the metric registry + +A metric registry a collection of metrics that corresponds to the different metric +scopes the MicroProfile Metrics specification defines. To access the +`MetricRegistry` you can inject and use it like this. Add the following code to +`PrimeNumberChecker`: + +[source,java] +---- +@Inject +@RegistryType(type = MetricRegistry.Type.APPLICATION) +private MetricRegistry applicationRegistry; + +@GET +@Path("/registry") +public void registry() { + // register a new application scoped metric + Counter programmaticCounter = applicationRegistry.counter(Metadata.builder() + .withName("programmaticCounter") + .withDescription("Programmatically created counter") + .build()); + + programmaticCounter.inc(42); +} +---- + +NOTE: You need to also set the Java language level to Java 8 to use the +interface method invocations. + +And rebuid, redeploy your application to the {productName} server, invoke the method +, and access the application metrics data again: + +[source,options="nowrap"] +---- +mvn clean package wildfly:deploy +curl http://localhost:8080/microprofile-metrics/registry +curl -H "Accept: application/json" http://localhost:9990/metrics/application/programmaticCounter +---- + +[source,json] +---- +{ + "programmaticCounter": 42 +} +---- + + +== Tags + +The tag is basically only a key-value pair that can be associated with a metric. This +means that we can have a metrics which have the same name if the tags differ. This +kind of labels are very common in modern microservices orchestration services like +https://kubernetes.io/[Kubernetes]. Tags can be set as a part of metadata in the +individual annotations or when you are registering a new metric with the metric +registry. Add the following code to `PrimeNumberChecker`: + +[source,java] +---- +@GET +@Path("/duplicates") +@Counted(name = "duplicatedCounter", absolute = true, tags = {"type=original"}) +public String duplicates() { + return "duplicated metrics"; +} + +@GET +@Path("/duplicates2") +@Counted(name = "duplicatedCounter", absolute = true, tags = {"type=copy"}) +public String duplicates2() { + return "duplicated metrics"; +} +---- + +And rebuid, redeploy your application to the {productName} server, invoke the method +, and access the application metrics data again: + +[source,options="nowrap"] +---- +mvn clean package wildfly:deploy +curl http://localhost:8080/microprofile-metrics/duplicates +curl http://localhost:8080/microprofile-metrics/duplicates2 +curl -H "Accept: application/json" http://localhost:9990/metrics/application/duplicatedCounter +---- + +[source,json] +---- +{ + "duplicatedCounter;type=original": 1, + "duplicatedCounter;type=copy": 1 +} +---- + + +== Displaying metrics with Prometheus and Grafana + +So far we exposed the metrics from our service but we didn't consume them efficiently +(in production we need to monitor services for long periods so manually invoking +`/metrics` endpoint is not acceptable). As mentioned above, MicroProfile Metrics +expose the metrics by default in the Prometheus format. So let's route our exposed +metrics from {productName} to an instance of Prometheus. + +NOTE: In this section we will be running external microservices as Docker +containers. If you don't have Docker installed on your machine please follow the +instructions at https://www.docker.com/get-started. + +Prometheus is a monitoring system and time series database. +You can find more information at https://prometheus.io/. To start the Prometheus +instance on our local computer we need: + +* a `prometheus.yml` file with the following content - link:prometheus.yml[] + +* Docker installed and running + +To start the prometheus docker container run the following command: + +[source] +---- +docker run --rm --name prometheus -p 9090:9090 --network host \ +-v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml:Z prom/prometheus:v2.14.0 \ +--config.file=/etc/prometheus/prometheus.yml +---- + +Wait for the container to start and then you can access `http://localhost:9090` in +your browser to view the Prometheus console. Because we configured Prometheus (in +`prometheus.yml`) to scrap our `prime-checker` service running on {productName} server +we can directly access the collected metrics which Prometheus collects from +`http://localhost:9990/metrics`. You can test it be typing for instance +`application_org_quickstart_microprofile_metrics_PrimeNumberChecker_performedChecks_total` +as the query expression and executing this query. + +Congratulations. Your service metrics are now queried by Prometheus every second so +you can try to make a few request for prime numbers and see the values reflected in +the console. + +Now that we have the Prometheus collecting the metrics we can expose Grafana. +Grafana is analytics & monitoring solution which can be viewed as a nice +presentation of metrics which we are being collected in the Prometheus. You can find +more information about Grafana at https://grafana.com/. + +To run Grafana make sure that the Prometheus is started and collecting metrics. If +you followed previous section, the prometheus should be running. + +To start Grafana you can run: + +[source] +---- +docker run --rm -p 3000:3000 --network host grafana/grafana:6.4.4 +---- + +This will expose Grafana console at `http://localhost:3000`. You can login with +default admin user (`admin:admin`). + +The first thing we need to do with the new Grafana instance is to add a data source. +Click the `Add data source` button on the welcome page. Select `Prometheus`. In +the HTTP section set URL to be `http://localhost:9090` and change the `Scrape +interval` to be `1s`. Click `Save & Test`. + +Now is the time to add a new dashboard with the metrics we are interested in. You +can add a new custom dashboard and new panels manually (the UI is intuitive) or +you can skip the manual configuration with the following simple provided dashboard: + +* click plus sign on the left side -> `Import` + +* paste the following JSON - link:grafana.json[] + +* set the Prometheus (Prometheus = Prometheus) data source on the next screen and +click `Import`. You will see four simple panels displaying some of the application +metrics that we exposed in our {productName} server. + +NOTE: In production you typically do not run these service yourself but they are +provided for you by operations or platform directly. So you just need to pass the +location of your services to allow these services to collect metrics for you. + +You can again make several requests for prime numbers +(e.g., `http://localhost:8080/microprofile-metrics/prime/7`) to see how the metrics +change. + +== Conclusion + +MicroProfile Metrics provide a way for your application to expose metrics about your +application/service to the outside world. They expose the metrics in a well-known +Prometheus format which is directly consumable in the cloud environment. The more +information can be found in the +https://github.com/eclipse/microprofile-metrics/blob/master/spec/src/main/asciidoc/metrics_spec.adoc[MicroProfile Metrics specification]. diff --git a/microprofile-metrics/grafana.json b/microprofile-metrics/grafana.json new file mode 100644 index 0000000000..45e90040d0 --- /dev/null +++ b/microprofile-metrics/grafana.json @@ -0,0 +1,418 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "6.4.4" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": true, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 4, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "6.4.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "application_org_quickstart_microprofile_metrics_PrimeNumberChecker_performedChecks_total", + "legendFormat": "performedChecks_total", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "performedChecks_total Counter", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "max" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "6.4.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "application_checksTimer_mean_seconds", + "legendFormat": "application_checksTimer_mean_seconds", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "application_checksTimer_mean_seconds", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 6, + "options": { + "fieldOptions": { + "calcs": [ + "max" + ], + "defaults": { + "mappings": [], + "max": 100, + "min": 0, + "thresholds": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "override": {}, + "values": false + }, + "orientation": "auto", + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "6.4.4", + "targets": [ + { + "expr": "application_org_quickstart_microprofile_metrics_PrimeNumberChecker_highestPrimeNumberSoFar", + "legendFormat": "Highest Prime so far", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Highest Prime So Far", + "type": "gauge" + }, + { + "aliasColors": {}, + "bars": false, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 8, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "6.4.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "application_checksTimer_mean_seconds", + "intervalFactor": 1, + "legendFormat": "Checks Timer Mean seconds", + "refId": "A" + }, + { + "expr": "application_checksTimer_max_seconds", + "legendFormat": "Checks Timer Mean seconds", + "refId": "B" + }, + { + "expr": "application_checksTimer_min_seconds", + "legendFormat": "Checks Timer Min seconds", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Checks Timer", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 2, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "5s", + "schemaVersion": 20, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "PrimeCheckerDashboard", + "uid": "tezPg5xZk", + "version": 1 +} diff --git a/microprofile-metrics/pom.xml b/microprofile-metrics/pom.xml new file mode 100644 index 0000000000..3c6927678c --- /dev/null +++ b/microprofile-metrics/pom.xml @@ -0,0 +1,81 @@ + + 4.0.0 + + org.wildfly.quickstarts + quickstart-parent + + 19.0.0.Beta4-SNAPSHOT + ../pom.xml + + + microprofile-metrics + war + + Quickstart: microprofile-metrics + + + 1.8 + 1.8 + false + + + + + + org.eclipse.microprofile.metrics + microprofile-metrics-api + provided + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + + org.jboss.spec.javax.ws.rs + jboss-jaxrs-api_2.1_spec + provided + + + + + junit + junit + test + + + + org.jboss.arquillian.junit + arquillian-junit-container + test + + + org.wildfly.arquillian + wildfly-arquillian-common + test + + + org.jboss.resteasy + resteasy-client + test + + + commons-logging + commons-logging + 1.2 + test + + + + + + ${project.artifactId} + + diff --git a/microprofile-metrics/prometheus.yml b/microprofile-metrics/prometheus.yml new file mode 100644 index 0000000000..c99706ee24 --- /dev/null +++ b/microprofile-metrics/prometheus.yml @@ -0,0 +1,13 @@ +# prometheus.yml +global: + scrape_interval: 1s + external_labels: + monitor: 'my-monitor' + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + - job_name: 'prime-checker' + static_configs: + - targets: ['localhost:9990'] diff --git a/microprofile-metrics/src/main/java/org/wildfly/quickstarts/microprofile/metrics/JaxRsApplication.java b/microprofile-metrics/src/main/java/org/wildfly/quickstarts/microprofile/metrics/JaxRsApplication.java new file mode 100644 index 0000000000..242be64306 --- /dev/null +++ b/microprofile-metrics/src/main/java/org/wildfly/quickstarts/microprofile/metrics/JaxRsApplication.java @@ -0,0 +1,8 @@ +package org.wildfly.quickstarts.microprofile.metrics; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +@ApplicationPath("/") +public class JaxRsApplication extends Application { +} diff --git a/microprofile-metrics/src/main/java/org/wildfly/quickstarts/microprofile/metrics/PrimeNumberChecker.java b/microprofile-metrics/src/main/java/org/wildfly/quickstarts/microprofile/metrics/PrimeNumberChecker.java new file mode 100644 index 0000000000..139fef96b6 --- /dev/null +++ b/microprofile-metrics/src/main/java/org/wildfly/quickstarts/microprofile/metrics/PrimeNumberChecker.java @@ -0,0 +1,132 @@ +package org.wildfly.quickstarts.microprofile.metrics; + +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.annotation.ConcurrentGauge; +import org.eclipse.microprofile.metrics.annotation.Counted; +import org.eclipse.microprofile.metrics.annotation.Gauge; +import org.eclipse.microprofile.metrics.annotation.Metered; +import org.eclipse.microprofile.metrics.annotation.Metric; +import org.eclipse.microprofile.metrics.annotation.RegistryType; +import org.eclipse.microprofile.metrics.annotation.Timed; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.util.concurrent.CountDownLatch; + +@Path("/") +@ApplicationScoped +public class PrimeNumberChecker { + + private static final long COUNTER_INCREMENT = 42; + private long highestPrimeNumberSoFar = 2; + + @GET + @Path("/prime/{number}") + @Produces(MediaType.TEXT_PLAIN) + @Counted(name = "performedChecks", displayName="Performed Checks", description = "How many prime checks have been performed.") + @Timed(name = "checksTimer", absolute = true, description = "A measure of how long it takes to perform the primality test.", unit = MetricUnits.MILLISECONDS) + @Metered(name = "checkIfPrimeFrequency", absolute = true) + public String checkIfPrime(@PathParam("number") long number) { + if (number < 1) { + return "Only natural numbers can be prime numbers."; + } + + if (number == 1) { + return "1 is not prime."; + } + + if (number == 2) { + return "2 is prime."; + } + + if (number % 2 == 0) { + return number + " is not prime, it is divisible by 2."; + } + + for (int i = 3; i < Math.floor(Math.sqrt(number)) + 1; i = i + 2) { + if (number % i == 0) { + return number + " is not prime, is divisible by " + i + "."; + } + } + + if (number > highestPrimeNumberSoFar) { + highestPrimeNumberSoFar = number; + } + + return number + " is prime."; + } + + @Gauge(name = "highestPrimeNumberSoFar", unit = MetricUnits.NONE, description = "Highest prime number so far.") + public Long highestPrimeNumberSoFar() { + return highestPrimeNumberSoFar; + } + + private CountDownLatch countDownLatch = new CountDownLatch(1); + + @GET + @Path("/parallel") + @ConcurrentGauge(name = "parallelAccess", description = "Number of parallel accesses") + public void parallelAccess() throws InterruptedException { + countDownLatch.await(); + System.out.println("DONE"); + } + + @GET + @Path("/parallel-finish") + public void parallelFinish() { + countDownLatch.countDown(); + System.out.println("Finished parallel execution"); + } + + @Inject + @Metric(name = "injectedCounter", absolute = true) + private Counter injectedCounter; + + @GET + @Path("/injected-metric") + @Produces(MediaType.TEXT_PLAIN) + public String injectedMetric() { + injectedCounter.inc(); + return "counter invoked"; + } + + @Inject + @RegistryType(type = MetricRegistry.Type.APPLICATION) + private MetricRegistry applicationRegistry; + + @GET + @Path("/registry") + public void registry() { + // register a new application scoped metric + Counter programmaticCounter = applicationRegistry.counter(Metadata.builder() + .withName("programmaticCounter") + .withDescription("Programmatically created counter") + .build()); + + programmaticCounter.inc(COUNTER_INCREMENT); + } + + @GET + @Path("duplicates") + @Produces(MediaType.TEXT_PLAIN) + @Counted(name = "duplicatedCounter", absolute = true, tags = {"type=original"}) + public String duplicates() { + return "duplicated metrics"; + } + + @GET + @Path("duplicates2") + @Produces(MediaType.TEXT_PLAIN) + @Counted(name = "duplicatedCounter", absolute = true, tags = {"type=copy"}) + public String duplicates2() { + return "duplicated metrics"; + } +} diff --git a/microprofile-metrics/src/test/java/org/wildfly/quickstarts/microprofile/metrics/MicroProfileMetricsIT.java b/microprofile-metrics/src/test/java/org/wildfly/quickstarts/microprofile/metrics/MicroProfileMetricsIT.java new file mode 100644 index 0000000000..ef155ac19d --- /dev/null +++ b/microprofile-metrics/src/test/java/org/wildfly/quickstarts/microprofile/metrics/MicroProfileMetricsIT.java @@ -0,0 +1,236 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2019, Red Hat, Inc. and/or its affiliates, and individual + * contributors by the @authors tag. See the copyright.txt in the + * distribution for a full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wildfly.quickstarts.microprofile.metrics; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.as.arquillian.container.ManagementClient; +import org.jboss.dmr.ModelNode; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; + +/** + * Simple tests for MicroProfile Metrics quickstart. Arquillian deploys an JAR archive to the application server, which + * contains several REST endpoints and verifies that the metrics are correctly exposed. + * + * @author Martin Stefanko + * + */ +@RunWith(Arquillian.class) +@RunAsClient +public class MicroProfileMetricsIT { + + private URL managementURL; + + @ArquillianResource + private ManagementClient managementClient; + + @ArquillianResource + private URL deploymentURL; + + private Client client; + + @Before + public void before() throws MalformedURLException { + managementURL = new URL(String.format("http://%s:%d", + managementClient.getMgmtAddress(), managementClient.getMgmtPort())); + client = ClientBuilder.newClient(); + } + + @After + public void after() { + if (client != null) { + client.close(); + } + } + + /** + * Constructs a deployment archive + * + * @return the deployment archive + */ + @Deployment + public static JavaArchive createDeployment() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(JaxRsApplication.class, PrimeNumberChecker.class) + // enable CDI + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); + } + + /** + * Tests that /prime/{number} metrics are correctly collected. + */ + @Test + public void testPrimeMetrics() { + restAppGetInvoke(client, "/prime/350"); + restAppGetInvoke(client, "/prime/7"); + restAppGetInvoke(client, "/prime/29"); + + Response response = client. + target(managementURL.toString()) + .path("/metrics/application") + .request() + .header("Accept", MediaType.APPLICATION_JSON) + .get(); + + Assert.assertEquals(200, response.getStatus()); + ModelNode json = ModelNode.fromJSONString(response.readEntity(String.class)); + + Assert.assertEquals(3, json.get("org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.performedChecks").asInt()); + Assert.assertEquals(3, json.get("checksTimer").get("count").asInt()); + Assert.assertEquals(3, json.get("checkIfPrimeFrequency").get("count").asInt()); + Assert.assertEquals(29, json.get("org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.highestPrimeNumberSoFar").asInt()); + } + + /** + * Tests that /parallel metrics are correctly collected. + */ + @Test + public void testParallelMetrics() throws Exception { + int n = 3; + WebTarget webTarget = client.target(deploymentURL.toString()) + .path("/parallel"); + + List> responses = new ArrayList<>(n); + + for (int i = 0; i < n; i++) { + responses.add(webTarget.request().async().get()); + } + + // 3 parallel requests are in flight, give metrics a while to be collected + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // check the application metrics + Response response = client. + target(managementURL.toString()) + .path("/metrics/application") + .request() + .header("Accept", MediaType.APPLICATION_JSON) + .get(); + + Assert.assertEquals(200, response.getStatus()); + ModelNode json = ModelNode.fromJSONString(response.readEntity(String.class)); + + Assert.assertEquals("Invalid number of current parallel accesses", + 3, json.get("org.wildfly.quickstarts.microprofile.metrics.PrimeNumberChecker.parallelAccess").get("current").asInt()); + + // finish the started requests + restAppGetInvoke(client, "/parallel-finish"); + + for (int i = 0; i < n; i++) { + responses.get(i).get().close(); + } + } + + /** + * Tests that /injected-metric metrics are correctly collected. + */ + @Test + public void testInjectedMetrics() { + for (int i = 0; i < 3; i++) { + restAppGetInvoke(client, "/injected-metric"); + } + + Response response = client. + target(managementURL.toString()) + .path("/metrics/application") + .request() + .header("Accept", MediaType.APPLICATION_JSON) + .get(); + + Assert.assertEquals(200, response.getStatus()); + ModelNode json = ModelNode.fromJSONString(response.readEntity(String.class)); + + Assert.assertEquals(3, json.get("injectedCounter").asInt()); + } + + /** + * Tests that /registry metrics are correctly collected. + */ + @Test + public void testRegistryMetrics() { + restAppGetInvoke(client, "/registry"); + + Response response = client. + target(managementURL.toString()) + .path("/metrics/application") + .request() + .header("Accept", MediaType.APPLICATION_JSON) + .get(); + + Assert.assertEquals(200, response.getStatus()); + ModelNode json = ModelNode.fromJSONString(response.readEntity(String.class)); + + Assert.assertEquals(42, json.get("programmaticCounter").asInt()); + } + + /** + * Tests that tagged duplicated metrics are correctly collected. + */ + @Test + public void testDuplicatedMetrics() { + restAppGetInvoke(client, "/duplicates"); + restAppGetInvoke(client, "/duplicates2"); + + Response response = client. + target(managementURL.toString()) + .path("/metrics/application") + .request() + .header("Accept", MediaType.APPLICATION_JSON) + .get(); + + Assert.assertEquals(200, response.getStatus()); + ModelNode json = ModelNode.fromJSONString(response.readEntity(String.class)); + + Assert.assertEquals(1, json.get("duplicatedCounter;type=original").asInt()); + Assert.assertEquals(1, json.get("duplicatedCounter;type=copy").asInt()); + } + + + private void restAppGetInvoke(Client client, String path) { + Response response = client + .target(deploymentURL.toString()) + .path(path) + .request() + .get(); + + response.close(); + } +} diff --git a/microprofile-metrics/src/test/resources/arquillian.xml b/microprofile-metrics/src/test/resources/arquillian.xml new file mode 100644 index 0000000000..0652c57ffe --- /dev/null +++ b/microprofile-metrics/src/test/resources/arquillian.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 6315468191..d9c474e376 100644 --- a/pom.xml +++ b/pom.xml @@ -418,6 +418,7 @@ messaging-clustering-singleton microprofile-health microprofile-config + microprofile-metrics numberguess payment-cdi-event resteasy-jaxrs-client