From 71f9fa830bb18106271847fbcad2fad2229b9413 Mon Sep 17 00:00:00 2001 From: Tadaya Tsuyukubo Date: Thu, 6 Oct 2022 14:31:17 -0700 Subject: [PATCH] Add "micrometer-docs-generator" module (#40) Add a new module "micrometer-docs-generator" which combines existing commons, metrics, and spans modules. In this module, `DocsGeneratorCommand` is the entry point to execute the document generation. By default, it generates all `_metrics.adoc`, `_spans.adoc`, and `_conventions.adoc` files. The command takes `--metrics`, `--spans`, `--conventions` optional arguments. Once these options are provided, only specified docs are generated. --- dependencies.gradle | 1 + micrometer-docs-generator/build.gradle | 19 + .../micrometer/docs/DocsGeneratorCommand.java | 110 +++++ .../docs/commons/KeyValueEntry.java | 79 ++++ .../micrometer/docs/commons/ParsingUtils.java | 384 ++++++++++++++++++ .../docs/commons/templates/ADocHelpers.java | 30 ++ .../commons/templates/HandlebarsUtils.java | 46 +++ .../docs/commons/utils/AsciidocUtils.java | 145 +++++++ .../micrometer/docs/commons/utils/Assert.java | 25 ++ .../docs/commons/utils/StringUtils.java | 49 +++ .../ObservationConventionEntry.java | 71 ++++ ...rvationConventionSearchingFileVisitor.java | 106 +++++ .../ObservationConventionsDocGenerator.java | 84 ++++ .../micrometer/docs/metrics/MetricEntry.java | 180 ++++++++ .../metrics/MetricSearchingFileVisitor.java | 216 ++++++++++ .../docs/metrics/MetricsDocGenerator.java | 83 ++++ .../io/micrometer/docs/spans/SpanEntry.java | 169 ++++++++ .../docs/spans/SpanSearchingFileVisitor.java | 230 +++++++++++ .../docs/spans/SpansDocGenerator.java | 80 ++++ .../src/main/resources/logback.xml | 26 ++ .../resources/templates/conventions.adoc.hbs | 31 ++ .../main/resources/templates/metrics.adoc.hbs | 68 ++++ .../main/resources/templates/spans.adoc.hbs | 48 +++ .../ObservationConventionEntryTests.java | 62 +++ .../docs/metrics/AnnotationObservation.java | 101 +++++ .../docs/metrics/AsyncObservation.java | 171 ++++++++ .../docs/metrics/DocsFromSourcesTests.java | 64 +++ .../docs/metrics/EventObservation.java | 81 ++++ .../docs/metrics/MyDistributionSummary.java | 65 +++ .../metrics/MyOtherDistributionSummary.java | 60 +++ .../metrics/PublicObservationConvention.java | 33 ++ .../sanitizing/ComplexJavadocTest.java | 39 ++ .../WithComplexJavadocDocumentedMeter.java | 152 +++++++ .../spans/conventions/AnnotationSpan.java | 177 ++++++++ .../spans/conventions/ConventionsTests.java | 45 ++ .../DynamicObservationConvention.java | 33 ++ .../ObservationConventionInterface.java | 34 ++ ...cExtendingGlobalObservationConvention.java | 32 ++ .../PublicGlobalObservationConvention.java | 33 ++ .../PublicObservationConvention.java | 33 ++ .../UseInterfaceObservationConvention.java | 20 + .../docs/spans/test1/AnnotationSpan.java | 111 +++++ .../docs/spans/test1/AsyncSpan.java | 96 +++++ .../docs/spans/test1/EventObservation.java | 81 ++++ ...oOverridingOfTagsDocsFromSourcesTests.java | 52 +++ .../docs/spans/test1/ParentSample.java | 105 +++++ .../docs/spans/test2/OverridingSpan.java | 58 +++ .../docs/spans/test2/ParentSample.java | 99 +++++ .../TagsFromTagKeysDocsFromSourcesTests.java | 44 ++ .../docs/spans/test3/OverridingSpan.java | 59 +++ .../docs/spans/test3/ParentSample.java | 99 +++++ ...entWithOverridingDocsFromSourcesTests.java | 48 +++ .../test/resources/expected-sanitizing.adoc | 106 +++++ .../src/test/resources/logback.xml | 46 +++ settings.gradle | 1 + 55 files changed, 4520 insertions(+) create mode 100644 micrometer-docs-generator/build.gradle create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/DocsGeneratorCommand.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/KeyValueEntry.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/ParsingUtils.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/templates/ADocHelpers.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/templates/HandlebarsUtils.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/AsciidocUtils.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/Assert.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/StringUtils.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionEntry.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionSearchingFileVisitor.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionsDocGenerator.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricEntry.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricSearchingFileVisitor.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricsDocGenerator.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpanEntry.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpanSearchingFileVisitor.java create mode 100644 micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpansDocGenerator.java create mode 100644 micrometer-docs-generator/src/main/resources/logback.xml create mode 100644 micrometer-docs-generator/src/main/resources/templates/conventions.adoc.hbs create mode 100644 micrometer-docs-generator/src/main/resources/templates/metrics.adoc.hbs create mode 100644 micrometer-docs-generator/src/main/resources/templates/spans.adoc.hbs create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/conventions/ObservationConventionEntryTests.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/AnnotationObservation.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/AsyncObservation.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/DocsFromSourcesTests.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/EventObservation.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/MyDistributionSummary.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/MyOtherDistributionSummary.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/PublicObservationConvention.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/usecases/sanitizing/ComplexJavadocTest.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/usecases/sanitizing/WithComplexJavadocDocumentedMeter.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/AnnotationSpan.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/ConventionsTests.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/DynamicObservationConvention.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/ObservationConventionInterface.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicExtendingGlobalObservationConvention.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicGlobalObservationConvention.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicObservationConvention.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/UseInterfaceObservationConvention.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/AnnotationSpan.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/AsyncSpan.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/EventObservation.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/NoOverridingOfTagsDocsFromSourcesTests.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/ParentSample.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/OverridingSpan.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/ParentSample.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/TagsFromTagKeysDocsFromSourcesTests.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/OverridingSpan.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/ParentSample.java create mode 100644 micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/TagsFromParentWithOverridingDocsFromSourcesTests.java create mode 100644 micrometer-docs-generator/src/test/resources/expected-sanitizing.adoc create mode 100644 micrometer-docs-generator/src/test/resources/logback.xml diff --git a/dependencies.gradle b/dependencies.gradle index 57bdf81..fabe1ff 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -2,6 +2,7 @@ def VERSIONS = [ 'org.jboss.forge.roaster:roaster-api:2.22.3.Final', // last using jdk8 'org.jboss.forge.roaster:roaster-jdt:2.22.3.Final', // last using jdk8 'com.github.jknack:handlebars:latest.release', + 'info.picocli:picocli:latest.release', // logging 'ch.qos.logback:logback-classic:1.2.+', diff --git a/micrometer-docs-generator/build.gradle b/micrometer-docs-generator/build.gradle new file mode 100644 index 0000000..ddac9ca --- /dev/null +++ b/micrometer-docs-generator/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'idea' +} + +dependencies { + api 'io.micrometer:micrometer-observation' + api 'io.micrometer:micrometer-core' + api 'io.micrometer:micrometer-tracing' + api 'org.jboss.forge.roaster:roaster-api' + api 'org.jboss.forge.roaster:roaster-jdt' + api 'ch.qos.logback:logback-classic' + api 'info.picocli:picocli' + api 'com.github.jknack:handlebars' + + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.mockito:mockito-core' +} + diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/DocsGeneratorCommand.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/DocsGeneratorCommand.java new file mode 100644 index 0000000..ab5c2da --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/DocsGeneratorCommand.java @@ -0,0 +1,110 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs; + +import java.io.File; +import java.util.regex.Pattern; + +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.docs.conventions.ObservationConventionsDocGenerator; +import io.micrometer.docs.metrics.MetricsDocGenerator; +import io.micrometer.docs.spans.SpansDocGenerator; +import picocli.CommandLine; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Entry point for document generation. + * + * @author Tadaya Tsuyukubo + */ +@Command(mixinStandardHelpOptions = true, description = "Generate documentation from source files") +public class DocsGeneratorCommand implements Runnable { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DocsGeneratorCommand.class); + + @ArgGroup(exclusive = false) + private final Options options = new Options(); + + @Parameters(index = "0", description = "The project root directory.") + private File projectRoot; + + @Parameters(index = "1", description = "The regex pattern for inclusion.") + private Pattern inclusionPattern; + + @Parameters(index = "2", description = "The output directory.") + private File outputDir; + + public static void main(String... args) { + new CommandLine(new DocsGeneratorCommand()).execute(args); + // Do not call System.exit here since exec-maven-plugin stops the maven run + } + + @Override + public void run() { + this.inclusionPattern = Pattern.compile(this.inclusionPattern.pattern().replace("/", File.separator)); + logger.info("Project root: {}", this.projectRoot); + logger.info("Inclusion pattern: {}", this.inclusionPattern); + logger.info("Output root: {}", this.outputDir); + + this.options.setAllIfNoneSpecified(); + if (this.options.metrics) { + generateMetricsDoc(); + } + if (this.options.spans) { + generateSpansDoc(); + } + if (this.options.conventions) { + generateConventionsDoc(); + } + } + + void generateMetricsDoc() { + new MetricsDocGenerator(this.projectRoot, this.inclusionPattern, this.outputDir).generate(); + } + + void generateSpansDoc() { + new SpansDocGenerator(this.projectRoot, this.inclusionPattern, this.outputDir).generate(); + } + + void generateConventionsDoc() { + new ObservationConventionsDocGenerator(this.projectRoot, this.inclusionPattern, this.outputDir).generate(); + } + + static class Options { + @Option(names = "--metrics", description = "Generate metrics documentation") + private boolean metrics; + + @Option(names = "--spans", description = "Generate spans documentation") + private boolean spans; + + @Option(names = "--conventions", description = "Generate conventions documentation") + private boolean conventions; + + void setAllIfNoneSpecified() { + if (!this.metrics && !this.spans && !this.conventions) { + this.metrics = true; + this.spans = true; + this.conventions = true; + } + } + + } +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/KeyValueEntry.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/KeyValueEntry.java new file mode 100644 index 0000000..c73410e --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/KeyValueEntry.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.commons; + +import java.util.Objects; + +public class KeyValueEntry implements Comparable { + + private final String name; + + private final String description; + + public KeyValueEntry(String name, String description) { + this.name = name; + this.description = description; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + KeyValueEntry tag = (KeyValueEntry) o; + return Objects.equals(name, tag.name) && Objects.equals(description, tag.description); + } + + @Override + public int hashCode() { + return Objects.hash(name, description); + } + + @Override + public int compareTo(KeyValueEntry o) { + return name.compareTo(o.name); + } + + @Override + public String toString() { + return "|`" + name + "`|" + description(); + } + + private String description() { + String suffix = ""; + if (this.name.contains("%s")) { + suffix = " (since the name contains `%s` the final value will be resolved at runtime)"; + } + return description + suffix; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getDisplayDescription() { + // TODO: use handlebar helper to compose the description + return description(); + } +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/ParsingUtils.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/ParsingUtils.java new file mode 100644 index 0000000..859d7ca --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/ParsingUtils.java @@ -0,0 +1,384 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.commons; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.common.lang.Nullable; +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.docs.commons.utils.AsciidocUtils; +import io.micrometer.observation.ObservationConvention; +import org.jboss.forge.roaster.Internal; +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.AbstractTypeDeclaration; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.CompilationUnit; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Expression; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ImportDeclaration; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.MethodDeclaration; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.MethodInvocation; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.QualifiedName; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ReturnStatement; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.StringLiteral; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Type; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.TypeLiteral; +import org.jboss.forge.roaster.model.JavaType; +import org.jboss.forge.roaster.model.JavaUnit; +import org.jboss.forge.roaster.model.impl.AbstractJavaSource; +import org.jboss.forge.roaster.model.impl.JavaClassImpl; +import org.jboss.forge.roaster.model.impl.JavaEnumImpl; +import org.jboss.forge.roaster.model.impl.JavaInterfaceImpl; +import org.jboss.forge.roaster.model.impl.JavaUnitImpl; +import org.jboss.forge.roaster.model.impl.MethodImpl; +import org.jboss.forge.roaster.model.source.EnumConstantSource; +import org.jboss.forge.roaster.model.source.JavaSource; +import org.jboss.forge.roaster.model.source.MemberSource; +import org.jboss.forge.roaster.model.source.MethodSource; + +public class ParsingUtils { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(ParsingUtils.class); + + public static void updateKeyValuesFromEnum(JavaEnumImpl parentEnum, JavaSource source, Class requiredClass, + Collection keyValues, @Nullable String methodName) { + if (!(source instanceof JavaEnumImpl)) { + return; + } + JavaEnumImpl myEnum = (JavaEnumImpl) source; + + // Based on how interfaces are implemented in enum, "myEnum.getInterfaces()" has different values. + // For example, "MyEnum" implements "Observation.Event" interface as: + // - "enum MyEnum implements Observation.Event {" + // "getInterfaces()" returns ["Observation.Event"] + // - "enum MyEnum implements Event {" + // "getInterfaces()" returns ["io.micrometer.observation.Observation.Event"] + // + // To make both cases work, use the simple name("Event" in the above example) for comparison. + if (!myEnum.hasInterface(requiredClass.getSimpleName())) { + return; + } + logger.debug("Checking [" + parentEnum.getName() + "." + myEnum.getName() + "]"); + if (myEnum.getEnumConstants().size() == 0) { + return; + } + for (EnumConstantSource enumConstant : myEnum.getEnumConstants()) { + String keyValue = enumKeyValue(enumConstant, methodName); + keyValues.add(new KeyValueEntry(keyValue, AsciidocUtils.javadocToAsciidoc(enumConstant.getJavaDoc()))); + } + } + + public static String readStringReturnValue(MethodDeclaration methodDeclaration) { + return stringFromReturnMethodDeclaration(methodDeclaration); + } + + @Nullable + public static String tryToReadStringReturnValue(Path file, String clazz) { + try { + return tryToReadNameFromConventionClass(file, clazz); + } catch (Exception ex) { + return null; + } + } + + private static String tryToReadNameFromConventionClass(Path file, String className) { + File parent = file.getParent().toFile(); + while (!parent.getAbsolutePath().endsWith(File.separator + "java")) { // TODO: Works only for Java + parent = parent.getParentFile(); + } + String filePath = filePath(className, parent); + try (InputStream streamForOverride = Files.newInputStream(new File(filePath).toPath())) { + JavaUnit parsedClass = Roaster.parseUnit(streamForOverride); + JavaType actualConventionImplementation; + if (className.contains("$")) { + String actualName = className.substring(className.indexOf("$") + 1); + List nestedTypes = ((AbstractJavaSource) parsedClass.getGoverningType()).getNestedTypes(); + Object foundType = nestedTypes.stream().filter(o -> (o).getName().equals(actualName)).findFirst().orElseThrow(() -> new IllegalStateException("Can't find a class with fqb [" + className + "]")); + actualConventionImplementation = (JavaType) foundType; + } else if (parsedClass instanceof JavaUnitImpl) { + actualConventionImplementation = parsedClass.getGoverningType(); + } else { + return null; + } + if (actualConventionImplementation instanceof JavaClassImpl) { + List interfaces = ((JavaClassImpl) actualConventionImplementation).getInterfaces(); + if (interfaces.stream().noneMatch(s -> s.contains(ObservationConvention.class.getSimpleName()))) { + return null; + } + MethodSource name = ((JavaClassImpl) actualConventionImplementation).getMethod("getName"); + if (name == null) { + // look for the implementing interfaces + for (String iface : interfaces) { + String interfaceFilePath = filePath(iface, parent); + try (InputStream stream = Files.newInputStream(Paths.get(interfaceFilePath))) { + JavaUnit parsed = Roaster.parseUnit(stream); + name = ((JavaInterfaceImpl) parsed.getGoverningType()).getMethod("getName"); + } + if (name != null) { + break; + } + } + } + + MethodSource nameToUse = name; + try { + MethodDeclaration methodDeclaration = (MethodDeclaration) Arrays.stream(MethodImpl.class.getDeclaredFields()).filter(f -> f.getName().equals("method")).findFirst().map(f -> { + try { + f.setAccessible(true); + return f.get(nameToUse); + } + catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }).get(); + return ParsingUtils.readStringReturnValue(methodDeclaration); + } catch (Exception ex) { + return name.toString().replace("return ", "").replace("\"", ""); + } + } + + } + catch (Throwable e) { + throw new RuntimeException(e); + } + return ""; + } + + private static String filePath(String className, File parent) { + if (className.contains("$")) { + return new File(parent, className.replace(".", File.separator).substring(0, className.indexOf("$")) + ".java").getAbsolutePath(); + } + return new File(parent, className.replace(".", File.separator) + ".java").getAbsolutePath(); + } + + public static Collection keyValueEntries(JavaEnumImpl myEnum, MethodDeclaration methodDeclaration, + Class requiredClass) { + return keyValueEntries(myEnum, methodDeclaration, requiredClass, null); + } + + public static Collection keyValueEntries(JavaEnumImpl myEnum, MethodDeclaration methodDeclaration, + Class requiredClass, @Nullable String methodName) { + Collection enumNames = readClassValue(methodDeclaration); + Collection keyValues = new TreeSet<>(); + enumNames.forEach(enumName -> { + List> nestedTypes = myEnum.getNestedTypes(); + JavaSource nestedSource = nestedTypes.stream() + .filter(javaSource -> javaSource.getName().equals(enumName)).findFirst().orElseThrow( + () -> new IllegalStateException("There's no nested type with name [" + enumName + "]")); + ParsingUtils.updateKeyValuesFromEnum(myEnum, nestedSource, requiredClass, keyValues, methodName); + }); + return keyValues; + } + + public static Collection readClassValue(MethodDeclaration methodDeclaration) { + Object statement = methodDeclaration.getBody().statements().get(0); + if (!(statement instanceof ReturnStatement)) { + logger.warn("Statement [" + statement.getClass() + "] is not a return statement."); + return Collections.emptyList(); + } + ReturnStatement returnStatement = (ReturnStatement) statement; + Expression expression = returnStatement.getExpression(); + if (!(expression instanceof MethodInvocation)) { + logger.warn("Statement [" + statement.getClass() + "] is not a method invocation."); + return Collections.emptyList(); + } + MethodInvocation methodInvocation = (MethodInvocation) expression; + if ("merge".equals(methodInvocation.getName().getIdentifier())) { + // TODO: There must be a better way to do this... + // KeyName.merge(TestSpanTags.values(),AsyncSpanTags.values()) + String invocationString = methodInvocation.toString(); + Matcher matcher = Pattern.compile("([a-zA-Z]+.values)").matcher(invocationString); + Collection classNames = new TreeSet<>(); + while (matcher.find()) { + String className = matcher.group(1).split("\\.")[0]; + classNames.add(className); + } + return classNames; + } + else if (!methodInvocation.toString().endsWith(".values()")) { + throw new IllegalStateException("You have to use the static .values() method on the enum that implements " + + KeyName.class + " interface or use [KeyName.merge(...)] method to merge multiple values from tags"); + } + // will return Tags + return Collections.singletonList(methodInvocation.getExpression().toString()); + } + + private static String enumKeyValue(EnumConstantSource enumConstant, @Nullable String methodName) { + List> members = enumConstant.getBody().getMembers(); + if (members.isEmpty()) { + logger.warn("No method declarations in the enum."); + return ""; + } + Object internal; + if (methodName == null) { + internal = members.get(0).getInternal(); + } else { + internal = members.stream().filter(bodyMemberSource -> bodyMemberSource.getName().equals(methodName)).findFirst().map(Internal::getInternal).orElse(null); + if (internal == null) { + logger.warn("Can't find the member with method name [" + methodName + "] on " + enumConstant.getName()); + return ""; + } + } + if (!(internal instanceof MethodDeclaration)) { + logger.warn("Can't read the member [" + internal.getClass() + "] as a method declaration."); + return ""; + } + MethodDeclaration methodDeclaration = (MethodDeclaration) internal; + if (methodDeclaration.getBody().statements().isEmpty()) { + logger.warn("Body was empty. Continuing..."); + return ""; + } + return stringFromReturnMethodDeclaration(methodDeclaration); + } + + private static String stringFromReturnMethodDeclaration(MethodDeclaration methodDeclaration) { + Object statement = methodDeclaration.getBody().statements().get(0); + if (!(statement instanceof ReturnStatement)) { + logger.warn("Statement [" + statement.getClass() + "] is not a return statement."); + return ""; + } + ReturnStatement returnStatement = (ReturnStatement) statement; + Expression expression = returnStatement.getExpression(); + if (!(expression instanceof StringLiteral)) { + logger.warn("Statement [" + statement.getClass() + "] is not a string literal statement."); + return ""; + } + return ((StringLiteral) expression).getLiteralValue(); + } + + @SuppressWarnings("unchecked") + public static T enumFromReturnMethodDeclaration(MethodDeclaration methodDeclaration, Class enumClass) { + Object statement = methodDeclaration.getBody().statements().get(0); + if (!(statement instanceof ReturnStatement)) { + logger.warn("Statement [" + statement.getClass() + "] is not a return statement."); + return null; + } + ReturnStatement returnStatement = (ReturnStatement) statement; + Expression expression = returnStatement.getExpression(); + if (!(expression instanceof QualifiedName)) { + logger.warn("Statement [" + statement.getClass() + "] is not a qualified statement."); + return null; + } + QualifiedName qualifiedName = (QualifiedName) expression; + String enumName = qualifiedName.getName().toString(); + return (T) Enum.valueOf(enumClass, enumName); + } + + public static String readClass(MethodDeclaration methodDeclaration) { + Object statement = methodDeclaration.getBody().statements().get(0); + if (!(statement instanceof ReturnStatement)) { + logger.warn("Statement [" + statement.getClass() + "] is not a return statement."); + return null; + } + ReturnStatement returnStatement = (ReturnStatement) statement; + Expression expression = returnStatement.getExpression(); + if (!(expression instanceof TypeLiteral)) { + logger.warn("Statement [" + statement.getClass() + "] is not a qualified name."); + return null; + } + TypeLiteral typeLiteral = (TypeLiteral) expression; + Type type = typeLiteral.getType(); + String className = type.toString(); + return matchingImportStatement(expression, className); + } + + public static Map.Entry readClassToEnum(MethodDeclaration methodDeclaration) { + Object statement = methodDeclaration.getBody().statements().get(0); + if (!(statement instanceof ReturnStatement)) { + logger.warn("Statement [" + statement.getClass() + "] is not a return statement."); + return null; + } + ReturnStatement returnStatement = (ReturnStatement) statement; + Expression expression = returnStatement.getExpression(); + if (!(expression instanceof QualifiedName)) { + logger.warn("Statement [" + statement.getClass() + "] is not a qualified name."); + return null; + } + QualifiedName qualifiedName = (QualifiedName) expression; + String className = qualifiedName.getQualifier().toString(); + String enumName = qualifiedName.getName().toString(); + String matchingImportStatement = matchingImportStatement(expression, className); + return new AbstractMap.SimpleEntry<>(matchingImportStatement, enumName); + } + + private static String matchingImportStatement(Expression expression, String className) { + CompilationUnit compilationUnit = (CompilationUnit) expression.getRoot(); + List imports = compilationUnit.imports(); + // Class is in the same package + String matchingImportStatement = compilationUnit.getPackage().getName().toString() + "." + className; + for (Object anImport : imports) { + ImportDeclaration importDeclaration = (ImportDeclaration) anImport; + String importStatement = importDeclaration.getName().toString(); + if (importStatement.endsWith(className)) { + // Class got imported from a different package + matchingImportStatement = importStatement; + } + } + return matchingStatementFromInnerClasses(className, compilationUnit, matchingImportStatement); + } + + private static String matchingStatementFromInnerClasses(String className, CompilationUnit compilationUnit, String matchingImportStatement) { + for (Object type : compilationUnit.types()) { + if (!(type instanceof AbstractTypeDeclaration)) { + continue; + } + AbstractTypeDeclaration typeDeclaration = (AbstractTypeDeclaration) type; + List declarations = typeDeclaration.bodyDeclarations(); + for (Object declaration : declarations) { + AbstractTypeDeclaration childDeclaration = (AbstractTypeDeclaration) declaration; + if (className.equals(childDeclaration.getName().toString())) { + // Class is an inner class (we support 1 level of such nesting for now - we can do recursion in the future + return compilationUnit.getPackage().getName().toString() + "." + typeDeclaration.getName() + "$" + childDeclaration.getName(); + } + } + } + return matchingImportStatement; + } + + public static Collection getTags(EnumConstantSource enumConstant, JavaEnumImpl myEnum, String getterName) { + List> members = enumConstant.getBody().getMembers(); + if (members.isEmpty()) { + return Collections.emptyList(); + } + Collection tags = new TreeSet<>(); + for (MemberSource member : members) { + Object internal = member.getInternal(); + if (!(internal instanceof MethodDeclaration)) { + return null; + } + MethodDeclaration methodDeclaration = (MethodDeclaration) internal; + String methodName = methodDeclaration.getName().getIdentifier(); + if (getterName.equals(methodName)) { + tags.addAll(ParsingUtils.keyValueEntries(myEnum, methodDeclaration, KeyName.class)); + } + } + return tags; + } +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/templates/ADocHelpers.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/templates/ADocHelpers.java new file mode 100644 index 0000000..bbe543f --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/templates/ADocHelpers.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.commons.templates; + +/** + * Helper source class for handlebars. + * + * @author Tadaya Tsuyukubo + */ +public class ADocHelpers { + + public static boolean isDynamic(String input) { + return input.contains("%s"); + } + +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/templates/HandlebarsUtils.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/templates/HandlebarsUtils.java new file mode 100644 index 0000000..1d3bfa5 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/templates/HandlebarsUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.commons.templates; + +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.helper.StringHelpers; +import com.github.jknack.handlebars.io.ClassPathTemplateLoader; +import com.github.jknack.handlebars.io.CompositeTemplateLoader; +import com.github.jknack.handlebars.io.FileTemplateLoader; +import com.github.jknack.handlebars.io.TemplateLoader; + +/** + * Utility for {@link Handlebars}. + * + * @author Tadaya Tsuyukubo + */ +public class HandlebarsUtils { + + public static Handlebars createHandlebars() { + // specify default prefix and empty suffix. The empty suffix forces users to + // specify the full template file name. (e.g. foo.adoc.hbs) + ClassPathTemplateLoader classPathLoader = new ClassPathTemplateLoader(TemplateLoader.DEFAULT_PREFIX, ""); + FileTemplateLoader fileLoader = new FileTemplateLoader(TemplateLoader.DEFAULT_PREFIX, ""); + CompositeTemplateLoader compositeLoader = new CompositeTemplateLoader(classPathLoader, fileLoader); + + Handlebars handlebars = new Handlebars(compositeLoader); + handlebars.registerHelpers(ADocHelpers.class); + StringHelpers.register(handlebars); + + return handlebars; + } +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/AsciidocUtils.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/AsciidocUtils.java new file mode 100644 index 0000000..b133f92 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/AsciidocUtils.java @@ -0,0 +1,145 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.commons.utils; + +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Javadoc; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.TagElement; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.TextElement; +import org.jboss.forge.roaster.model.source.JavaDocSource; + +/** + * Utilities to parse javadoc fragments in various form (String, modelling objects) to asciidoc strings. + */ +public class AsciidocUtils { + + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(AsciidocUtils.class); + + private static final String NEWLINE = System.lineSeparator(); + private static final String LINE_BREAK = " +" + NEWLINE; + private static final String PARAGRAPH_BREAK = NEWLINE + NEWLINE; + + private static final Pattern NEWLINE_PATTERN = Pattern.compile("\\R"); + + public static final String simpleHtmlToAsciidoc(String line, boolean assumeLiOrdered) { + String asciidoc = line + .replaceAll("

", PARAGRAPH_BREAK) + .replaceAll("
", LINE_BREAK) + .replaceAll("", PARAGRAPH_BREAK + "IMPORTANT: ") + .replaceAll("\\h+", PARAGRAPH_BREAK) + .replaceAll("", "*") + .replaceAll("", "_") + .replaceAll("

    ", NEWLINE) + .replaceAll("
      ", NEWLINE) + .replaceAll("", NEWLINE) + .replaceAll("
    1. ", NEWLINE + (assumeLiOrdered ? " 1. " : " - ")); + //strip all other tags (closing tags, unknown tags) + return asciidoc.replaceAll("<[^<>]*>", ""); + } + + public static final String simpleTagletToAsciidoc(String tagletName, List tagletFragments) { + if ("@code".equals(tagletName) || "@value".equals(tagletName)) { + return tagletFragments + .stream() + .map(o -> o.toString().trim()) + .collect(Collectors.joining(" ", "`", "`")); + } + if ("@link".equals(tagletName) || "@linkplain".equals(tagletName)) { + Stream stream = tagletFragments + .stream() + .map(o -> o.toString().trim()); + + if (tagletFragments.size() > 1) + return stream + .skip(1) + .collect(Collectors.joining(" ")); + return stream + .collect(Collectors.joining(" ", "`", "`")); + } + //render the full taglet as an inline code block + return Stream.concat( + Stream.of(tagletName), + tagletFragments.stream().map(o -> o.toString().trim()) + ) + .collect(Collectors.joining(" ", "`{", "}`")); + } + + public static final String javadocToAsciidoc(JavaDocSource javadoc) { + Object internal = javadoc.getInternal(); + if (!(internal instanceof Javadoc)) { + return javadoc.getText(); + } + Javadoc internalJavadoc = (Javadoc) internal; + @SuppressWarnings("unchecked") + List tagList = internalJavadoc.tags(); + StringBuilder text = new StringBuilder(); + + boolean openedOrderedList = false; + for (TagElement tagElement : tagList) { + //only consider the javadoc description + if (tagElement.getTagName() != null) + continue; + for (Object fragment: tagElement.fragments()) { + //ignored: SimpleName + if (fragment instanceof TextElement) { + TextElement textElement = (TextElement) fragment; + String line = textElement.getText(); + //inline taglets will be separate fragments. we only care for embedded HTML subset + if (line.contains("<") && line.contains(">")) { + //only reset the li type when explicitly encountering an ol or ul. + //note ol takes precedence, and this doesn't really work with nested ol/ul combinations. + if (line.contains("
        ")) { + openedOrderedList = false; + } + if (line.contains("
          ")) { + openedOrderedList = true; + } + + text.append(simpleHtmlToAsciidoc(line, openedOrderedList)); + } + else { + //we append a space at the end so that javadoc linebreaks in the middle of a simple text translate to a space + text.append(line).append(' '); + } + } + else if (fragment instanceof TagElement) { + TagElement tagFragment = (TagElement) fragment; + text.append(simpleTagletToAsciidoc(tagFragment.getTagName(), tagFragment.fragments())); + } + else { + LOGGER.debug("dropped fragment during javadoc to asciidoc parsing: %s", tagElement); + } + } + } + //second pass on each line to trim undesirable spaces + String trimmed = NEWLINE_PATTERN + .splitAsStream(text) + .map(line -> line + // we don't want multiple spaces in a row + .replaceAll("\\h\\h+", " ") + //we don't want trailing whitespaces, trim() doesn't work because we do want leading space when relevant + .replaceAll("\\h+$", "")) + .collect(Collectors.joining(System.lineSeparator())); + return trimmed; + } +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/Assert.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/Assert.java new file mode 100644 index 0000000..35d0213 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/Assert.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.commons.utils; + +public final class Assert { + public static void hasText(String text, String description) { + if (!StringUtils.hasText(text)) { + throw new IllegalArgumentException(description); + } + } +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/StringUtils.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/StringUtils.java new file mode 100644 index 0000000..189e55d --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/commons/utils/StringUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.commons.utils; + +public final class StringUtils { + public static boolean hasText(String text) { + return text != null && text.length() > 0; + } + + public static String capitalize(String text) { + return changeFirstCharacterCase(text, true); + } + + private static String changeFirstCharacterCase(String str, boolean capitalize) { + if (str == null || str.length() == 0) { + return str; + } + + char baseChar = str.charAt(0); + char updatedChar; + if (capitalize) { + updatedChar = Character.toUpperCase(baseChar); + } + else { + updatedChar = Character.toLowerCase(baseChar); + } + if (baseChar == updatedChar) { + return str; + } + + char[] chars = str.toCharArray(); + chars[0] = updatedChar; + return new String(chars); + } +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionEntry.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionEntry.java new file mode 100644 index 0000000..ad1e6e2 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionEntry.java @@ -0,0 +1,71 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.conventions; + +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.docs.commons.utils.StringUtils; + +class ObservationConventionEntry implements Comparable { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(ObservationConventionEntry.class); + + private final String className; + + private final Type type; + + private final String contextClassName; + + public ObservationConventionEntry(String className, Type type, String contextClassName) { + this.className = className; + this.type = type; + this.contextClassName = StringUtils.hasText(contextClassName) ? contextClassName : "Unable to resolve"; + } + + public String getClassName() { + return className; + } + + public String getContextClassName() { + return contextClassName; + } + + public Type getType() { + return this.type; + } + + @Override + public int compareTo(ObservationConventionEntry o) { + int compare = this.contextClassName.compareTo(o.contextClassName); + if (compare != 0) { + return compare; + } + compare = this.type.compareTo(o.type); + if (compare != 0) { + return compare; + } + if (this.className != null) { + return this.className.compareTo(o.className); + } + return compare; + } + + public enum Type { + GLOBAL, LOCAL + } + +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionSearchingFileVisitor.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionSearchingFileVisitor.java new file mode 100644 index 0000000..4bb6b14 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionSearchingFileVisitor.java @@ -0,0 +1,106 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.conventions; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.observation.GlobalObservationConvention; +import io.micrometer.observation.ObservationConvention; +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster.model.JavaType; +import org.jboss.forge.roaster.model.JavaUnit; +import org.jboss.forge.roaster.model.impl.JavaClassImpl; +import org.jboss.forge.roaster.model.impl.JavaEnumImpl; + +class ObservationConventionSearchingFileVisitor extends SimpleFileVisitor { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(ObservationConventionSearchingFileVisitor.class); + + private final Pattern pattern; + + private final Collection observationConventionEntries; + + ObservationConventionSearchingFileVisitor(Pattern pattern, Collection observationConventionEntries) { + this.pattern = pattern; + this.observationConventionEntries = observationConventionEntries; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!pattern.matcher(file.toString()).matches()) { + return FileVisitResult.CONTINUE; + } + else if (!file.toString().endsWith(".java")) { + return FileVisitResult.CONTINUE; + } + try (InputStream stream = Files.newInputStream(file)) { + JavaUnit unit = Roaster.parseUnit(stream); + JavaType myClass = unit.getGoverningType(); + if (!(myClass instanceof JavaEnumImpl)) { + if (myClass instanceof JavaClassImpl) { + Pattern classPattern = Pattern.compile("^.*ObservationConvention<(.*)>$"); + JavaClassImpl holder = (JavaClassImpl) myClass; + for (String anInterface : holder.getInterfaces()) { + if (isGlobalObservationConvention(anInterface)) { + this.observationConventionEntries.add(new ObservationConventionEntry(unit.getGoverningType().getCanonicalName(), ObservationConventionEntry.Type.GLOBAL, contextClassName(classPattern, anInterface))); + } + else if (isLocalObservationConvention(anInterface)) { + this.observationConventionEntries.add(new ObservationConventionEntry(unit.getGoverningType().getCanonicalName(), ObservationConventionEntry.Type.LOCAL, contextClassName(classPattern, anInterface))); + } + } + return FileVisitResult.CONTINUE; + } + return FileVisitResult.CONTINUE; + } + return FileVisitResult.CONTINUE; + } + catch (Exception e) { + throw new IOException("Failed to parse file [" + file + "] due to an error", e); + } + } + + private String contextClassName(Pattern classPattern, String anInterface) { + Matcher matcher = classPattern.matcher(anInterface); + if (matcher.matches()) { + return matcher.group(1); + } + if (!anInterface.contains("<") && !anInterface.contains(">")) { + return "n/a"; + } + return ""; + } + + private boolean isLocalObservationConvention(String interf) { + return interf.contains(ObservationConvention.class.getSimpleName()) || interf.contains(ObservationConvention.class.getCanonicalName()); + } + + private boolean isGlobalObservationConvention(String interf) { + return interf.contains(GlobalObservationConvention.class.getSimpleName()) || interf.contains(GlobalObservationConvention.class.getCanonicalName()); + } + +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionsDocGenerator.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionsDocGenerator.java new file mode 100644 index 0000000..40a12ae --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/conventions/ObservationConventionsDocGenerator.java @@ -0,0 +1,84 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.conventions; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Template; +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.docs.conventions.ObservationConventionEntry.Type; +import io.micrometer.docs.commons.templates.HandlebarsUtils; + +public class ObservationConventionsDocGenerator { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(ObservationConventionsDocGenerator.class); + + private final File projectRoot; + + private final Pattern inclusionPattern; + + private final File outputDir; + + public ObservationConventionsDocGenerator(File projectRoot, Pattern inclusionPattern, File outputDir) { + this.projectRoot = projectRoot; + this.inclusionPattern = inclusionPattern; + this.outputDir = outputDir; + } + + public void generate() { + Path path = this.projectRoot.toPath(); + logger.debug("Path is [" + this.projectRoot.getAbsolutePath() + "]. Inclusion pattern is [" + this.inclusionPattern + "]"); + TreeSet observationConventionEntries = new TreeSet<>(); + FileVisitor fv = new ObservationConventionSearchingFileVisitor(this.inclusionPattern, observationConventionEntries); + try { + Files.walkFileTree(path, fv); + printObservationConventionsAdoc(observationConventionEntries); + } + catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + private void printObservationConventionsAdoc(TreeSet entries) throws IOException { + List globals = entries.stream().filter(e -> e.getType() == Type.GLOBAL).collect(Collectors.toList()); + List locals = entries.stream().filter(e -> e.getType() == Type.LOCAL).collect(Collectors.toList()); + + String location = "templates/conventions.adoc.hbs"; + Handlebars handlebars = HandlebarsUtils.createHandlebars(); + Template template = handlebars.compile(location); + + Map map = new HashMap<>(); + map.put("globals", globals); + map.put("locals", locals); + String result = template.apply(map); + + Path output = new File(this.outputDir, "_conventions.adoc").toPath(); + Files.write(output, result.getBytes()); + } + +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricEntry.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricEntry.java new file mode 100644 index 0000000..c12f994 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricEntry.java @@ -0,0 +1,180 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Meter.Type; +import io.micrometer.docs.commons.KeyValueEntry; +import io.micrometer.docs.commons.utils.Assert; +import io.micrometer.docs.commons.utils.StringUtils; + +class MetricEntry implements Comparable { + + final String name; + + final String conventionClass; + + final String nameFromConventionClass; + + final String enclosingClass; + + final String enumName; + + final String description; + + final String prefix; + + final String baseUnit; + + final Meter.Type type; + + final Collection lowCardinalityKeyNames; + + final Collection highCardinalityKeyNames; + + final Map.Entry overridesDefaultMetricFrom; + + final Collection events; + + MetricEntry(String name, String conventionClass, String nameFromConventionClass, String enclosingClass, String enumName, String description, String prefix, String baseUnit, Meter.Type meterType, Collection lowCardinalityKeyNames, Collection highCardinalityKeyNames, Map.Entry overridesDefaultMetricFrom, Collection events) { + Assert.hasText(description, "Observation / Meter javadoc description must not be empty. Check <" + enclosingClass + "#" + enumName + ">"); + this.name = name; + this.conventionClass = conventionClass; + this.nameFromConventionClass = nameFromConventionClass; + this.enclosingClass = enclosingClass; + this.enumName = enumName; + this.description = description; + this.prefix = prefix; + this.baseUnit = StringUtils.hasText(baseUnit) ? baseUnit : meterType == Meter.Type.TIMER ? "seconds" : ""; + this.type = meterType; + this.lowCardinalityKeyNames = lowCardinalityKeyNames; + this.highCardinalityKeyNames = highCardinalityKeyNames; + this.overridesDefaultMetricFrom = overridesDefaultMetricFrom; + if (StringUtils.hasText(this.name) && this.conventionClass != null) { + throw new IllegalStateException("You can't declare both [getName()] and [getDefaultConvention()] methods at the same time, you have to chose only one. Problem occurred in [" + this.enclosingClass + "] class"); + } + else if (this.name == null && this.conventionClass == null) { + throw new IllegalStateException("You have to set either [getName()] or [getDefaultConvention()] methods. In case of [" + this.enclosingClass + "] you haven't defined any"); + } + this.events = events; + } + + static void assertThatProperlyPrefixed(Collection entries) { + List>> collect = entries.stream().map(MetricEntry::notProperlyPrefixedTags).filter(Objects::nonNull).collect(Collectors.toList()); + if (collect.isEmpty()) { + return; + } + throw new IllegalStateException("The following documented objects do not have properly prefixed tag keys according to their prefix() method. Please align the tag keys.\n\n" + collect.stream() + .map(e -> "\tName <" + e.getKey().enumName + "> in class <" + e.getKey().enclosingClass + "> has the following prefix <" + e.getKey().prefix + "> and following invalid tag keys " + e.getValue()) + .collect(Collectors.joining("\n")) + "\n\n"); + } + + Map.Entry> notProperlyPrefixedTags() { + if (!StringUtils.hasText(this.prefix)) { + return null; + } + List allTags = new ArrayList<>(this.lowCardinalityKeyNames); + allTags.addAll(this.highCardinalityKeyNames); + List collect = allTags.stream().map(KeyValueEntry::getName).filter(eName -> !eName.startsWith(this.prefix)).collect(Collectors.toList()); + if (collect.isEmpty()) { + return null; + } + return new AbstractMap.SimpleEntry<>(this, collect); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MetricEntry that = (MetricEntry) o; + return Objects.equals(name, that.name) && Objects.equals(conventionClass, that.conventionClass) && Objects.equals(nameFromConventionClass, that.nameFromConventionClass) && Objects.equals(enclosingClass, that.enclosingClass) && Objects.equals(enumName, that.enumName) && Objects.equals(description, that.description) && Objects.equals(prefix, that.prefix) && Objects.equals(baseUnit, that.baseUnit) && type == that.type && Objects.equals(lowCardinalityKeyNames, that.lowCardinalityKeyNames) && Objects.equals(highCardinalityKeyNames, that.highCardinalityKeyNames) && Objects.equals(overridesDefaultMetricFrom, that.overridesDefaultMetricFrom); + } + + @Override + public int hashCode() { + return Objects.hash(name, conventionClass, nameFromConventionClass, enclosingClass, enumName, description, prefix, baseUnit, type, lowCardinalityKeyNames, highCardinalityKeyNames, overridesDefaultMetricFrom); + } + + @Override + public int compareTo(MetricEntry o) { + return enumName.compareTo(o.enumName); + } + + private String name() { + if (StringUtils.hasText(this.name)) { + return "`" + this.name + "`"; + } + else if (StringUtils.hasText(this.nameFromConventionClass)) { + return "`" + this.nameFromConventionClass + "` (defined by convention class `" + this.conventionClass + "`)"; + } + return "Unable to resolve the name - please check the convention class `" + this.conventionClass + "` for more details"; + } + + public String getDescription() { + return this.description; + } + + public String getMetricName() { + // TODO: convert to handlebar helper + return name(); + } + + public String getName() { + return this.name; + } + + public String getEnumName() { + return this.enumName; + } + + public Type getType() { + return this.type; + } + + public String getBaseUnit() { + return this.baseUnit; + } + + public String getEnclosingClass() { + return this.enclosingClass; + } + + public String getPrefix() { + return this.prefix; + } + + + public Collection getLowCardinalityKeyNames() { + return this.lowCardinalityKeyNames; + } + + public Collection getHighCardinalityKeyNames() { + return this.highCardinalityKeyNames; + } + + public Collection getEvents() { + return this.events; + } +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricSearchingFileVisitor.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricSearchingFileVisitor.java new file mode 100644 index 0000000..c76ac48 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricSearchingFileVisitor.java @@ -0,0 +1,216 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.docs.MeterDocumentation; +import io.micrometer.docs.commons.KeyValueEntry; +import io.micrometer.docs.commons.ParsingUtils; +import io.micrometer.docs.commons.utils.AsciidocUtils; +import io.micrometer.observation.Observation; +import io.micrometer.observation.docs.ObservationDocumentation; +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.MethodDeclaration; +import org.jboss.forge.roaster.model.JavaType; +import org.jboss.forge.roaster.model.JavaUnit; +import org.jboss.forge.roaster.model.impl.JavaEnumImpl; +import org.jboss.forge.roaster.model.source.EnumConstantSource; +import org.jboss.forge.roaster.model.source.MemberSource; + +class MetricSearchingFileVisitor extends SimpleFileVisitor { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(MetricSearchingFileVisitor.class); + + private final Pattern pattern; + + private final Collection sampleEntries; + + MetricSearchingFileVisitor(Pattern pattern, Collection sampleEntries) { + this.pattern = pattern; + this.sampleEntries = sampleEntries; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!pattern.matcher(file.toString()).matches()) { + return FileVisitResult.CONTINUE; + } + else if (!file.toString().endsWith(".java")) { + return FileVisitResult.CONTINUE; + } + try (InputStream stream = Files.newInputStream(file)) { + JavaUnit unit = Roaster.parseUnit(stream); + JavaType myClass = unit.getGoverningType(); + if (!(myClass instanceof JavaEnumImpl)) { + return FileVisitResult.CONTINUE; + } + JavaEnumImpl myEnum = (JavaEnumImpl) myClass; + if (Stream.of(MeterDocumentation.class.getCanonicalName(), ObservationDocumentation.class.getCanonicalName()).noneMatch(ds -> myEnum.getInterfaces().contains(ds))) { + return FileVisitResult.CONTINUE; + } + logger.debug("Checking [" + myEnum.getName() + "]"); + if (myEnum.getEnumConstants().size() == 0) { + return FileVisitResult.CONTINUE; + } + for (EnumConstantSource enumConstant : myEnum.getEnumConstants()) { + MetricEntry entry = parseMetric(file, enumConstant, myEnum); + if (entry != null) { + sampleEntries.add(entry); + logger.debug( + "Found [" + entry.lowCardinalityKeyNames.size() + "] low cardinality tags and [" + entry.highCardinalityKeyNames.size() + "] high cardinality tags"); + } + if (entry != null) { + if (entry.overridesDefaultMetricFrom != null && entry.lowCardinalityKeyNames.isEmpty()) { + addTagsFromOverride(file, entry); + } + sampleEntries.add(entry); + logger.debug( + "Found [" + entry.lowCardinalityKeyNames.size() + "]"); + } + } + return FileVisitResult.CONTINUE; + } + catch (Exception e) { + throw new IOException("Failed to parse file [" + file + "] due to an error", e); + } + } + + // if entry has overridesDefaultSpanFrom - read tags from that thing + // if entry has overridesDefaultSpanFrom AND getKeyNames() - we pick only the latter + // if entry has overridesDefaultSpanFrom AND getAdditionalKeyNames() - we pick both + private void addTagsFromOverride(Path file, MetricEntry entry) throws IOException { + Map.Entry overrideDefaults = entry.overridesDefaultMetricFrom; + logger.debug("Reading additional meta data from [" + overrideDefaults + "]"); + String className = overrideDefaults.getKey(); + File parent = file.getParent().toFile(); + while (!parent.getAbsolutePath().endsWith(File.separator + "java")) { + parent = parent.getParentFile(); + } + String filePath = new File(parent, className.replace(".", File.separator) + ".java").getAbsolutePath(); + try (InputStream streamForOverride = Files.newInputStream(new File(filePath).toPath())) { + JavaUnit parsedForOverride = Roaster.parseUnit(streamForOverride); + JavaType overrideClass = parsedForOverride.getGoverningType(); + if (!(overrideClass instanceof JavaEnumImpl)) { + return; + } + JavaEnumImpl myEnum = (JavaEnumImpl) overrideClass; + if (!myEnum.getInterfaces().contains(ObservationDocumentation.class.getCanonicalName())) { + return; + } + logger.debug("Checking [" + myEnum.getName() + "]"); + if (myEnum.getEnumConstants().size() == 0) { + return; + } + for (EnumConstantSource enumConstant : myEnum.getEnumConstants()) { + if (!enumConstant.getName().equals(overrideDefaults.getValue())) { + continue; + } + Collection low = ParsingUtils.getTags(enumConstant, myEnum, "getLowCardinalityKeyNames"); + if (low != null) { + entry.lowCardinalityKeyNames.addAll(low); + } + } + } + } + + private MetricEntry parseMetric(Path file, EnumConstantSource enumConstant, JavaEnumImpl myEnum) { + List> members = enumConstant.getBody().getMembers(); + if (members.isEmpty()) { + return null; + } + String name = ""; + String description = AsciidocUtils.javadocToAsciidoc(enumConstant.getJavaDoc()); + String prefix = ""; + String baseUnit = ""; + Meter.Type type = Meter.Type.TIMER; + Collection lowCardinalityTags = new TreeSet<>(); + Collection highCardinalityTags = new TreeSet<>(); + Map.Entry overridesDefaultMetricFrom = null; + String conventionClass = null; + String nameFromConventionClass = null; + Collection events = new ArrayList<>(); + for (MemberSource member : members) { + Object internal = member.getInternal(); + if (!(internal instanceof MethodDeclaration)) { + return null; + } + MethodDeclaration methodDeclaration = (MethodDeclaration) internal; + String methodName = methodDeclaration.getName().getIdentifier(); + if ("getName".equals(methodName)) { + name = ParsingUtils.readStringReturnValue(methodDeclaration); + } + else if ("getKeyNames".equals(methodName)) { + lowCardinalityTags.addAll(ParsingUtils.keyValueEntries(myEnum, methodDeclaration, KeyName.class)); + } + else if ("getDefaultConvention".equals(methodName)) { + conventionClass = ParsingUtils.readClass(methodDeclaration); + nameFromConventionClass = ParsingUtils.tryToReadStringReturnValue(file, conventionClass); + } + else if ("getLowCardinalityKeyNames".equals(methodName) || "asString".equals(methodName)) { + lowCardinalityTags.addAll(ParsingUtils.keyValueEntries(myEnum, methodDeclaration, KeyName.class)); + } + else if ("getHighCardinalityKeyNames".equals(methodName)) { + highCardinalityTags.addAll(ParsingUtils.keyValueEntries(myEnum, methodDeclaration, KeyName.class)); + } + else if ("getPrefix".equals(methodName)) { + prefix = ParsingUtils.readStringReturnValue(methodDeclaration); + } + else if ("getBaseUnit".equals(methodName)) { + baseUnit = ParsingUtils.readStringReturnValue(methodDeclaration); + } + else if ("getType".equals(methodName)) { + type = ParsingUtils.enumFromReturnMethodDeclaration(methodDeclaration, Meter.Type.class); + } + else if ("getDescription".equals(methodName)) { + description = ParsingUtils.readStringReturnValue(methodDeclaration); + } + else if ("overridesDefaultMetricFrom".equals(methodName)) { + overridesDefaultMetricFrom = ParsingUtils.readClassToEnum(methodDeclaration); + } + else if ("getEvents".equals(methodName)) { + Collection entries = ParsingUtils.keyValueEntries(myEnum, methodDeclaration, Observation.Event.class, "getName"); + Collection counters = entries.stream().map(k -> new MetricEntry(k.getName(), null, null, myEnum.getCanonicalName(), enumConstant.getName(), k.getDescription(), null, null, Meter.Type.COUNTER, new TreeSet<>(), new TreeSet<>(), null, new TreeSet<>())).collect(Collectors.toList()); + events.addAll(counters); + } + } + final String newName = name; + events = events.stream().map(m -> new MetricEntry(newName + "." + m.name, m.conventionClass, m.nameFromConventionClass, m.enclosingClass, m.enumName, m.description, m.prefix, m.baseUnit, m.type, m.lowCardinalityKeyNames, m.highCardinalityKeyNames, m.overridesDefaultMetricFrom, m.events)).collect(Collectors.toList()); + return new MetricEntry(name, conventionClass, nameFromConventionClass, myEnum.getCanonicalName(), enumConstant.getName(), description, prefix, baseUnit, type, lowCardinalityTags, + highCardinalityTags, overridesDefaultMetricFrom, events); + } + +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricsDocGenerator.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricsDocGenerator.java new file mode 100644 index 0000000..6297221 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/metrics/MetricsDocGenerator.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeSet; +import java.util.regex.Pattern; + +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Template; +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.docs.commons.templates.HandlebarsUtils; + +// TODO: Assert on prefixes +public class MetricsDocGenerator { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(MetricsDocGenerator.class); + + private final File projectRoot; + + private final Pattern inclusionPattern; + + private final File outputDir; + + public MetricsDocGenerator(File projectRoot, Pattern inclusionPattern, File outputDir) { + this.projectRoot = projectRoot; + this.inclusionPattern = inclusionPattern; + this.outputDir = outputDir; + } + + public void generate() { + Path path = this.projectRoot.toPath(); + logger.debug("Path is [" + this.projectRoot.getAbsolutePath() + "]. Inclusion pattern is [" + this.inclusionPattern + "]"); + Collection entries = new TreeSet<>(); + FileVisitor fv = new MetricSearchingFileVisitor(this.inclusionPattern, entries); + try { + Files.walkFileTree(path, fv); + MetricEntry.assertThatProperlyPrefixed(entries); + + this.outputDir.mkdirs(); + printMetricsAdoc(entries); + } + catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + private void printMetricsAdoc(Collection entries) throws IOException { + String location = "templates/metrics.adoc.hbs"; + Handlebars handlebars = HandlebarsUtils.createHandlebars(); + Template template = handlebars.compile(location); + + Map map = new HashMap<>(); + map.put("entries", entries); + String result = template.apply(map); + + Path output = new File(this.outputDir, "_metrics.adoc").toPath(); + Files.write(output, result.getBytes()); + } + +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpanEntry.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpanEntry.java new file mode 100644 index 0000000..9cde9c4 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpanEntry.java @@ -0,0 +1,169 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import io.micrometer.docs.commons.KeyValueEntry; +import io.micrometer.docs.commons.utils.Assert; +import io.micrometer.docs.commons.utils.StringUtils; + +class SpanEntry implements Comparable { + + final String name; + + final String conventionClass; + + final String nameFromConventionClass; + + final String enclosingClass; + + final String enumName; + + final String description; + + final String prefix; + + final Collection tagKeys; + + final Collection additionalKeyNames; + + final Collection events; + + final Map.Entry overridesDefaultSpanFrom; + + SpanEntry(String name, String conventionClass, String nameFromConventionClass, String enclosingClass, String enumName, String description, String prefix, + Collection tagKeys, Collection additionalKeyNames, Collection events, Map.Entry overridesDefaultSpanFrom) { + Assert.hasText(description, "Span javadoc description must not be empty"); + this.conventionClass = conventionClass; + this.nameFromConventionClass = nameFromConventionClass; + this.name = name; + this.enclosingClass = enclosingClass; + this.enumName = enumName; + this.description = description; + this.prefix = prefix; + this.tagKeys = tagKeys; + this.additionalKeyNames = additionalKeyNames; + this.events = events; + this.overridesDefaultSpanFrom = overridesDefaultSpanFrom; + if (StringUtils.hasText(this.name) && this.conventionClass != null) { + throw new IllegalStateException("You can't declare both [getName()] and [getDefaultConvention()] methods at the same time, you have to chose only one. Problem occurred in [" + this.enclosingClass + "] class"); + } else if (this.name == null && this.conventionClass == null) { + throw new IllegalStateException("You have to set either [getName()] or [getDefaultConvention()] methods. In case of [" + this.enclosingClass + "] you haven't defined any"); + } + } + + static void assertThatProperlyPrefixed(Collection entries) { + List>> collect = entries.stream().map(SpanEntry::notProperlyPrefixedTags).filter(Objects::nonNull).collect(Collectors.toList()); + if (collect.isEmpty()) { + return; + } + throw new IllegalStateException("The following documented objects do not have properly prefixed tag keys according to their prefix() method. Please align the tag keys.\n\n" + collect.stream().map(e -> "\tName <" + e.getKey().enumName + "> in class <" + e.getKey().enclosingClass + "> has the following prefix <" + e.getKey().prefix + "> and following invalid tag keys " + e.getValue()).collect(Collectors.joining("\n")) + "\n\n"); + } + + Map.Entry> notProperlyPrefixedTags() { + if (!StringUtils.hasText(this.prefix)) { + return null; + } + List allTags = new ArrayList<>(this.tagKeys); + allTags.addAll(this.additionalKeyNames); + List collect = allTags.stream().map(KeyValueEntry::getName).filter(eName -> !eName.startsWith(this.prefix)).collect(Collectors.toList()); + if (collect.isEmpty()) { + return null; + } + return new AbstractMap.SimpleEntry<>(this, collect); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SpanEntry spanEntry = (SpanEntry) o; + return Objects.equals(name, spanEntry.name) && Objects.equals(conventionClass, spanEntry.conventionClass) && Objects.equals(nameFromConventionClass, spanEntry.nameFromConventionClass) && Objects.equals(enclosingClass, spanEntry.enclosingClass) && Objects.equals(enumName, spanEntry.enumName) && Objects.equals(description, spanEntry.description) && Objects.equals(prefix, spanEntry.prefix) && Objects.equals(tagKeys, spanEntry.tagKeys) && Objects.equals(additionalKeyNames, spanEntry.additionalKeyNames) && Objects.equals(events, spanEntry.events) && Objects.equals(overridesDefaultSpanFrom, spanEntry.overridesDefaultSpanFrom); + } + + @Override + public int hashCode() { + return Objects.hash(name, conventionClass, nameFromConventionClass, enclosingClass, enumName, description, prefix, tagKeys, additionalKeyNames, events, overridesDefaultSpanFrom); + } + + @Override + public int compareTo(SpanEntry o) { + return enumName.compareTo(o.enumName); + } + + private String spanName() { + String name = Arrays.stream(enumName.replace("_", " ").split(" ")).map(s -> StringUtils.capitalize(s.toLowerCase(Locale.ROOT))).collect(Collectors.joining(" ")); + if (!name.toLowerCase(Locale.ROOT).endsWith("span")) { + return name + " Span"; + } + return name; + } + + private String name() { + if (StringUtils.hasText(this.name)) { + return "`" + this.name + "`"; + } else if (StringUtils.hasText(this.nameFromConventionClass)) { + return "`" + this.nameFromConventionClass + "` (defined by convention class `" + this.conventionClass + "`)"; + } + return "Unable to resolve the name - please check the convention class `" + this.conventionClass + "` for more details"; + } + + public String getSpanTitle() { + // TODO: convert to handlebar helper + return spanName(); + } + public String getDisplayName() { + return name(); + } + + public String getName() { + return this.name; + } + + public String getEnumName() { + return this.enumName; + } + + public String getDescription() { + return this.description; + } + + public String getEnclosingClass() { + return this.enclosingClass; + } + + public String getPrefix() { + return this.prefix; + } + + public Collection getTagKeys() { + return this.tagKeys; + } + + public Collection getEvents() { + return this.events; + } +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpanSearchingFileVisitor.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpanSearchingFileVisitor.java new file mode 100644 index 0000000..b613f65 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpanSearchingFileVisitor.java @@ -0,0 +1,230 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeSet; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.docs.commons.KeyValueEntry; +import io.micrometer.docs.commons.ParsingUtils; +import io.micrometer.docs.commons.utils.AsciidocUtils; +import io.micrometer.observation.Observation; +import io.micrometer.observation.docs.ObservationDocumentation; +import io.micrometer.tracing.docs.EventValue; +import io.micrometer.tracing.docs.SpanDocumentation; +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.MethodDeclaration; +import org.jboss.forge.roaster.model.JavaType; +import org.jboss.forge.roaster.model.JavaUnit; +import org.jboss.forge.roaster.model.impl.JavaEnumImpl; +import org.jboss.forge.roaster.model.source.EnumConstantSource; +import org.jboss.forge.roaster.model.source.MemberSource; + +class SpanSearchingFileVisitor extends SimpleFileVisitor { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(SpanSearchingFileVisitor.class); + + private final Pattern pattern; + + private final Collection spanEntries; + + SpanSearchingFileVisitor(Pattern pattern, Collection spanEntries) { + this.pattern = pattern; + this.spanEntries = spanEntries; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!pattern.matcher(file.toString()).matches()) { + return FileVisitResult.CONTINUE; + } + else if (!file.toString().endsWith(".java")) { + return FileVisitResult.CONTINUE; + } + try (InputStream stream = Files.newInputStream(file)) { + JavaUnit unit = Roaster.parseUnit(stream); + JavaType myClass = unit.getGoverningType(); + if (!(myClass instanceof JavaEnumImpl)) { + return FileVisitResult.CONTINUE; + } + JavaEnumImpl myEnum = (JavaEnumImpl) myClass; + if (Stream.of(SpanDocumentation.class.getName(), ObservationDocumentation.class.getCanonicalName()).noneMatch(ds -> myEnum.getInterfaces().contains(ds))) { + return FileVisitResult.CONTINUE; + } + logger.debug("Checking [" + myEnum.getName() + "]"); + if (myEnum.getEnumConstants().size() == 0) { + return FileVisitResult.CONTINUE; + } + for (EnumConstantSource enumConstant : myEnum.getEnumConstants()) { + SpanEntry entry = parseSpan(file, enumConstant, myEnum); + if (entry != null) { + if (entry.overridesDefaultSpanFrom != null && entry.tagKeys.isEmpty()) { + addTagsFromOverride(file, entry); + } + if (!entry.additionalKeyNames.isEmpty()) { + entry.tagKeys.addAll(entry.additionalKeyNames); + } + spanEntries.add(entry); + logger.debug( + "Found [" + entry.tagKeys.size() + "] tags and [" + entry.events.size() + "] events"); + } + } + return FileVisitResult.CONTINUE; + } + catch (Exception e) { + throw new IOException("Failed to parse file [" + file + "] due to an error", e); + } + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + List overridingNames = spanEntries.stream().filter(s -> s.overridesDefaultSpanFrom != null) + .map(spanEntry -> spanEntry.overridesDefaultSpanFrom.getKey()) + .collect(Collectors.toList()); + List spansToRemove = spanEntries.stream() + .filter(spanEntry -> overridingNames.stream().anyMatch(name -> spanEntry.enclosingClass.toLowerCase(Locale.ROOT).contains(name.toLowerCase(Locale.ROOT)))) + .collect(Collectors.toList()); + if (!spansToRemove.isEmpty()) { + logger.debug("Will remove the span entries <" + spansToRemove.stream().map(s -> s.name).collect(Collectors.joining(",")) + "> because they are overridden"); + } + spanEntries.removeAll(spansToRemove); + return FileVisitResult.CONTINUE; + } + + // if entry has overridesDefaultSpanFrom - read tags from that thing + // if entry has overridesDefaultSpanFrom AND getKeyNames() - we pick only the latter + // if entry has overridesDefaultSpanFrom AND getAdditionalKeyNames() - we pick both + private void addTagsFromOverride(Path file, SpanEntry entry) throws IOException { + Map.Entry overridesDefaultSpanFrom = entry.overridesDefaultSpanFrom; + logger.debug("Reading additional meta data from [" + overridesDefaultSpanFrom + "]"); + String className = overridesDefaultSpanFrom.getKey(); + File parent = file.getParent().toFile(); + while (!parent.getAbsolutePath().endsWith(File.separator + "java")) { + parent = parent.getParentFile(); + } + String filePath = new File(parent, className.replace(".", File.separator) + ".java").getAbsolutePath(); + try (InputStream streamForOverride = Files.newInputStream(new File(filePath).toPath())) { + JavaUnit parsedForOverride = Roaster.parseUnit(streamForOverride); + JavaType overrideClass = parsedForOverride.getGoverningType(); + if (!(overrideClass instanceof JavaEnumImpl)) { + return; + } + JavaEnumImpl myEnum = (JavaEnumImpl) overrideClass; + if (!myEnum.getInterfaces().contains(ObservationDocumentation.class.getCanonicalName())) { + return; + } + logger.debug("Checking [" + myEnum.getName() + "]"); + if (myEnum.getEnumConstants().size() == 0) { + return; + } + for (EnumConstantSource enumConstant : myEnum.getEnumConstants()) { + if (!enumConstant.getName().equals(overridesDefaultSpanFrom.getValue())) { + continue; + } + Collection low = ParsingUtils.getTags(enumConstant, myEnum, "getLowCardinalityKeyNames"); + Collection high = ParsingUtils.getTags(enumConstant, myEnum, "getHighCardinalityKeyNames"); + if (low != null) { + entry.tagKeys.addAll(low); + } + if (high != null) { + entry.tagKeys.addAll(high); + } + } + } + } + + private SpanEntry parseSpan(Path file, EnumConstantSource enumConstant, JavaEnumImpl myEnum) { + List> members = enumConstant.getBody().getMembers(); + if (members.isEmpty()) { + return null; + } + String name = ""; + String contextualName = null; + String description = AsciidocUtils.javadocToAsciidoc(enumConstant.getJavaDoc()); + String prefix = ""; + Collection tags = new TreeSet<>(); + Collection additionalKeyNames = new TreeSet<>(); + Collection events = new TreeSet<>(); + Map.Entry overridesDefaultSpanFrom = null; + String conventionClass = null; + String nameFromConventionClass = null; + for (MemberSource member : members) { + Object internal = member.getInternal(); + if (!(internal instanceof MethodDeclaration)) { + return null; + } + MethodDeclaration methodDeclaration = (MethodDeclaration) internal; + String methodName = methodDeclaration.getName().getIdentifier(); + if ("getName".equals(methodName)) { + name = ParsingUtils.readStringReturnValue(methodDeclaration); + } + else if ("getDefaultConvention".equals(methodName)) { + conventionClass = ParsingUtils.readClass(methodDeclaration); + nameFromConventionClass = ParsingUtils.tryToReadStringReturnValue(file, conventionClass); + } + else if ("getContextualName".equals(methodName)) { + contextualName = ParsingUtils.readStringReturnValue(methodDeclaration); + } + else if ("getKeyNames".equals(methodName)) { + tags.addAll(ParsingUtils.keyValueEntries(myEnum, methodDeclaration, KeyName.class)); + } + else if ("getLowCardinalityKeyNames".equals(methodName)) { + tags.addAll(ParsingUtils.keyValueEntries(myEnum, methodDeclaration, KeyName.class)); + } + else if ("getHighCardinalityKeyNames".equals(methodName)) { + tags.addAll(ParsingUtils.keyValueEntries(myEnum, methodDeclaration, KeyName.class)); + } + else if ("getAdditionalKeyNames".equals(methodName)) { + additionalKeyNames.addAll(ParsingUtils.keyValueEntries(myEnum, methodDeclaration, KeyName.class)); + } + else if ("getEvents".equals(methodName)) { + if (methodDeclaration.getReturnType2().toString().contains("EventValue")) { + events.addAll(ParsingUtils.keyValueEntries(myEnum, methodDeclaration, EventValue.class)); + } + else { + events.addAll(ParsingUtils.keyValueEntries(myEnum, methodDeclaration, Observation.Event.class, "getContextualName")); + } + } + else if ("getPrefix".equals(methodName)) { + prefix = ParsingUtils.readStringReturnValue(methodDeclaration); + } + else if ("overridesDefaultSpanFrom".equals(methodName)) { + overridesDefaultSpanFrom = ParsingUtils.readClassToEnum(methodDeclaration); + } + } + return new SpanEntry(contextualName != null ? contextualName : name, conventionClass, nameFromConventionClass, myEnum.getCanonicalName(), enumConstant.getName(), description, prefix, tags, + additionalKeyNames, events, overridesDefaultSpanFrom); + } + +} diff --git a/micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpansDocGenerator.java b/micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpansDocGenerator.java new file mode 100644 index 0000000..0afeb79 --- /dev/null +++ b/micrometer-docs-generator/src/main/java/io/micrometer/docs/spans/SpansDocGenerator.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeSet; +import java.util.regex.Pattern; + +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Template; +import io.micrometer.common.util.internal.logging.InternalLogger; +import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.docs.commons.templates.HandlebarsUtils; + +public class SpansDocGenerator { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(SpansDocGenerator.class); + + private final File projectRoot; + + private final Pattern inclusionPattern; + + private final File outputDir; + + public SpansDocGenerator(File projectRoot, Pattern inclusionPattern, File outputDir) { + this.projectRoot = projectRoot; + this.inclusionPattern = inclusionPattern; + this.outputDir = outputDir; + } + + public void generate() { + Path path = this.projectRoot.toPath(); + logger.debug("Path is [" + this.projectRoot.getAbsolutePath() + "]. Inclusion pattern is [" + this.inclusionPattern + "]"); + Collection spanEntries = new TreeSet<>(); + FileVisitor fv = new SpanSearchingFileVisitor(this.inclusionPattern, spanEntries); + try { + Files.walkFileTree(path, fv); + SpanEntry.assertThatProperlyPrefixed(spanEntries); + + printSpansAdoc(spanEntries); + } + catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + private void printSpansAdoc(Collection spanEntries) throws IOException { + String location = "templates/spans.adoc.hbs"; + Handlebars handlebars = HandlebarsUtils.createHandlebars(); + Template template = handlebars.compile(location); + + Map map = new HashMap<>(); + map.put("entries", spanEntries); + String result = template.apply(map); + + Path output = new File(this.outputDir, "_spans.adoc").toPath(); + Files.write(output, result.getBytes()); + } + +} diff --git a/micrometer-docs-generator/src/main/resources/logback.xml b/micrometer-docs-generator/src/main/resources/logback.xml new file mode 100644 index 0000000..1889899 --- /dev/null +++ b/micrometer-docs-generator/src/main/resources/logback.xml @@ -0,0 +1,26 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/micrometer-docs-generator/src/main/resources/templates/conventions.adoc.hbs b/micrometer-docs-generator/src/main/resources/templates/conventions.adoc.hbs new file mode 100644 index 0000000..9161714 --- /dev/null +++ b/micrometer-docs-generator/src/main/resources/templates/conventions.adoc.hbs @@ -0,0 +1,31 @@ +{{! + This is a handlebars template file for conventions documentation. +~}} +[[observability-conventions]] +=== Observability - Conventions + +Below you can find a list of all `GlobalObservabilityConventions` and `ObservabilityConventions` declared by this project. + +{{~#each globals}} +{{#if @first}} +.GlobalObservationConvention implementations +|=== +|GlobalObservationConvention Class Name | Applicable ObservationContext Class Name +{{/if~}} +|`{{this.className}}`|`{{this.contextClassName}}` +{{#if @last~}} +|=== +{{~/if}} +{{~/each}} + +{{#each locals~}} +{{#if @first~}} +.ObservationConvention implementations +|=== +|ObservationConvention Class Name | Applicable ObservationContext Class Name +{{/if~}} +|`{{this.className}}`|`{{this.contextClassName}}` +{{#if @last~}} +|=== +{{~/if}} +{{~/each}} diff --git a/micrometer-docs-generator/src/main/resources/templates/metrics.adoc.hbs b/micrometer-docs-generator/src/main/resources/templates/metrics.adoc.hbs new file mode 100644 index 0000000..51ca81b --- /dev/null +++ b/micrometer-docs-generator/src/main/resources/templates/metrics.adoc.hbs @@ -0,0 +1,68 @@ +{{! + This is a handlebars template file for metrics documentation. +~}} +[[observability-metrics]] +=== Observability - Metrics + +Below you can find a list of all metrics declared by this project. + +{{#each entries~}} +[[observability-metrics-{{slugify (lower (replace enumName "_" " "))}}]] +==== {{capitalize (lower (replace enumName "_" " "))}} + +____ +{{{description}}} +____ + +**Metric name** {{{metricName}}}{{#if (isDynamic name)}} - since it contains `%s`, the name is dynamic and will be resolved at runtime. +{{~else}}.{{/if}} **Type** `{{replace (lower type) "_" " "}}`{{#if baseUnit}} and **base unit** `{{lower baseUnit}}`{{/if~}}. + +Fully qualified name of the enclosing class `{{enclosingClass}}`. + +{{#if prefix~}} +IMPORTANT: All tags must be prefixed with `{{prefix}}` prefix! +{{~/if}} + +{{#each lowCardinalityKeyNames~}} +{{~#if @first~}} +.Low cardinality Keys +[cols="a,a"] +|=== +|Name | Description +{{~/if}} +|`{{this.name}}`|{{{this.displayDescription}}} +{{~#if @last}} +|=== +{{~/if}} +{{~/each}} + +{{#each highCardinalityKeyNames~}} +{{~#if @first~}} +.High cardinality Keys +[cols="a,a"] +|=== +|Name | Description +{{~/if}} +|`{{this.name}}`|{{{this.displayDescription}}} +{{~#if @last}} +|=== +{{~/if}} +{{~/each}} + +{{#each events~}} +{{~#if @first~}} +Since, events were set on this documented entry, they will be converted to the following counters. +{{/if}} +[[observability-metrics-{{slugify (lower (replace this.enumName "_" " "))}}-{{replace this.name "." "-"}}]] +===== {{capitalize (lower (replace this.enumName "_" " "))}} - {{replace this.name "." " "}} + +> {{{this.description}}} + +**Metric name** `{{{this.name}}}`{{#if (isDynamic this.name)~}} + - since it contains `%s`, the name is dynamic and will be resolved at runtime. + {{~else~}} + . +{{~/if}} **Type** `{{replace (lower this.type) "_" " "}}`. +{{/each}} + +{{~/each}} diff --git a/micrometer-docs-generator/src/main/resources/templates/spans.adoc.hbs b/micrometer-docs-generator/src/main/resources/templates/spans.adoc.hbs new file mode 100644 index 0000000..d0a46c2 --- /dev/null +++ b/micrometer-docs-generator/src/main/resources/templates/spans.adoc.hbs @@ -0,0 +1,48 @@ +{{! + This is a handlebars template file for metrics documentation. +~}} +[[observability-spans]] +=== Observability - Spans + +Below you can find a list of all spans declared by this project. + +{{#each entries~}} + +[[observability-spans-{{slugify (lower (replace enumName "_" " "))}}]] +==== {{capitalize (lower (replace spanTitle "_" " "))}} + +> {{{description}}} + +**Span name** {{{displayName}}}{{#if (isDynamic name)}} - since it contains `%s`, the name is dynamic and will be resolved at runtime{{/if}}. + +Fully qualified name of the enclosing class `{{enclosingClass}}`. + +{{#if prefix~}} +IMPORTANT: All tags must be prefixed with `{{prefix}}` prefix! +{{~/if}} + +{{#each tagKeys~}} +{{~#if @first~}} +.Tag Keys +|=== +|Name | Description +{{~/if}} +|`{{this.name}}`|{{{this.displayDescription}}} +{{~#if @last}} +|=== +{{~/if}} +{{~/each}} + +{{#each events~}} +{{~#if @first~}} +.Event Values +|=== +|Name | Description +{{~/if}} +|`{{this.name}}`|{{{this.displayDescription}}} +{{~#if @last}} +|=== +{{~/if}} +{{~/each}} + +{{/each}} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/conventions/ObservationConventionEntryTests.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/conventions/ObservationConventionEntryTests.java new file mode 100644 index 0000000..4438614 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/conventions/ObservationConventionEntryTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.conventions; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.github.jknack.handlebars.Handlebars; +import io.micrometer.docs.commons.templates.HandlebarsUtils; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ObservationConventionEntryTests { + File output = new File(".", "build/conventions"); + + @BeforeEach + void setup() { + output.mkdirs(); + } + + //TODO: Write other test cases + @Test + void should_save_conventions_as_adoc_table() throws IOException { + ObservationConventionEntry localEntry = new ObservationConventionEntry("foo.bar.LocalBaz", ObservationConventionEntry.Type.LOCAL, "Observation.Context"); + ObservationConventionEntry globalEntry = new ObservationConventionEntry("foo.bar.GlobalBaz", ObservationConventionEntry.Type.GLOBAL, "Foo"); + List globals = Collections.singletonList(globalEntry); + List locals = Collections.singletonList(localEntry); + + Handlebars handlebars = HandlebarsUtils.createHandlebars(); + + Map map = new HashMap<>(); + map.put("globals", globals); + map.put("locals", locals); + + String template = "templates/conventions.adoc.hbs"; + + String result = handlebars.compile(template).apply(map); + + BDDAssertions.then(result) + .contains("|`foo.bar.GlobalBaz`|`Foo`") + .contains("|`foo.bar.LocalBaz`|`Observation.Context`"); + } +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/AnnotationObservation.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/AnnotationObservation.java new file mode 100644 index 0000000..1c3d431 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/AnnotationObservation.java @@ -0,0 +1,101 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.docs.ObservationDocumentation; + +; + +enum AnnotationObservation implements ObservationDocumentation { + + /** + * Observation that wraps annotations. + */ + ANNOTATION_NEW_OR_CONTINUE { + @Override + public String getName() { + return "%s"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return Tags.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return Tags2.values(); + } + + }; + + /** + * Low cardinality tags. + */ + enum Tags implements KeyName { + + /** + * Class name where a method got annotated. + */ + CLASS { + @Override + public String asString() { + return "class"; + } + }, + + /** + * Method name that got annotated with annotation. + */ + METHOD { + @Override + public String asString() { + return "method"; + } + } + + } + + /** + * High cardinality tags. + */ + enum Tags2 implements KeyName { + + /** + * Class name where a method got annotated. + */ + CLASS2 { + @Override + public String asString() { + return "class2"; + } + }, + + /** + * Method name that got annotated. + */ + METHOD2 { + @Override + public String asString() { + return "method2"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/AsyncObservation.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/AsyncObservation.java new file mode 100644 index 0000000..1c0c354 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/AsyncObservation.java @@ -0,0 +1,171 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics; + + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +enum AsyncObservation implements ObservationDocumentation { + + /** + * Observation that wraps a @Async annotation. + */ + ASYNC_ANNOTATION { + @Override + public String getName() { + return "%s"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return AsyncSpanTags.values(); + } + + }, + + /** + * FOO. + */ + TEST { + @Override + public String getName() { + return "fixed"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return KeyName.merge(TestSpanTags.values(), AsyncSpanTags.values()); + } + + }, + + /** + * FOO. + */ + TEST_WITH_CONVENTION { + @Override + public Class> getDefaultConvention() { + return MyConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return KeyName.merge(TestSpanTags.values(), AsyncSpanTags.values()); + } + + }, + + /** + * FOO2. + */ + TEST_WITH_CONVENTION_2 { + @Override + public Class> getDefaultConvention() { + return PublicObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return KeyName.merge(TestSpanTags.values(), AsyncSpanTags.values()); + } + + }, + + /** + * FOO23 + */ + TEST_WITH_CONVENTION_3 { + @Override + public Class> getDefaultConvention() { + return MyDynamicConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return KeyName.merge(TestSpanTags.values(), AsyncSpanTags.values()); + } + + }; + + static class MyConvention implements ObservationConvention { + + @Override + public String getName() { + return "name.from.convention"; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return true; + } + } + + static class MyDynamicConvention implements ObservationConvention { + + @Override + public String getName() { + return "A" + "name.from.convention" + "C"; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return true; + } + } + + enum AsyncSpanTags implements KeyName { + + /** + * Class name where a method got annotated with @Async. + */ + CLASS { + @Override + public String asString() { + return "class"; + } + }, + + /** + * Method name that got annotated with @Async. + */ + METHOD { + @Override + public String asString() { + return "method"; + } + } + + } + + enum TestSpanTags implements KeyName { + + /** + * Test foo + */ + FOO { + @Override + public String asString() { + return "foooooo"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/DocsFromSourcesTests.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/DocsFromSourcesTests.java new file mode 100644 index 0000000..4cfbb85 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/DocsFromSourcesTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.regex.Pattern; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +class DocsFromSourcesTests { + + @Test + void should_build_a_table_out_of_enum_tag_key() throws IOException { + File root = new File("."); + File output = new File(root, "build"); + + //FIXME consider isolating classes relevant to this test into their own package and use that as source root + //for now only consider the java classes at the root of package io.micrometer.docs.metrics + File sourceRoot = new File(root, "src/test"); + new MetricsDocGenerator(sourceRoot, Pattern.compile(".*/docs/metrics/[a-zA-Z]+\\.java"), output).generate(); + + BDDAssertions.then(new String(Files.readAllBytes(new File(output, "_metrics.adoc").toPath()))) + .contains("==== Async Annotation") + .contains("____" + System.lineSeparator() + "Observation that wraps a") + .contains("**Metric name** `%s` - since").contains("Fully qualified name of") + .contains("|`class`|Class name where a method got annotated with @Async.") + .contains("|`class2`|Class name where a method got annotated.") + .contains("==== Annotation New Or Continue") + .contains("**Metric name** `my distribution`. **Type** `distribution summary` and **base unit** `bytes`") + .contains("baaaar") + .contains("**Metric name** `my other distribution`. **Type** `distribution summary`.") + .contains("**Metric name** `name.from.convention` (defined by convention class `io.micrometer.docs.metrics.AsyncObservation$MyConvention`).") + .contains("**Metric name** `foo` (defined by convention class `io.micrometer.docs.metrics.PublicObservationConvention`)") + .contains("**Metric name** Unable to resolve the name - please check the convention class `io.micrometer.docs.metrics.AsyncObservation$MyDynamicConvention`") + .contains("Since, events were set on this documented entry, they will be converted to the following counters.") + .contains("[[observability-metrics-events-having-observation-foo-start]]") + .contains("===== Events Having Observation - foo start") + .contains("> Start event.") + .contains("**Metric name** `foo.start`. **Type** `counter`.") + .contains("[[observability-metrics-events-having-observation-foo-stop]]") + .contains("===== Events Having Observation - foo stop") + .contains("> Stop event.") + .contains("**Metric name** `foo.stop`. **Type** `counter`.") + .doesNotContain("docs.metrics.usecases"); //smoke test that usecases have been excluded + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/EventObservation.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/EventObservation.java new file mode 100644 index 0000000..4df1181 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/EventObservation.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.docs.ObservationDocumentation; + +enum EventObservation implements ObservationDocumentation { + + /** + * Events having observation. + */ + EVENTS_HAVING_OBSERVATION { + @Override + public String getName() { + return "foo"; + } + + @Override + public String getContextualName() { + return "foo span name"; + } + + @Override + public Observation.Event[] getEvents() { + return Events.values(); + } + }; + + /** + * Observation events. + */ + enum Events implements Observation.Event { + + /** + * Start event. + */ + START { + @Override + public String getName() { + return "start"; + } + + @Override + public String getContextualName() { + return "start annotation"; + } + }, + + /** + * Stop event. + */ + STOP { + @Override + public String getName() { + return "stop"; + } + + @Override + public String getContextualName() { + return "stop %s %s foo"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/MyDistributionSummary.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/MyDistributionSummary.java new file mode 100644 index 0000000..5376ca3 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/MyDistributionSummary.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.docs.MeterDocumentation; + +enum MyDistributionSummary implements MeterDocumentation { + + /** + * A test distribution. + */ + TEST_DISTRIBUTION { + @Override + public String getName() { + return "my distribution"; + } + + @Override + public String getBaseUnit() { + return "bytes"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.DISTRIBUTION_SUMMARY; + } + + @Override + public KeyName[] getKeyNames() { + return TestSpanTags.values(); + } + + }; + + enum TestSpanTags implements KeyName { + + /** + * Test bar. + */ + BAR { + @Override + public String asString() { + return "baaaar"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/MyOtherDistributionSummary.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/MyOtherDistributionSummary.java new file mode 100644 index 0000000..62c715e --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/MyOtherDistributionSummary.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.docs.MeterDocumentation; + +enum MyOtherDistributionSummary implements MeterDocumentation { + + /** + * A test distribution. + */ + OTHER_TEST_DISTRIBUTION { + @Override + public String getName() { + return "my other distribution"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.DISTRIBUTION_SUMMARY; + } + + @Override + public KeyName[] getKeyNames() { + return TestSpanTags.values(); + } + + }; + + enum TestSpanTags implements KeyName { + + /** + * Test bar. + */ + BAR { + @Override + public String asString() { + return "baaaar"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/PublicObservationConvention.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/PublicObservationConvention.java new file mode 100644 index 0000000..9d1799a --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/PublicObservationConvention.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +public class PublicObservationConvention implements ObservationConvention { + + @Override + public String getName() { + return "foo"; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof Observation.Context; + } +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/usecases/sanitizing/ComplexJavadocTest.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/usecases/sanitizing/ComplexJavadocTest.java new file mode 100644 index 0000000..49e85c4 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/usecases/sanitizing/ComplexJavadocTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics.usecases.sanitizing; + + +import io.micrometer.docs.metrics.MetricsDocGenerator; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.regex.Pattern; + +class ComplexJavadocTest { + + @Test + void should_sanitize_meter_and_tag_javadocs() { + File sourceRoot = new File("src/test/java/io/micrometer/docs/metrics/usecases/sanitizing"); + File output = new File( "./build"); + + new MetricsDocGenerator(sourceRoot, Pattern.compile(".*"), output).generate(); + + BDDAssertions.then(new File(output, "_metrics.adoc")) + .hasSameTextualContentAs(new File(getClass().getResource("/expected-sanitizing.adoc").getFile())); + } +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/usecases/sanitizing/WithComplexJavadocDocumentedMeter.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/usecases/sanitizing/WithComplexJavadocDocumentedMeter.java new file mode 100644 index 0000000..2999531 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/metrics/usecases/sanitizing/WithComplexJavadocDocumentedMeter.java @@ -0,0 +1,152 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.metrics.usecases.sanitizing; + + +import io.micrometer.common.docs.KeyName; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.docs.MeterDocumentation; + +enum WithComplexJavadocMeterDocumentation implements MeterDocumentation { + + /** + * This javadoc includes sanitized HTML elements and should result in multi-line output, + * except when a line is just there for wrapping like this one. + *

          + * A paragraph. + *

          + *

          + * An unclosed paragraph. + *
          + * An unclosed BR. + *
          + * A closed in single tag BR. + *

            + *
          • it also contains
          • + *
          • an unordered list
          • + *
          + * This is a sentence with bold and italics inside a strong tag. + * + * @return nothing + * @param none no parameter + */ + HTML { + @Override + public String getName() { + return "html"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.TIMER; + } + }, + + /** + * This javadoc includes sanitized taglets elements which should all result in a single line: + * This is code: {@code someCode}. + * This is a simple link: {@linkplain #HTML}. + * This is a complex link with alias text: {@link io.micrometer.docs.commons.utils.AsciidocUtils#simpleHtmlToAsciidoc(String, boolean) some custom alias}. + */ + TAGLETS { + @Override + public String getName() { + return "taglets"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.TIMER; + } + }, + + /** + * Single line with
          inline new line then an admonition.This is an admonition with *bold* and _italics_. This text is not part of the admonition. + */ + INLINE_HTML_TAGS { + @Override + public String getName() { + return "inline_html_tags"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.TIMER; + } + }, + + /** + * This one demonstrates javadoc extraction and sanitization in tags. + */ + WITH_TAGS { + @Override + public String getName() { + return "tags"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.TIMER; + } + + @Override + public KeyName[] getKeyNames() { + return ComplexJavadocTags.values(); + } + }; + + enum ComplexJavadocTags implements KeyName { + + /** + * This tag javadoc includes sanitized HTML elements and should result in multi-line output: + *

          + * A paragraph. + *

          + *

          + * An unclosed paragraph. + *
          + * An unclosed BR. + *
          + * A closed in single tag BR. + *

            + *
          • it also contains
          • + *
          • an unordered list
          • + *
          + */ + TAG_HTML { + @Override + public String asString() { + return "class"; + } + }, + + /** + * This tag javadoc includes sanitized taglets elements which should all result in a single line: + * This is code: {@code someCode}. + * This is a simple link: {@link #HTML}. + * This is a link with alias text: {@link #HTML alias}. + */ + TAG_TAGLETS { + @Override + public String asString() { + return "method"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/AnnotationSpan.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/AnnotationSpan.java new file mode 100644 index 0000000..afdc783 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/AnnotationSpan.java @@ -0,0 +1,177 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.conventions; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +enum AnnotationSpan implements ObservationDocumentation { + + /** + * Observation that wraps annotations. + */ + PUBLIC_CONVENTION { + @Override + public Class> getDefaultConvention() { + return PublicObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return Tags.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return Tags2.values(); + } + + }, + + /** + * Observation that wraps annotations. + */ + NESTED_CONVENTION { + @Override + public Class> getDefaultConvention() { + return NestedConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return Tags.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return Tags2.values(); + } + + }, + + /** + * Observation that wraps annotations. + */ + DYNAMIC_CONVENTION { + @Override + public Class> getDefaultConvention() { + return DynamicObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return Tags.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return Tags2.values(); + } + + }, + + /** + * Observation with interface that implements getName method. + */ + CONCRETE_CONVENTION { + @Override + public Class> getDefaultConvention() { + return UseInterfaceObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return Tags.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return Tags2.values(); + } + + }; + + static class NestedConvention implements ObservationConvention { + + @Override + public String getName() { + return "nested convention"; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return false; + } + } + + /** + * Low cardinality tags. + */ + enum Tags implements KeyName { + + /** + * Class name where a method got annotated with a annotation. + */ + CLASS { + @Override + public String asString() { + return "class"; + } + }, + + /** + * Method name that got annotated with annotation. + */ + METHOD { + @Override + public String asString() { + return "method"; + } + } + + } + + /** + * High cardinality tags. + */ + enum Tags2 implements KeyName { + + /** + * Class name where a method got annotated with a annotation. + */ + CLASS2 { + @Override + public String asString() { + return "class2"; + } + }, + + /** + * Method name that got annotated with annotation. + */ + METHOD2 { + @Override + public String asString() { + return "method2"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/ConventionsTests.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/ConventionsTests.java new file mode 100644 index 0000000..e67533a --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/ConventionsTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.conventions; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.regex.Pattern; + +import io.micrometer.docs.spans.SpansDocGenerator; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +class ConventionsTests { + + @Test + void should_build_a_table_out_of_enum_tag_key() throws IOException { + File root = new File("./src/test/java/io/micrometer/docs/spans/conventions"); + File output = new File(".", "build/conventions"); + output.mkdirs(); + + new SpansDocGenerator(root, Pattern.compile(".*"), output).generate(); + + BDDAssertions.then(new String(Files.readAllBytes(new File(output, "_spans.adoc").toPath()))) + .contains("**Span name** `nested convention` (defined by convention class `io.micrometer.docs.spans.conventions.AnnotationSpan$NestedConvention`)") + .contains("**Span name** `foo` (defined by convention class `io.micrometer.docs.spans.conventions.PublicObservationConvention`)") + .contains("**Span name** `foo-iface` (defined by convention class `io.micrometer.docs.spans.conventions.UseInterfaceObservationConvention`)") + .contains("**Span name** Unable to resolve the name - please check the convention class `io.micrometer.docs.spans.conventions.DynamicObservationConvention` for more details."); + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/DynamicObservationConvention.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/DynamicObservationConvention.java new file mode 100644 index 0000000..1e338b8 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/DynamicObservationConvention.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.conventions; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +public class DynamicObservationConvention implements ObservationConvention { + + @Override + public String getName() { + return "A" + "foo" + "B"; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof Observation.Context; + } +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/ObservationConventionInterface.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/ObservationConventionInterface.java new file mode 100644 index 0000000..67af60f --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/ObservationConventionInterface.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.conventions; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +public interface ObservationConventionInterface extends ObservationConvention { + + @Override + default String getName() { + return "foo-iface"; + } + + @Override + default boolean supportsContext(Context context) { + return true; + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicExtendingGlobalObservationConvention.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicExtendingGlobalObservationConvention.java new file mode 100644 index 0000000..008a62b --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicExtendingGlobalObservationConvention.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.conventions; + +import io.micrometer.observation.Observation; + +public class PublicExtendingGlobalObservationConvention extends PublicGlobalObservationConvention { + + @Override + public String getName() { + return "public global extending"; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof Observation.Context; + } +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicGlobalObservationConvention.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicGlobalObservationConvention.java new file mode 100644 index 0000000..3db69c5 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicGlobalObservationConvention.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.conventions; + +import io.micrometer.observation.GlobalObservationConvention; +import io.micrometer.observation.Observation; + +public class PublicGlobalObservationConvention implements GlobalObservationConvention { + + @Override + public String getName() { + return "public global"; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof Observation.Context; + } +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicObservationConvention.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicObservationConvention.java new file mode 100644 index 0000000..768b37d --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/PublicObservationConvention.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.conventions; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +public class PublicObservationConvention implements ObservationConvention { + + @Override + public String getName() { + return "foo"; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof Observation.Context; + } +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/UseInterfaceObservationConvention.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/UseInterfaceObservationConvention.java new file mode 100644 index 0000000..4199c71 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/conventions/UseInterfaceObservationConvention.java @@ -0,0 +1,20 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.conventions; + +public class UseInterfaceObservationConvention implements ObservationConventionInterface { +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/AnnotationSpan.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/AnnotationSpan.java new file mode 100644 index 0000000..bbbb53a --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/AnnotationSpan.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test1; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.tracing.docs.EventValue; +import io.micrometer.tracing.docs.SpanDocumentation; + +enum AnnotationSpan implements SpanDocumentation { + + /** + * Span that wraps a @NewSpan or @ContinueSpan annotations. + */ + ANNOTATION_NEW_OR_CONTINUE_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public KeyName[] getKeyNames() { + return Tags.values(); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + + }; + + /** + * Tags related to annotations. + * + * @author Marcin Grzejszczak + * @since 3.0.3 + */ + enum Tags implements KeyName { + + /** + * Class name where a method got annotated with a annotation. + */ + CLASS { + @Override + public String asString() { + return "class"; + } + }, + + /** + * Method name that got annotated with annotation. + */ + METHOD { + @Override + public String asString() { + return "method"; + } + } + + } + + enum Events implements EventValue { + + /** + * Annotated before executing a method annotated with @ContinueSpan or @NewSpan. + */ + BEFORE { + @Override + public String getValue() { + return "%s.before"; + } + }, + + /** + * Annotated after executing a method annotated with @ContinueSpan or @NewSpan. + */ + AFTER { + @Override + public String getValue() { + return "%s.after"; + } + }, + + /** + * Annotated after throwing an exception from a method annotated + * with @ContinueSpan or @NewSpan. + */ + AFTER_FAILURE { + @Override + public String getValue() { + return "%.afterFailure"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/AsyncSpan.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/AsyncSpan.java new file mode 100644 index 0000000..a2d6d49 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/AsyncSpan.java @@ -0,0 +1,96 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test1; + + +import io.micrometer.common.docs.KeyName; +import io.micrometer.tracing.docs.SpanDocumentation; + +enum AsyncSpan implements SpanDocumentation { + + /** + * Span that wraps a @Async annotation. Either continues an existing one or creates a + * new one if there was no present one. + */ + ASYNC_ANNOTATION_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public KeyName[] getKeyNames() { + return AsyncSpanTags.values(); + } + + }, + + /** + * Test span. + */ + TEST_SPAN { + @Override + public String getName() { + return "fixed"; + } + + @Override + public KeyName[] getKeyNames() { + return KeyName.merge(TestSpanTags.values(), AsyncSpanTags.values()); + } + + }; + + enum AsyncSpanTags implements KeyName { + + /** + * Class name where a method got annotated with @Async. + */ + CLASS { + @Override + public String asString() { + return "class"; + } + }, + + /** + * Method name that got annotated with @Async. + */ + METHOD { + @Override + public String asString() { + return "method"; + } + } + + } + + enum TestSpanTags implements KeyName { + + /** + * Test foo + */ + FOO { + @Override + public String asString() { + return "foooooo"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/EventObservation.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/EventObservation.java new file mode 100644 index 0000000..b3663b3 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/EventObservation.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test1; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.docs.ObservationDocumentation; + +enum EventObservation implements ObservationDocumentation { + + /** + * Events having observation. + */ + EVENTS_HAVING_OBSERVATION { + @Override + public String getName() { + return "foo"; + } + + @Override + public String getContextualName() { + return "foo span name"; + } + + @Override + public Observation.Event[] getEvents() { + return Events.values(); + } + }; + + /** + * Observation events. + */ + enum Events implements Observation.Event { + + /** + * Start event. + */ + START { + @Override + public String getName() { + return "start"; + } + + @Override + public String getContextualName() { + return "start annotation"; + } + }, + + /** + * Stop event. + */ + STOP { + @Override + public String getName() { + return "stop"; + } + + @Override + public String getContextualName() { + return "stop %s %s foo"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/NoOverridingOfTagsDocsFromSourcesTests.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/NoOverridingOfTagsDocsFromSourcesTests.java new file mode 100644 index 0000000..40dd6cb --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/NoOverridingOfTagsDocsFromSourcesTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test1; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.regex.Pattern; + +import io.micrometer.docs.spans.SpansDocGenerator; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +class NoOverridingOfTagsDocsFromSourcesTests { + + @Test + void should_build_a_table_out_of_enum_tag_key() throws IOException { + File root = new File("./src/test/java/io/micrometer/docs/spans/test1"); + File output = new File(".", "build/test1"); + output.mkdirs(); + + new SpansDocGenerator(root, Pattern.compile(".*"), output).generate(); + + BDDAssertions.then(new String(Files.readAllBytes(new File(output, "_spans.adoc").toPath()))) + .contains("==== Async Annotation Span").contains("> Span that wraps a") + .contains("**Span name** `%s` - since").contains("Fully qualified name of") + .contains("|`class`|Class name where a method got annotated with @Async.") + .contains("==== Annotation New Or Continue Span") + .contains("|`%s.before`|Annotated before executing a method annotated with @ContinueSpan or @NewSpan.") + .contains("==== Test Span").contains("**Span name** `fixed`.").contains("|`foooooo`|Test foo") + .contains("==== Parent Span") + .contains("|`parent.class`|Class name where a method got annotated with a annotation.") + .contains("Events Having Observation Span") + .contains("|`start annotation`|Start event.") + .contains("|`stop %s %s foo`|Stop event. (since the name contains `%s` the final value will be resolved at runtime)"); + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/ParentSample.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/ParentSample.java new file mode 100644 index 0000000..38078b5 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test1/ParentSample.java @@ -0,0 +1,105 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test1; + + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.docs.ObservationDocumentation; + +enum ParentSample implements ObservationDocumentation { + + /** + * Observation that wraps annotations. + */ + PARENT { + @Override + public String getName() { + return "%s"; + } + + @Override + public String getContextualName() { + return "span name"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return Tags.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return Tags2.values(); + } + + }; + + /** + * Low cardinality tags. + */ + enum Tags implements KeyName { + + /** + * Class name where a method got annotated with a annotation. + */ + CLASS { + @Override + public String asString() { + return "parent.class"; + } + }, + + /** + * Method name that got annotated with annotation. + */ + METHOD { + @Override + public String asString() { + return "parent.method"; + } + } + + } + + /** + * High cardinality tags. + */ + enum Tags2 implements KeyName { + + /** + * Class name where a method got annotated with a annotation. + */ + CLASS2 { + @Override + public String asString() { + return "class2"; + } + }, + + /** + * Method name that got annotated with annotation. + */ + METHOD2 { + @Override + public String asString() { + return "method2"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/OverridingSpan.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/OverridingSpan.java new file mode 100644 index 0000000..ae9b4ce --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/OverridingSpan.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test2; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.tracing.docs.SpanDocumentation; + +enum OverridingSpan implements SpanDocumentation { + + /** + * Span. + */ + SHOULD_RETURN_TAG_KEYS_ONLY { + @Override + public String getName() { + return "%s"; + } + + @Override + public KeyName[] getKeyNames() { + return TestSpanTags.values(); + } + + @Override + public Enum overridesDefaultSpanFrom() { + return ParentSample.PARENT; + } + }; + + enum TestSpanTags implements KeyName { + + /** + * Test foo + */ + FOO { + @Override + public String asString() { + return "foooooo"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/ParentSample.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/ParentSample.java new file mode 100644 index 0000000..befb9a5 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/ParentSample.java @@ -0,0 +1,99 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test2; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.docs.ObservationDocumentation; + +enum ParentSample implements ObservationDocumentation { + + /** + * Observation that wraps annotations. + */ + PARENT { + @Override + public String getName() { + return "%s"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return Tags.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return Tags2.values(); + } + + }; + + /** + * Low cardinality tags. + */ + enum Tags implements KeyName { + + /** + * Class name where a method got annotated with a annotation. + */ + CLASS { + @Override + public String asString() { + return "class"; + } + }, + + /** + * Method name that got annotated with annotation. + */ + METHOD { + @Override + public String asString() { + return "method"; + } + } + + } + + /** + * High cardinality tags. + */ + enum Tags2 implements KeyName { + + /** + * Class name where a method got annotated with a annotation. + */ + CLASS2 { + @Override + public String asString() { + return "class2"; + } + }, + + /** + * Method name that got annotated with annotation. + */ + METHOD2 { + @Override + public String asString() { + return "method2"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/TagsFromTagKeysDocsFromSourcesTests.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/TagsFromTagKeysDocsFromSourcesTests.java new file mode 100644 index 0000000..751502c --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test2/TagsFromTagKeysDocsFromSourcesTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test2; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.regex.Pattern; + +import io.micrometer.docs.spans.SpansDocGenerator; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +class TagsFromKeyNamesDocsFromSourcesTests { + + @Test + void should_take_tags_from_tag_keys() throws IOException { + File root = new File("./src/test/java/io/micrometer/docs/spans/test2"); + File output = new File(".", "build/test2"); + output.mkdirs(); + + new SpansDocGenerator(root, Pattern.compile(".*"), output).generate(); + + BDDAssertions.then(new String(Files.readAllBytes(new File(output, "_spans.adoc").toPath()))) + .doesNotContain("==== Parent Span") // this should be overridden + .contains("**Span name** `%s` - since").contains("Fully qualified name of") + .contains("==== Should Return Tag Keys Only Span").contains("|`foooooo`|Test foo"); + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/OverridingSpan.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/OverridingSpan.java new file mode 100644 index 0000000..e88f11a --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/OverridingSpan.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test3; + + +import io.micrometer.common.docs.KeyName; +import io.micrometer.tracing.docs.SpanDocumentation; + +enum OverridingSpan implements SpanDocumentation { + + /** + * Span. + */ + SHOULD_APPEND_ADDITIONAL_TAG_KEYS_TO_PARENT_SAMPLE { + @Override + public String getName() { + return "%s"; + } + + @Override + public KeyName[] getAdditionalKeyNames() { + return TestSpanTags.values(); + } + + @Override + public Enum overridesDefaultSpanFrom() { + return ParentSample.PARENT; + } + }; + + enum TestSpanTags implements KeyName { + + /** + * Test foo + */ + FOO { + @Override + public String asString() { + return "foooooo"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/ParentSample.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/ParentSample.java new file mode 100644 index 0000000..e6b1823 --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/ParentSample.java @@ -0,0 +1,99 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test3; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.docs.ObservationDocumentation; + +enum ParentSample implements ObservationDocumentation { + + /** + * Observation that wraps annotations. + */ + PARENT { + @Override + public String getName() { + return "%s"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return Tags.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return Tags2.values(); + } + + }; + + /** + * Low cardinality tags. + */ + enum Tags implements KeyName { + + /** + * Class name where a method got annotated with a annotation. + */ + CLASS { + @Override + public String asString() { + return "class"; + } + }, + + /** + * Method name that got annotated with annotation. + */ + METHOD { + @Override + public String asString() { + return "method"; + } + } + + } + + /** + * High cardinality tags. + */ + enum Tags2 implements KeyName { + + /** + * Class name where a method got annotated with a annotation. + */ + CLASS2 { + @Override + public String asString() { + return "class2"; + } + }, + + /** + * Method name that got annotated with annotation. + */ + METHOD2 { + @Override + public String asString() { + return "method2"; + } + } + + } + +} diff --git a/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/TagsFromParentWithOverridingDocsFromSourcesTests.java b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/TagsFromParentWithOverridingDocsFromSourcesTests.java new file mode 100644 index 0000000..cbdf16f --- /dev/null +++ b/micrometer-docs-generator/src/test/java/io/micrometer/docs/spans/test3/TagsFromParentWithOverridingDocsFromSourcesTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://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 io.micrometer.docs.spans.test3; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.regex.Pattern; + +import io.micrometer.docs.spans.SpansDocGenerator; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +class TagsFromParentWithOverridingDocsFromSourcesTests { + + @Test + void should_append_tag_keys_to_parent_ones() throws IOException { + File root = new File("./src/test/java/io/micrometer/docs/spans/test3"); + File output = new File(".", "build/test3"); + output.mkdirs(); + + new SpansDocGenerator(root, Pattern.compile(".*"), output).generate(); + + BDDAssertions.then(new String(Files.readAllBytes(new File(output, "_spans.adoc").toPath()))) + .doesNotContain("==== Parent Span") // this should be overridden + .contains("==== Should Append Additional Tag Keys To Parent Sample Span").contains("> Span.") + .contains("|`class`|Class name where a method got annotated with a annotation.") + .contains("|`class2`|Class name where a method got annotated with a annotation.") + .contains("|`foooooo`|Test foo") + .contains("|`method`|Method name that got annotated with annotation.") + .contains("|`method2`|Method name that got annotated with annotation."); + } + +} diff --git a/micrometer-docs-generator/src/test/resources/expected-sanitizing.adoc b/micrometer-docs-generator/src/test/resources/expected-sanitizing.adoc new file mode 100644 index 0000000..79dead3 --- /dev/null +++ b/micrometer-docs-generator/src/test/resources/expected-sanitizing.adoc @@ -0,0 +1,106 @@ +[[observability-metrics]] +=== Observability - Metrics + +Below you can find a list of all metrics declared by this project. + +[[observability-metrics-html]] +==== Html + +____ +This javadoc includes sanitized HTML elements and should result in multi-line output, except when a line is just there for wrapping like this one. + +A paragraph. + +An unclosed paragraph. + +An unclosed BR. + +A closed in single tag BR. + + - it also contains + - an unordered list + + +IMPORTANT: This is a sentence with *bold* and _italics_ inside a strong tag. +____ + +**Metric name** `html`. **Type** `timer` and **base unit** `seconds`. + +Fully qualified name of the enclosing class `io.micrometer.docs.metrics.usecases.sanitizing.WithComplexJavadocMeterDocumentation`. + + + + + + + +[[observability-metrics-inline-html-tags]] +==== Inline Html Tags + +____ +Single line with + +inline new line then an admonition. + +IMPORTANT: This is an admonition with *bold* and _italics_. + +This text is not part of the admonition. +____ + +**Metric name** `inline_html_tags`. **Type** `timer` and **base unit** `seconds`. + +Fully qualified name of the enclosing class `io.micrometer.docs.metrics.usecases.sanitizing.WithComplexJavadocMeterDocumentation`. + + + + + + + +[[observability-metrics-taglets]] +==== Taglets + +____ +This javadoc includes sanitized taglets elements which should all result in a single line: This is code: `someCode`. This is a simple link: `#HTML`. This is a complex link with alias text: some custom alias. +____ + +**Metric name** `taglets`. **Type** `timer` and **base unit** `seconds`. + +Fully qualified name of the enclosing class `io.micrometer.docs.metrics.usecases.sanitizing.WithComplexJavadocMeterDocumentation`. + + + + + + + +[[observability-metrics-with-tags]] +==== With Tags + +____ +This one demonstrates javadoc extraction and sanitization in tags. +____ + +**Metric name** `tags`. **Type** `timer` and **base unit** `seconds`. + +Fully qualified name of the enclosing class `io.micrometer.docs.metrics.usecases.sanitizing.WithComplexJavadocMeterDocumentation`. + + + +.Low cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`class`|This tag javadoc includes sanitized HTML elements and should result in multi-line output: + +A paragraph. + +An unclosed paragraph. + +An unclosed BR. + +A closed in single tag BR. + + - it also contains + - an unordered list +|`method`|This tag javadoc includes sanitized taglets elements which should all result in a single line: This is code: `someCode`. This is a simple link: `#HTML`. This is a link with alias text: alias. +|=== + + + + diff --git a/micrometer-docs-generator/src/test/resources/logback.xml b/micrometer-docs-generator/src/test/resources/logback.xml new file mode 100644 index 0000000..7831a9a --- /dev/null +++ b/micrometer-docs-generator/src/test/resources/logback.xml @@ -0,0 +1,46 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index cbdea6f..991b96a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -27,4 +27,5 @@ include "micrometer-docs-generator-commons", "micrometer-docs-generator-bom" ['metrics', 'spans'].each { type -> include "micrometer-docs-generator-${type}" } +include "micrometer-docs-generator"