diff --git a/.github/workflows/doc-build.yml b/.github/workflows/doc-build.yml index 555944bd55789..475721a14fd4c 100644 --- a/.github/workflows/doc-build.yml +++ b/.github/workflows/doc-build.yml @@ -52,7 +52,8 @@ jobs: key: q2maven-doc-${{ steps.get-date.outputs.date }} - name: Build run: | - ./mvnw -Dquickly-ci -B --settings .github/mvn-settings.xml install + ./mvnw -Dquickly-ci -B -DskipDocs=false --settings .github/mvn-settings.xml install + - name: Build Docs run: | ./mvnw -e -B --settings .github/mvn-settings.xml clean org.asciidoctor:asciidoctor-maven-plugin:process-asciidoc -pl docs -Ddocumentation-pdf diff --git a/docs/pom.xml b/docs/pom.xml index 9b40ac0c59e36..a22583d71d58d 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,25 @@ + + copy-tagged-java-source + process-classes + + java + + + ${skipDocs} + io.quarkus.docs.generation.CopyExampleSource + + ${code-example-dir} + ${project.basedir}/.. + ${project.basedir}/src/main/asciidoc + + + ${env.MAVEN_CMD_LINE_ARGS} + + + generate-doc-manifests prepare-package @@ -2830,6 +2850,7 @@ true ${project.basedir}/../target/asciidoc/generated + ${code-example-dir} ./images font true @@ -2912,7 +2933,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/ 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 7b330a696f2b0..f7d6d8046196d 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 cbd77b52c10e1..bc434590ce30e 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 @@ -183,10 +183,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]`. + +=== Reference source code + +There are many ways to include source code and examples in documentation. + +The simplest is to write it directly in the file, like this: + +[source,asciidoc] +----- +[source,java] +---- +System.out.println("Hello, World!"); +---- +----- + +In documents like tutorials, you may want to reference source code that is built and tested regularly. +The Quarkus documentation module build will copy source files enumerated in `*-examples/yaml` files into a flattened structure in the `target/asciidoc/examples` directory (from the project root). + +[source,yaml] +---- +examples: +- source: path/to/source/file/SomeClassFile.java <1> + target: prefix-simplified-unique-filename.java <2> +---- -=== Variables for use in documents +<1> define the path of source to be copied +<2> define the simplified target file name to use when copying the file into the `target/asciidoc/examples` directory. We recommend using the same prefix as the related/consuming documentation in the file name. -The following variables externalize key information that can change over time, and so references -to such information should be done by using the variable inside of {} curly brackets. The -complete list of externalized variables for use is given in the following table: +Content copied in this way is referenced using the `\{code-examples}` source attribute. If a copied file contains the literal string `{{source}}`, that literal value is replaced with the path of the source file. + +.Micrometer example +* The source file to be copied is: ++ +`integration-tests/micrometer-prometheus/src/main/java/documentation/example/telemetry/micrometer/tutorial/ExampleResource.java` + +* The target file name we want to use in docs is: ++ +`telemetry-micrometer-tutorial-example-resource.java`. + +* The source and target file names are declared in `docs/src/main/asciidoc/telemetry-examples.yaml`: ++ +[source,yaml] +---- +examples: +- source: integration-tests/micrometer-prometheus/src/main/java/io/quarkus/doc/micrometer/ExampleResource.java + target: telemetry-micrometer-tutorial-example-resource.java +---- + +* Snippets from this source file are then included using the following path: ++ +`\{code-examples}/telemetry-micrometer-tutorial-example-resource.java`. +* The source file contains the following comment: +[source,java] +---- +// Source: {{source}} +---- +* The copied file contains this comment instead: +[source,java] +---- +// Source: integration-tests/micrometer-prometheus/src/main/java/io/quarkus/doc/micrometer/ExampleResource.java +---- + +=== Quarkus documentation variables + +The following variables externalize key information that can change over time. References +to such information should use the variable inside of curly brackets, `{}`. + +The complete list of externalized variables for use is given in the following table: .Variables [cols=" 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-example-resource.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/CopyExampleSource.java b/docs/src/main/java/io/quarkus/docs/generation/CopyExampleSource.java new file mode 100755 index 0000000000000..e8ccbb845fada --- /dev/null +++ b/docs/src/main/java/io/quarkus/docs/generation/CopyExampleSource.java @@ -0,0 +1,171 @@ +package io.quarkus.docs.generation; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; + +public class CopyExampleSource { + + static Path docsDir() { + Path path = Paths.get(System.getProperty("user.dir")); + if (path.endsWith("docs")) { + return path; + } + return path.resolve("docs"); + } + + public Path outputPath; + public Path rootPath; + public List srcPaths; + Map allTargets = new HashMap<>(); + + // Two arguments: + // ${project.basedir}/../target/asciidoc/generated/examples ${project.basedir}/.. ${project.basedir}/src/main/asciidoc + public static void main(String[] args) throws Exception { + CopyExampleSource copyExamples = new CopyExampleSource(); + + // Required first parameter: Target output directory + if (args.length < 1) { + System.err.println("Must specify target output directory"); + System.exit(1); + } + copyExamples.outputPath = Path.of(args[0]).normalize(); + System.out.println("[INFO] Output directory: " + copyExamples.outputPath); + + // Optional second parameter: Project root directory + if (args.length > 1) { + copyExamples.rootPath = Path.of(args[1]).normalize(); + } else { + copyExamples.rootPath = docsDir().resolve("..").normalize(); + } + System.out.println("[INFO] Project root: " + copyExamples.rootPath); + + // third parameter and on .. source paths + if (args.length > 2) { + copyExamples.srcPaths = Arrays.stream(args).skip(2) + .map(x -> Path.of(x).normalize()) + .collect(Collectors.toList()); + } else { + copyExamples.srcPaths = List.of(docsDir().resolve("src/main/asciidoc").normalize()); + } + + try { + copyExamples.run(); + } catch (Exception e) { + System.err.println("Exception occurred while trying to copy examples"); + e.printStackTrace(); + System.exit(1); + } + } + + public void run() throws Exception { + Files.createDirectories(outputPath); + ObjectMapper om = new ObjectMapper(new YAMLFactory().enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)) + .setVisibility(PropertyAccessor.FIELD, Visibility.ANY); + + for (Path path : srcPaths) { + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { + if (path.toString().endsWith("-examples.yaml")) { + System.out.println("[INFO] Reading: " + path); + + MappingList mapping = om.readValue(path.toFile(), MappingList.class); + + // For each example: + for (Example example : mapping.examples) { + + // Resolve the source path against the root directory + Path relativePath = Path.of(example.source); + Path sourcePath = rootPath.resolve(relativePath); + // Resolve the target path against the output directory + Path targetPath = outputPath.resolve(example.target); + + if (Files.exists(sourcePath)) { + // Record an error if the target already exists + String former = allTargets.put(example.target, example.source); + if (former != null) { + System.err.printf( + "[ERROR] Duplicate target: %s%n Previous value: %s%n New value: %s%n", + example.target, former, example.source); + + mapping.duplicateKey = true; + continue; + } + + // Make sure required target directories exist + Files.createDirectories(targetPath.getParent()); + + // Copy the source file to the target file. + // Replace {{source}} in comment lines with the relative source path + try (BufferedReader br = new BufferedReader( + new InputStreamReader(Files.newInputStream(sourcePath, StandardOpenOption.READ), + "UTF-8"))) { + try (PrintWriter bw = new PrintWriter(new OutputStreamWriter( + Files.newOutputStream(targetPath, StandardOpenOption.CREATE, + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)))) { + String line; + while ((line = br.readLine()) != null) { + if (line.startsWith("// Source: {{source}}")) { + bw.println("// Source: " + relativePath); + } else if (line.startsWith("# Source: {{source}}")) { + bw.println("# Source: " + relativePath); + } else { + bw.println(line); + } + } + System.out.printf("[INFO] Copied %s %n to %s%n", relativePath, targetPath); + } + } catch (IOException ioe) { + System.err.printf("[ERROR] Error copying %s %n to %s%n", relativePath, + targetPath); + throw ioe; + } + } else { + System.err.println("[ERROR] Specified source file doesn't exist: " + sourcePath); + mapping.missingSource = true; + } + } + } + return FileVisitResult.CONTINUE; + } + }); + } + } + + static class MappingList { + List examples; + + @JsonIgnore + boolean missingSource = false; + + @JsonIgnore + boolean duplicateKey = false; + } + + static class Example { + String source; + String target; + } +} 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/doc/micrometer/ExampleResource.java similarity index 50% rename from integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/ExampleResource.java rename to integration-tests/micrometer-prometheus/src/main/java/io/quarkus/doc/micrometer/ExampleResource.java index 627344ea7505d..f66df272b1d5e 100644 --- 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/doc/micrometer/ExampleResource.java @@ -1,5 +1,15 @@ -package io.quarkus.it.micrometer.prometheus; +// tag::example[] +// tag::ignore[] +// Source: {{source}} +package io.quarkus.doc.micrometer; +/* +// end::ignore[] +package org.acme.micrometer; + +// tag::ignore[] +*/ +// end::ignore[] import java.util.LinkedList; import java.util.NoSuchElementException; @@ -15,15 +25,18 @@ @Path("/example") @Produces("text/plain") public class ExampleResource { - + private final LinkedList list = new LinkedList<>(); + // tag::registry[] private final MeterRegistry registry; - LinkedList list = new LinkedList<>(); - 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}") @@ -41,41 +54,80 @@ public Long checkListSize(@PathParam("number") long number) { } return number; } + // end::gauge[] + // tag::timed[] + // tag::counted[] @GET @Path("prime/{number}") public String checkIfPrime(@PathParam("number") long number) { if (number < 1) { - registry.counter("example.prime.number", "type", "not-natural").increment(); + // 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) { - registry.counter("example.prime.number", "type", "one").increment(); + // 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) { - registry.counter("example.prime.number", "type", "even").increment(); + // tag::counter[] + registry.counter("example.prime.number", "type", "even") // <1> + .increment(); // <2> + // end::counter[] return number + " is not prime."; } - if (testPrimeNumber(number)) { - registry.counter("example.prime.number", "type", "prime").increment(); + // 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 { - registry.counter("example.prime.number", "type", "not-prime").increment(); + // 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) { - 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; - } + for (int i = 3; i < Math.floor(Math.sqrt(number)) + 1; i = i + 2) { + if (number % i == 0) { + return false; } - return true; - }); + } + return true; } } +// end::example[] diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExampleResourcesTest.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExampleResourcesTest.java index 0cd5c7e874adb..814147eae4f1e 100644 --- a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExampleResourcesTest.java +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExampleResourcesTest.java @@ -54,14 +54,14 @@ void testTimerExample() { when().get("/example/prime/257").then().statusCode(200); when().get("/q/metrics").then().statusCode(200) .body(containsString( - "example_prime_number_test_seconds_sum{env=\"test\",registry=\"prometheus\",}")) + "example_prime_number_test_seconds_sum{env=\"test\",prime=\"true\",registry=\"prometheus\",}")) .body(containsString( - "example_prime_number_test_seconds_max{env=\"test\",registry=\"prometheus\",}")) + "example_prime_number_test_seconds_max{env=\"test\",prime=\"true\",registry=\"prometheus\",}")) .body(containsString( - "example_prime_number_test_seconds_count{env=\"test\",registry=\"prometheus\",} 1.0")); + "example_prime_number_test_seconds_count{env=\"test\",prime=\"true\",registry=\"prometheus\",} 1.0")); when().get("/example/prime/7919").then().statusCode(200); when().get("/q/metrics").then().statusCode(200) .body(containsString( - "example_prime_number_test_seconds_count{env=\"test\",registry=\"prometheus\",} 2.0")); + "example_prime_number_test_seconds_count{env=\"test\",prime=\"true\",registry=\"prometheus\",} 2.0")); } }