diff --git a/docs/pom.xml b/docs/pom.xml index f7ede35460abd..7ff6a11b1e19c 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -23,6 +23,7 @@ https://quarkus.io https://github.com/quarkusio/quarkus https://github.com/quarkusio/quarkus-quickstarts + ${project.basedir}/../target/asciidoc/examples Quarkus - Documentation @@ -2794,6 +2795,24 @@ + + copy-tagged-java-source + process-classes + + java + + + ${skipDocs} + io.quarkus.docs.generation.CopyJavaExamples + + ${code-example-dir} + ${project.basedir}/../integration-tests + + + ${env.MAVEN_CMD_LINE_ARGS} + + + generate-doc-manifests prepare-package @@ -2830,6 +2849,7 @@ true ${project.basedir}/../target/asciidoc/generated + ${code-example-dir} ./images font true @@ -2911,7 +2931,7 @@ https://quarkiverse.github.io/quarkiverse-docs/quarkus-neo4j/dev/index.html https://quarkiverse.github.io/quarkiverse-docs/quarkus-vault/dev/index.html https://quarkiverse.github.io/quarkiverse-docs/quarkus-vault/dev/vault-datasource.html - https://quarkiverse.github.io/quarkiverse-docs/quarkus-micrometer-registry/dev/ + https://quarkiverse.github.io/quarkiverse-docs/quarkus-micrometer-registry/dev/ @@ -3007,6 +3027,33 @@ + + org.codehaus.mojo + exec-maven-plugin + + ${skipDocs} + + + + copy-tagged-java-source + process-classes + + java + + + ${skipDocs} + io.quarkus.docs.generation.CopyJavaExamples + + ${code-example-dir} + ${project.basedir}/../integration-tests + + + ${env.MAVEN_CMD_LINE_ARGS} + + + + + org.asciidoctor asciidoctor-maven-plugin diff --git a/docs/src/main/asciidoc/_examples/attributes.adoc b/docs/src/main/asciidoc/_examples/attributes.adoc index 601d8289973a4..03eff399e165a 100644 --- a/docs/src/main/asciidoc/_examples/attributes.adoc +++ b/docs/src/main/asciidoc/_examples/attributes.adoc @@ -3,8 +3,9 @@ :idprefix: :idseparator: - :icons: font +:code-examples: ../../../../../target/asciidoc/examples :doc-guides: .. :doc-examples: . :imagesdir: ./images :includes: ../includes -:root: ../../asciidoc + diff --git a/docs/src/main/asciidoc/_templates/attributes.adoc b/docs/src/main/asciidoc/_templates/attributes.adoc index f67f3f84a8d9c..cd4f7aeebf0d1 100644 --- a/docs/src/main/asciidoc/_templates/attributes.adoc +++ b/docs/src/main/asciidoc/_templates/attributes.adoc @@ -3,8 +3,8 @@ :idprefix: :idseparator: - :icons: font +:code-examples: ../../../../../target/asciidoc/examples :doc-guides: ../ :doc-examples: ../_examples -:imagesdir: ../../asciidoc/images +:imagesdir: ../images :includes: ../includes -:root: ../../asciidoc/ diff --git a/docs/src/main/asciidoc/attributes.adoc b/docs/src/main/asciidoc/attributes.adoc index 36a0d40806710..efa26032c201f 100644 --- a/docs/src/main/asciidoc/attributes.adoc +++ b/docs/src/main/asciidoc/attributes.adoc @@ -4,8 +4,8 @@ :idprefix: :idseparator: - :icons: font -// tag::xref-attributes[] +:code-examples: ../../../../target/asciidoc/examples +:doc-guides: ./ :doc-examples: ./_examples :imagesdir: ./images :includes: ./includes -// end::xref-attributes[] diff --git a/docs/src/main/asciidoc/doc-reference.adoc b/docs/src/main/asciidoc/doc-reference.adoc index 9c7b7ba9cf1d7..06481c7ca9d29 100644 --- a/docs/src/main/asciidoc/doc-reference.adoc +++ b/docs/src/main/asciidoc/doc-reference.adoc @@ -23,10 +23,10 @@ The Asciidoc files can be found in the `src/main/asciidoc` directory within the Create new documentation files using the appropriate template for the content type: -Concepts:: Use `src/main/asciidoc/_templates/template-concepts.adoc` -How-To Guides:: Use `src/main/asciidoc/_templates/template-howto.adoc` -Reference:: Use `src/main/asciidoc/_templates/template-reference.adoc` -Tutorials:: Use `src/main/asciidoc/_templates/template-tutorial.adoc` +Concepts:: Use `docs/src/main/asciidoc/_templates/template-concepts.adoc` +How-To Guides:: Use `docs/src/main/asciidoc/_templates/template-howto.adoc` +Reference:: Use `docs/src/main/asciidoc/_templates/template-reference.adoc` +Tutorials:: Use `docs/src/main/asciidoc/_templates/template-tutorial.adoc` == Output locations @@ -138,10 +138,16 @@ Quarkus documentation is built from source in a few different environments. We use attributes in our cross-references to ensure our docs can be built across these environments. .Cross-reference source attributes -[source,asciidoc] ----- -include::attributes.adoc[tag=xref-attributes] ----- +[cols=" ---- -<1> The cross reference starts with `xref:`, uses a cross-reference source attribute(`\{doc-guides}`), and provides a readable description: `[Quarkus Documentation concepts]` +<1> The cross reference starts with `xref:`, uses a cross-reference source attribute(`\{doc-guides}`), and provides a readable description: `[Quarkus Documentation concepts]`. + +=== Referencing source code + +There are many ways to include source examples in documentation. + +The simplest way is to write it directly in the file, like this: + +[source,asciidoc] +----- +[source,java] +---- +System.out.println("Hello, World!"); +---- +----- + +For tutorials, you may want to reference code that you know is being built and tested regularly. +As of 2.10-ish, the Quarkus documentation module build will look for source files in `integration-tests` modules that are in the `documentaton.examples` package. +It will copy those files into a flattened structure in the `target/asciidoc/examples` directory (from the project root). +Copied content can then be referenced using the `\{code-examples}` source attribute + +.Micrometer example +- Integration test source: ++ +`integration-tests/micrometer-prometheus/src/main/java/documentation/example/telemetry/micrometer/tutorial/ExampleResource.java` + +- Copy for inclusion in docs (flattened file structure): ++ +`target/asciidoc/examples/telemetry-micrometer-tutorial-ExampleResource.java`. +- Content is included using the following: ++ +`\{code-examples}/telemetry-micrometer-tutorial-ExampleResource.java`. diff --git a/docs/src/main/asciidoc/telemetry-micrometer-tutorial.adoc b/docs/src/main/asciidoc/telemetry-micrometer-tutorial.adoc new file mode 100644 index 0000000000000..b69ae2cb6abf1 --- /dev/null +++ b/docs/src/main/asciidoc/telemetry-micrometer-tutorial.adoc @@ -0,0 +1,200 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// +[id="tutorial-micrometer"] += Collect metrics using Micrometer +include::./attributes.adoc[] + +Create an application that uses the Micrometer metrics library to collect runtime, extension and application metrics and expose them as a Prometheus (OpenMetrics) endpoint. + +== Prerequisites + +include::{includes}/prerequisites.adoc[] + +== Solution + +We recommend that you follow the instructions to create the application step by step, but you can skip right to the solution if you prefer. Either: + +* Clone the git repository: `git clone {quickstarts-clone-url}`, or +* Download an {quickstarts-archive-url}[archive]. + +The solution is located in the `micrometer-quickstart` {quickstarts-tree-url}/micrometer-quickstart[directory]. + +:sectnums: +:sectnumlevels: 3 +== Creating the Maven Project + +Create a new project with the following command: + +:create-app-artifact-id: micrometer-quickstart +:create-app-extensions: resteasy-reactive,micrometer-registry-prometheus +include::{includes}/devtools/create-app.adoc[] + +This command generates a Maven project, that imports the `micrometer-registry-prometheus` extension as a dependency. +This extension will load the core `micrometer` extension as well as additional library dependencies required to support prometheus. + +== Writing the application + +Let's first add a simple endpoint that calculates prime numbers. + +[source,java] +---- +include::{code-examples}/telemetry-micrometer-tutorial-ExampleResource.java[tags=example;!ignore;!registry;!gauge;!counter;!timer] +---- + +Start your application in dev mode: + +include::{includes}/devtools/dev.adoc[] + +=== Review automatically generated metrics + +The Micrometer extension automatically times HTTP server requests. + +Let's use `curl` (or a browser) to visit our endpoint a few times: + +[source,shell] +---- +curl http://localhost:8080/example/prime/256 +curl http://localhost:8080/example/prime/7919 +---- + +The Micrometer Prometheus MeterRegistry extension creates an endpoint we can use to observe collected metrics. Let's take a look at the metrics that have been collected: + +[source,shell] +---- +curl http://localhost:8080/q/metrics +---- + +Look for `http_server_requests_seconds_count`, `http_server_requests_seconds_sum`, and +`http_server_requests_seconds_max` in the output. + +Dimensional labels are added for the request uri, the HTTP method +(GET, POST, etc.), the status code (200, 302, 404, etc.), and a more general outcome field. You should find something like this: + +[source,text] +---- +# HELP http_server_requests_seconds +# TYPE http_server_requests_seconds summary +http_server_requests_seconds_count{method="GET",outcome="SUCCESS",status="200",uri="/example/prime/{number}",} 2.0 +http_server_requests_seconds_sum{method="GET",outcome="SUCCESS",status="200",uri="/example/prime/{number}",} 0.017385896 +# HELP http_server_requests_seconds_max +# TYPE http_server_requests_seconds_max gauge +http_server_requests_seconds_max{method="GET",outcome="SUCCESS",status="200",uri="/example/prime/{number}",} 0.017385896 +# +---- + +NOTE: Metrics appear lazily, you often won't see any data for your endpoint until it is accessed. + +== Inject the MeterRegistry + +To register meters, you need a reference to the `MeterRegistry` that is configured and maintained by the Micrometer extension. + +The `MeterRegistry` can be injected into your application as follows: + +[source,java] +---- +include::{code-examples}/telemetry-micrometer-tutorial-ExampleResource.java[tags=registry;!gauge] +---- + +== Add a Counter + +Counters are used to measure values that only increase. + +Let's add a counter that tracks how often we test a number to see if it is prime. +We'll add a dimensional label (also called an attribute or a tag) that will allow us to aggregate this counter value in different ways. + +[source,java] +---- +include::{code-examples}/telemetry-micrometer-tutorial-ExampleResource.java[tags=counted;!ignore;!timer] +---- + +<1> Find or create a counter called `example.prime.number` that has a `type` label with the specified value. +<2> Increment that counter. + +=== Review collected metrics + +If you did not leave Quarkus running in dev mode, start it again: + +include::{includes}/devtools/dev.adoc[] + +Try the following sequence and look for `example_prime_number_total` in the plain text +output. + +Note that the `_total` suffix is added when Micrometer applies Prometheus naming conventions to +`example.prime.number`, the originally specified counter name. + +[source,shell] +---- +curl http://localhost:8080/example/prime/-1 +curl http://localhost:8080/example/prime/0 +curl http://localhost:8080/example/prime/1 +curl http://localhost:8080/example/prime/2 +curl http://localhost:8080/example/prime/3 +curl http://localhost:8080/example/prime/15 +curl http://localhost:8080/q/metrics +---- + +Notice that there is one measured value for each unique combination of `example_prime_number_total` and `type` value. + +Looking at the dimensional data produced by this counter, you can count: + +- how often a negative number was checked: `type="not-natural"` +- how often the number one was checked: `type="one"` +- how often an even number was checked: `type="even"` +- how often a prime number was checked: `type="prime"` +- how often a non-prime number was checked: `type="not-prime"` + +You can also count how often a number was checked (generally) by aggregating all of these values together. + +== Add a Timer + +Timers are a specialized abstraction for measuring duration. Let's add a timer to measure how long it takes to determine if a number is prime. + +[source,java] +---- +include::{code-examples}/telemetry-micrometer-tutorial-ExampleResource.java[tags=timed;!ignore;!default] +---- + +<1> Find or create a counter called `example.prime.number` that has a `type` label with the specified value. +<2> Increment that counter. +<3> Call a method that wraps the original `testPrimeNumber` method. +<4> Create a `Timer.Sample` that tracks the start time +<5> Call the method to be timed and store the boolean result +<6> Find or create a `Timer` using the specified id and a `prime` label with the result value, and record the duration captured by the `Timer.Sample`. + +=== Review collected metrics + +If you did not leave Quarkus running in dev mode, start it again: + +include::{includes}/devtools/dev.adoc[] + +Micrometer will apply Prometheus conventions when emitting metrics for this timer. +Specifically, measured durations are converted into seconds and this unit is included in the metric name. + +Try the following sequence and look for the following entries in the plain text output: + +- `example_prime_number_test_seconds_count` -- how many times the method was called +- `example_prime_number_test_seconds_sum` -- the total duration of all method calls +- `example_prime_number_test_seconds_max` -- the maximum observed duration within a decaying interval. This value will return to 0 if the method is not invoked frequently. + +[source,shell] +---- +curl http://localhost:8080/example/prime/256 +curl http://localhost:8080/q/metrics +curl http://localhost:8080/example/prime/7919 +curl http://localhost:8080/q/metrics +---- + +Looking at the dimensional data produced by this counter, you can use the sum and the count to calculate how long (on average) it takes to determine if a number is prime. Using the dimensional label, you might be able to understand if there is a significant difference in duration for numbers that are prime when compared with numbers that are not. + +:sectnums!: +== Summary + +Congratulations! + +You have created a project that uses the Micrometer and Prometheus Meter Registry extensions to collect metrics. You've observed some of the metrics that Quarkus captures automatically, and have added a `Counter` and `Timer` that are unique to the application. You've also added dimensional labels to metrics, and have observed how those labels shape the data emitted by the prometheus endpoint. + + + diff --git a/docs/src/main/java/io/quarkus/docs/generation/CopyJavaExamples.java b/docs/src/main/java/io/quarkus/docs/generation/CopyJavaExamples.java new file mode 100755 index 0000000000000..598109a3bdddc --- /dev/null +++ b/docs/src/main/java/io/quarkus/docs/generation/CopyJavaExamples.java @@ -0,0 +1,63 @@ +package io.quarkus.docs.generation; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class CopyJavaExamples { + + public Path outputPath; + public List paths; + + // ${project.basedir}/../target/asciidoc/generated/examples ${project.basedir}/../integration-tests + public static void main(String[] args) throws Exception { + if (args.length < 2) { + System.err.println("Must specify target output directory (first) followed by at least one source directory"); + System.exit(1); + } + CopyJavaExamples javaExamples = new CopyJavaExamples(); + + javaExamples.outputPath = Path.of(args[0]); + javaExamples.paths = Arrays.stream(args).skip(1) + .map(Path::of) + .collect(Collectors.toList()); + + try { + javaExamples.run(); + } catch (Exception e) { + System.err.println("Exception occurred while trying to copy java examples"); + e.printStackTrace(); + System.exit(1); + } + } + + public void run() throws Exception { + if (outputPath != null) { + Files.createDirectories(outputPath); + } + + for (Path path : paths) { + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (file.toString().contains("src/main/java/documentation/example/")) { + String name = file.toString(); + name = name.substring(name.indexOf("example/") + 8) + .replace("/", "-"); + + Files.copy(file, outputPath.resolve(name), StandardCopyOption.REPLACE_EXISTING); + } + return FileVisitResult.CONTINUE; + } + }); + } + } + +} diff --git a/integration-tests/micrometer-prometheus/src/main/java/documentation/example/telemetry/micrometer/tutorial/ExampleResource.java b/integration-tests/micrometer-prometheus/src/main/java/documentation/example/telemetry/micrometer/tutorial/ExampleResource.java new file mode 100644 index 0000000000000..15b176d1d5bb4 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/main/java/documentation/example/telemetry/micrometer/tutorial/ExampleResource.java @@ -0,0 +1,133 @@ +// tag::example[] +// tag::ignore[] +// This example is maintained in quarkus micrometer integration tests +// integration-tests/micrometer-prometheus/src/main/java/documentation/example/telemetry/micrometer/tutorial/ExampleResource.java +package documentation.example.telemetry.micrometer.tutorial; +/* +// end::ignore[] +package org.acme.micrometer; +// tag::ignore[] +*/ +// end::ignore[] + +import java.util.LinkedList; +import java.util.NoSuchElementException; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; + +@Path("/example") +@Produces("text/plain") +public class ExampleResource { + private final LinkedList list = new LinkedList<>(); + // tag::registry[] + private final MeterRegistry registry; + + ExampleResource(MeterRegistry registry) { + this.registry = registry; + // tag::gauge[] + registry.gaugeCollectionSize("example.list.size", Tags.empty(), list); + // end::gauge[] + } + // end::registry[] + // tag::gauge[] + + @GET + @Path("gauge/{number}") + public Long checkListSize(@PathParam("number") long number) { + if (number == 2 || number % 2 == 0) { + // add even numbers to the list + list.add(number); + } else { + // remove items from the list for odd numbers + try { + number = list.removeFirst(); + } catch (NoSuchElementException nse) { + number = 0; + } + } + return number; + } + // end::gauge[] + + // tag::timed[] + // tag::counted[] + @GET + @Path("prime/{number}") + public String checkIfPrime(@PathParam("number") long number) { + if (number < 1) { + // tag::counter[] + registry.counter("example.prime.number", "type", "not-natural") // <1> + .increment(); // <2> + // end::counter[] + return "Only natural numbers can be prime numbers."; + } + if (number == 1) { + // tag::counter[] + registry.counter("example.prime.number", "type", "one") // <1> + .increment(); // <2> + // end::counter[] + return number + " is not prime."; + } + if (number == 2 || number % 2 == 0) { + // tag::counter[] + registry.counter("example.prime.number", "type", "even") // <1> + .increment(); // <2> + // end::counter[] + return number + " is not prime."; + } + + // tag::timer[] + if (timedTestPrimeNumber(number)) { // <3> + // end::timer[] + // tag::ignore[] + /* + } + // end::ignore[] + // tag::default[] + if (testPrimeNumber(number)) { + // end::default[] + // tag::ignore[] + */ + // end::ignore[] + // tag::counter[] + registry.counter("example.prime.number", "type", "prime") // <1> + .increment(); // <2> + // end::counter[] + return number + " is prime."; + } else { + // tag::counter[] + registry.counter("example.prime.number", "type", "not-prime") // <1> + .increment(); // <2> + // end::counter[] + return number + " is not prime."; + } + } + // end::counted[] + // tag::timer[] + + protected boolean timedTestPrimeNumber(long number){ + Timer.Sample sample = Timer.start(registry); // <4> + boolean result = testPrimeNumber(number); // <5> + sample.stop(registry.timer("example.prime.number.test", "prime", result + "")); // <6> + return result; + } + // end::timer[] + // end::timed[] + + protected boolean testPrimeNumber(long number) { + for (int i = 3; i < Math.floor(Math.sqrt(number)) + 1; i = i + 2) { + if (number % i == 0) { + return false; + } + } + return true; + } +} +// end::example[] diff --git a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/ExampleResource.java b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/ExampleResource.java deleted file mode 100644 index 627344ea7505d..0000000000000 --- a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/ExampleResource.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.quarkus.it.micrometer.prometheus; - -import java.util.LinkedList; -import java.util.NoSuchElementException; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tags; -import io.micrometer.core.instrument.Timer; - -@Path("/example") -@Produces("text/plain") -public class ExampleResource { - - private final MeterRegistry registry; - - LinkedList list = new LinkedList<>(); - - ExampleResource(MeterRegistry registry) { - this.registry = registry; - registry.gaugeCollectionSize("example.list.size", Tags.empty(), list); - } - - @GET - @Path("gauge/{number}") - public Long checkListSize(@PathParam("number") long number) { - if (number == 2 || number % 2 == 0) { - // add even numbers to the list - list.add(number); - } else { - // remove items from the list for odd numbers - try { - number = list.removeFirst(); - } catch (NoSuchElementException nse) { - number = 0; - } - } - return number; - } - - @GET - @Path("prime/{number}") - public String checkIfPrime(@PathParam("number") long number) { - if (number < 1) { - registry.counter("example.prime.number", "type", "not-natural").increment(); - return "Only natural numbers can be prime numbers."; - } - if (number == 1) { - registry.counter("example.prime.number", "type", "one").increment(); - return number + " is not prime."; - } - if (number == 2 || number % 2 == 0) { - registry.counter("example.prime.number", "type", "even").increment(); - return number + " is not prime."; - } - - if (testPrimeNumber(number)) { - registry.counter("example.prime.number", "type", "prime").increment(); - return number + " is prime."; - } else { - registry.counter("example.prime.number", "type", "not-prime").increment(); - return number + " is not prime."; - } - } - - protected boolean testPrimeNumber(long number) { - Timer timer = registry.timer("example.prime.number.test"); - return timer.record(() -> { - for (int i = 3; i < Math.floor(Math.sqrt(number)) + 1; i = i + 2) { - if (number % i == 0) { - return false; - } - } - return true; - }); - } -}