Skip to content

Commit

Permalink
Adding Conventions adoc generation
Browse files Browse the repository at this point in the history
with this change we're doing our best to parse your sources to retrieve information about your global and local ObservationConvention implementations / interfaces. If we can we try to retrieve the information about the applicable Observation.Context type too.
  • Loading branch information
marcingrzejszczak committed Jul 20, 2022
1 parent 41f7a89 commit a04ddae
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 37 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

Generating adoc tables for metrics and spans.

IMPORTANT: This project is in an incubating stage.

## Join the discussion

Join the [Micrometer Slack](https://slack.micrometer.io) to share your questions, concerns, and feature requests.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import io.micrometer.common.util.internal.logging.InternalLogger;
import io.micrometer.common.util.internal.logging.InternalLoggerFactory;
import io.micrometer.docs.commons.utils.StringUtils;

public class ObservationConventionEntry implements Comparable<ObservationConventionEntry> {

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;
}

@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
}

public static void saveEntriesAsAdocTableInAFile(Collection<ObservationConventionEntry> entries, File outputFile) throws IOException {
if (entries.isEmpty()) {
logger.debug("No ObservationConventions found - will not output anything");
return;
}

StringBuilder builder = new StringBuilder();
List<ObservationConventionEntry> global = entries.stream().filter(e -> e.type == Type.GLOBAL).collect(Collectors.toList());
List<ObservationConventionEntry> local = entries.stream().filter(e -> e.type == Type.LOCAL).collect(Collectors.toList());

Path output = outputFile.toPath();
logger.debug("======================================");
logger.debug("Summary of sources analysis for conventions");
logger.debug("Found [" + entries.size() + "] conventions");
logger.debug(
"Found [" + global.size() + "] GlobalObservationConvention implementations");
logger.debug(
"Found [" + local.size() + "] ObservationConvention implementations");

builder.append("[[observability-conventions]]\n")
.append("=== Observability - Conventions\n\n")
.append("Below you can find a list of all `GlobalObservabilityConventions` and `ObservabilityConventions` declared by this project.")
.append("\n\n");

if (!global.isEmpty()) {
builder.append(".GlobalObservationConvention implementations\n")
.append("|===\n")
.append("|GlobalObservationConvention Class Name | Applicable ObservationContext Class Name\n")
.append(global.stream().map(e -> "|`" + e.getClassName() + "`|`" + e.contextClassName + "`").collect(Collectors.joining("\n")))
.append("\n|===");

builder.append("\n\n");
}

// TODO: Duplication
if (!local.isEmpty()) {

builder.append(".ObservationConvention implementations\n")
.append("|===\n")
.append("|ObservationConvention Class Name | Applicable ObservationContext Class Name\n")
.append(local.stream().map(e -> "|`" + e.getClassName() + "`|`" + e.contextClassName + "`").collect(Collectors.joining("\n")))
.append("\n|===");
}

Files.write(output, builder.toString().getBytes());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.IOException;
import java.nio.file.Files;
import java.util.Arrays;

import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

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");
File file = new File(this.output, "_success.adoc");

ObservationConventionEntry.saveEntriesAsAdocTableInAFile(Arrays.asList(localEntry, globalEntry), file);

BDDAssertions.then(new String(Files.readAllBytes(file.toPath())))
.contains("|`foo.bar.GlobalBaz`|`Foo`")
.contains("|`foo.bar.LocalBaz`|`Observation.Context`");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import io.micrometer.core.util.internal.logging.InternalLogger;
import io.micrometer.core.util.internal.logging.InternalLoggerFactory;
import io.micrometer.docs.commons.ObservationConventionEntry;

// TODO: Assert on prefixes
public class DocsFromSources {
Expand Down Expand Up @@ -57,32 +58,37 @@ public void generate() {
Path path = this.projectRoot.toPath();
logger.debug("Path is [" + this.projectRoot.getAbsolutePath() + "]. Inclusion pattern is [" + this.inclusionPattern + "]");
Collection<MetricEntry> entries = new TreeSet<>();
FileVisitor<Path> fv = new MetricSearchingFileVisitor(this.inclusionPattern, entries);
Collection<ObservationConventionEntry> observationConventionEntries = new TreeSet<>();
FileVisitor<Path> fv = new MetricSearchingFileVisitor(this.inclusionPattern, entries, observationConventionEntries);
try {
Files.walkFileTree(path, fv);
MetricEntry.assertThatProperlyPrefixed(entries);
File file = new File(this.outputDir, "_metrics.adoc");
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}
logger.debug("Will create files under [" + file + "]");
StringBuilder stringBuilder = new StringBuilder();
Path output = file.toPath();
logger.debug("======================================");
logger.debug("Summary of sources analysis");
logger.debug("Found [" + entries.size() + "] samples");
logger.debug(
"Found [" + entries.stream().flatMap(e -> e.lowCardinalityKeyNames.stream()).distinct().count() + "] low cardinality tags");
logger.debug(
"Found [" + entries.stream().flatMap(e -> e.highCardinalityKeyNames.stream()).distinct().count() + "] high cardinality tags");
stringBuilder.append("[[observability-metrics]]\n=== Observability - Metrics\n\nBelow you can find a list of all samples declared by this project.\n\n");
entries.forEach(metricEntry -> stringBuilder.append(metricEntry.toString()).append("\n\n"));
Files.write(output, stringBuilder.toString().getBytes());
printMetricsAdoc(entries);
ObservationConventionEntry.saveEntriesAsAdocTableInAFile(observationConventionEntries, new File(this.outputDir, "_conventions.adoc"));
}
catch (IOException e) {
throw new IllegalArgumentException(e);
}
}

private void printMetricsAdoc(Collection<MetricEntry> entries) throws IOException {
File file = new File(this.outputDir, "_metrics.adoc");
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}
logger.debug("Will create files under [" + file + "]");
StringBuilder stringBuilder = new StringBuilder();
Path output = file.toPath();
logger.debug("======================================");
logger.debug("Summary of sources analysis");
logger.debug("Found [" + entries.size() + "] samples");
logger.debug(
"Found [" + entries.stream().flatMap(e -> e.lowCardinalityKeyNames.stream()).distinct().count() + "] low cardinality tags");
logger.debug(
"Found [" + entries.stream().flatMap(e -> e.highCardinalityKeyNames.stream()).distinct().count() + "] high cardinality tags");
stringBuilder.append("[[observability-metrics]]\n=== Observability - Metrics\n\nBelow you can find a list of all samples declared by this project.\n\n");
entries.forEach(metricEntry -> stringBuilder.append(metricEntry.toString()).append("\n\n"));
Files.write(output, stringBuilder.toString().getBytes());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,25 @@
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.DocumentedMeter;
import io.micrometer.core.util.internal.logging.InternalLogger;
import io.micrometer.core.util.internal.logging.InternalLoggerFactory;
import io.micrometer.docs.commons.KeyValueEntry;
import io.micrometer.docs.commons.ObservationConventionEntry;
import io.micrometer.docs.commons.ParsingUtils;
import io.micrometer.observation.Observation;
import io.micrometer.observation.docs.DocumentedObservation;
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.JavaClassImpl;
import org.jboss.forge.roaster.model.impl.JavaEnumImpl;
import org.jboss.forge.roaster.model.source.EnumConstantSource;
import org.jboss.forge.roaster.model.source.MemberSource;
Expand All @@ -55,9 +59,12 @@ class MetricSearchingFileVisitor extends SimpleFileVisitor<Path> {

private final Collection<MetricEntry> sampleEntries;

MetricSearchingFileVisitor(Pattern pattern, Collection<MetricEntry> sampleEntries) {
private final Collection<ObservationConventionEntry> observationConventionEntries;

MetricSearchingFileVisitor(Pattern pattern, Collection<MetricEntry> sampleEntries, Collection<ObservationConventionEntry> observationConventionEntries) {
this.pattern = pattern;
this.sampleEntries = sampleEntries;
this.observationConventionEntries = observationConventionEntries;
}

@Override
Expand All @@ -72,6 +79,19 @@ else if (!file.toString().endsWith(".java")) {
JavaUnit unit = Roaster.parseUnit(stream);
JavaType myClass = unit.getGoverningType();
if (!(myClass instanceof JavaEnumImpl)) {
// TODO: Duplication
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;
}
JavaEnumImpl myEnum = (JavaEnumImpl) myClass;
Expand Down Expand Up @@ -105,6 +125,28 @@ else if (!file.toString().endsWith(".java")) {
return FileVisitResult.CONTINUE;
}

// TODO: Duplication
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 "";
}

// TODO: Duplication
private boolean isLocalObservationConvention(String interf) {
return interf.contains(Observation.ObservationConvention.class.getSimpleName()) || interf.contains(Observation.ObservationConvention.class.getCanonicalName());
}

// TODO: Duplication
private boolean isGlobalObservationConvention(String interf) {
return interf.contains(Observation.GlobalObservationConvention.class.getSimpleName()) || interf.contains(Observation.GlobalObservationConvention.class.getCanonicalName());
}

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import io.micrometer.common.util.internal.logging.InternalLogger;
import io.micrometer.common.util.internal.logging.InternalLoggerFactory;
import io.micrometer.docs.commons.ObservationConventionEntry;


public class DocsFromSources {
Expand Down Expand Up @@ -56,26 +57,33 @@ public void generate() {
Path path = this.projectRoot.toPath();
logger.debug("Path is [" + this.projectRoot.getAbsolutePath() + "]. Inclusion pattern is [" + this.inclusionPattern + "]");
Collection<SpanEntry> spanEntries = new TreeSet<>();
FileVisitor<Path> fv = new SpanSearchingFileVisitor(this.inclusionPattern, spanEntries);
Collection<ObservationConventionEntry> observationConventionEntries = new TreeSet<>();
FileVisitor<Path> fv = new SpanSearchingFileVisitor(this.inclusionPattern, spanEntries, observationConventionEntries);
try {
Files.walkFileTree(path, fv);
SpanEntry.assertThatProperlyPrefixed(spanEntries);
Path output = new File(this.outputDir, "_spans.adoc").toPath();
StringBuilder stringBuilder = new StringBuilder();
logger.debug("======================================");
logger.debug("Summary of sources analysis");
logger.debug("Found [" + spanEntries.size() + "] spans");
logger.debug(
"Found [" + spanEntries.stream().flatMap(e -> e.tagKeys.stream()).distinct().count() + "] tags");
logger.debug(
"Found [" + spanEntries.stream().flatMap(e -> e.events.stream()).distinct().count() + "] events");
stringBuilder.append("[[observability-spans]]\n=== Observability - Spans\n\nBelow you can find a list of all spans declared by this project.\n\n");
spanEntries.forEach(spanEntry -> stringBuilder.append(spanEntry.toString()).append("\n\n"));
Files.write(output, stringBuilder.toString().getBytes());
printSpansAdoc(spanEntries);
ObservationConventionEntry.saveEntriesAsAdocTableInAFile(observationConventionEntries, new File(this.outputDir, "_conventions.adoc"));
}
catch (IOException e) {
throw new IllegalArgumentException(e);
}
}

private void printSpansAdoc(Collection<SpanEntry> spanEntries) throws IOException {
Path output = new File(this.outputDir, "_spans.adoc").toPath();
StringBuilder stringBuilder = new StringBuilder();
logger.debug("======================================");
logger.debug("Summary of sources analysis");
logger.debug("Found [" + spanEntries.size() + "] spans");
logger.debug(
"Found [" + spanEntries.stream().flatMap(e -> e.tagKeys.stream()).distinct().count() + "] tags");
logger.debug(
"Found [" + spanEntries.stream().flatMap(e -> e.events.stream()).distinct().count() + "] events");
stringBuilder.append("[[observability-spans]]\n=== Observability - Spans\n\nBelow you can find a list of all spans declared by this project.\n\n");
spanEntries.forEach(spanEntry -> stringBuilder.append(spanEntry.toString()).append("\n\n"));
Files.write(output, stringBuilder.toString().getBytes());
}


}
Loading

0 comments on commit a04ddae

Please sign in to comment.