Skip to content

Commit 0cfd2d9

Browse files
authored
Automatically Generate Instrumentation Documentation (#13449)
1 parent d28aca1 commit 0cfd2d9

16 files changed

+2556
-0
lines changed

docs/instrumentation-list.yaml

+1,603
Large diffs are not rendered by default.

instrumentation-docs/build.gradle.kts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
plugins {
2+
id("otel.java-conventions")
3+
}
4+
5+
otelJava {
6+
minJavaVersionSupported.set(JavaVersion.VERSION_17)
7+
}
8+
9+
dependencies {
10+
implementation("org.yaml:snakeyaml:2.0")
11+
12+
testImplementation(enforcedPlatform("org.junit:junit-bom:5.12.0"))
13+
testImplementation("org.assertj:assertj-core:3.27.3")
14+
testImplementation("org.junit.jupiter:junit-jupiter-api")
15+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
16+
}
17+
18+
tasks {
19+
val generateDocs by registering(JavaExec::class) {
20+
dependsOn(classes)
21+
22+
mainClass.set("io.opentelemetry.instrumentation.docs.DocGeneratorApplication")
23+
classpath(sourceSets["main"].runtimeClasspath)
24+
}
25+
}

instrumentation-docs/readme.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Doc Generator
2+
3+
Runs analysis on instrumentation modules in order to generate documentation.
4+
5+
## Instrumentation Hierarchy
6+
7+
An "InstrumentationEntity" represents a module that that targets specific code in a framework/library/technology.
8+
Each instrumentation uses muzzle to determine which versions of the target code it supports.
9+
10+
Using these structures as examples:
11+
12+
```
13+
├── instrumentation
14+
│ ├── clickhouse-client-05
15+
│ ├── jaxrs
16+
│ │ ├── jaxrs-1.0
17+
│ │ ├── jaxrs-2.0
18+
│ ├── spring
19+
│ │ ├── spring-cloud-gateway
20+
│ │ │ ├── spring-cloud-gateway-2.0
21+
│ │ │ ├── spring-cloud-gateway-2.2
22+
│ │ │ └── spring-cloud-gateway-common
23+
```
24+
25+
* Name
26+
* Ex: `clickhouse-client-05`, `jaxrs-1.0`, `spring-cloud-gateway-2.0`
27+
* Namespace - direct parent. if none, use name and strip version
28+
* `clickhouse-client`, `jaxrs`, `spring-cloud-gateway`
29+
* Group - top most parent
30+
* `clickhouse-client`, `jaxrs`, `spring`
31+
32+
This information is also referenced in `InstrumentationModule` code for each module:
33+
34+
```java
35+
public class SpringWebInstrumentationModule extends InstrumentationModule
36+
implements ExperimentalInstrumentationModule {
37+
public SpringWebInstrumentationModule() {
38+
super("spring-web", "spring-web-3.1");
39+
}
40+
```
41+
42+
## Instrumentation meta-data
43+
44+
* name
45+
* Identifier for instrumentation module, used to enable/disable
46+
* Configured in `InstrumentationModule` code for each module
47+
* versions
48+
* List of supported versions by the module
49+
* type
50+
* List of instrumentation types, options of either `library` or `javaagent`
51+
52+
## Methodology
53+
54+
### Versions targeted
55+
56+
Javaagent versions are determined by the `muzzle` plugin, so we can attempt to parse the gradle files
57+
58+
Library versions are determined by the library versions used in the gradle files.
59+
60+
### TODO / Notes
61+
62+
- Is the `library` dependency actually the target version? Is there a better way to present the information?
63+
- How to handle oshi target version with a conditional?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.docs;
7+
8+
import io.opentelemetry.instrumentation.docs.utils.FileManager;
9+
import io.opentelemetry.instrumentation.docs.utils.YamlHelper;
10+
import java.io.BufferedWriter;
11+
import java.io.IOException;
12+
import java.nio.charset.Charset;
13+
import java.nio.file.Files;
14+
import java.nio.file.Paths;
15+
import java.util.List;
16+
import java.util.logging.Logger;
17+
18+
public class DocGeneratorApplication {
19+
20+
private static final Logger logger = Logger.getLogger(DocGeneratorApplication.class.getName());
21+
22+
public static void main(String[] args) {
23+
FileManager fileManager = new FileManager("instrumentation/");
24+
List<InstrumentationEntity> entities = new InstrumentationAnalyzer(fileManager).analyze();
25+
26+
try (BufferedWriter writer =
27+
Files.newBufferedWriter(
28+
Paths.get("docs/instrumentation-list.yaml"), Charset.defaultCharset())) {
29+
YamlHelper.printInstrumentationList(entities, writer);
30+
} catch (IOException e) {
31+
logger.severe("Error writing instrumentation list: " + e.getMessage());
32+
}
33+
}
34+
35+
private DocGeneratorApplication() {}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.docs;
7+
8+
import java.util.HashMap;
9+
import java.util.HashSet;
10+
import java.util.Map;
11+
import java.util.Set;
12+
import java.util.regex.Matcher;
13+
import java.util.regex.Pattern;
14+
15+
class GradleParser {
16+
private static final Pattern passBlockPattern =
17+
Pattern.compile("pass\\s*\\{(.*?)}", Pattern.DOTALL);
18+
19+
private static final Pattern libraryPattern =
20+
Pattern.compile("library\\(\"([^\"]+:[^\"]+:[^\"]+)\"\\)");
21+
22+
private static final Pattern variablePattern =
23+
Pattern.compile("val\\s+(\\w+)\\s*=\\s*\"([^\"]+)\"");
24+
25+
private static final Pattern compileOnlyPattern =
26+
Pattern.compile(
27+
"compileOnly\\(\"([^\"]+)\"\\)\\s*\\{\\s*version\\s*\\{(?:\\s*//.*\\n)*\\s*strictly\\(\"([^\"]+)\"\\)\\s*}\\s*}");
28+
29+
/**
30+
* Parses gradle files for muzzle and dependency information
31+
*
32+
* @param gradleFileContents Contents of a Gradle build file as a String
33+
* @return A set of strings summarizing the group, module, and version ranges
34+
*/
35+
public static Set<String> parseGradleFile(String gradleFileContents, InstrumentationType type) {
36+
Set<String> results = new HashSet<>();
37+
Map<String, String> variables = extractVariables(gradleFileContents);
38+
39+
if (type.equals(InstrumentationType.JAVAAGENT)) {
40+
results.addAll(parseMuzzle(gradleFileContents, variables));
41+
} else {
42+
results.addAll(parseLibraryDependencies(gradleFileContents, variables));
43+
}
44+
45+
return results;
46+
}
47+
48+
/**
49+
* Parses the "muzzle" block from the given Gradle file content and extracts information about
50+
* each "pass { ... }" entry, returning a set of version summary strings.
51+
*
52+
* @param gradleFileContents Contents of a Gradle build file as a String
53+
* @param variables Map of variable names to their values
54+
* @return A set of strings summarizing the group, module, and version ranges
55+
*/
56+
private static Set<String> parseMuzzle(String gradleFileContents, Map<String, String> variables) {
57+
Set<String> results = new HashSet<>();
58+
Matcher passBlockMatcher = passBlockPattern.matcher(gradleFileContents);
59+
60+
while (passBlockMatcher.find()) {
61+
String passBlock = passBlockMatcher.group(1);
62+
63+
String group = extractValue(passBlock, "group\\.set\\(\"([^\"]+)\"\\)");
64+
String module = extractValue(passBlock, "module\\.set\\(\"([^\"]+)\"\\)");
65+
String versionRange = extractValue(passBlock, "versions\\.set\\(\"([^\"]+)\"\\)");
66+
67+
if (group != null && module != null && versionRange != null) {
68+
String summary = group + ":" + module + ":" + interpolate(versionRange, variables);
69+
results.add(summary);
70+
}
71+
}
72+
return results;
73+
}
74+
75+
/**
76+
* Parses the "dependencies" block from the given Gradle file content and extracts information
77+
* about what library versions are supported.
78+
*
79+
* @param gradleFileContents Contents of a Gradle build file as a String
80+
* @param variables Map of variable names to their values
81+
* @return A set of strings summarizing the group, module, and versions
82+
*/
83+
private static Set<String> parseLibraryDependencies(
84+
String gradleFileContents, Map<String, String> variables) {
85+
Set<String> results = new HashSet<>();
86+
Matcher libraryMatcher = libraryPattern.matcher(gradleFileContents);
87+
while (libraryMatcher.find()) {
88+
String dependency = libraryMatcher.group(1);
89+
results.add(interpolate(dependency, variables));
90+
}
91+
92+
Matcher compileOnlyMatcher = compileOnlyPattern.matcher(gradleFileContents);
93+
while (compileOnlyMatcher.find()) {
94+
String dependency = compileOnlyMatcher.group(1) + ":" + compileOnlyMatcher.group(2);
95+
results.add(interpolate(dependency, variables));
96+
}
97+
return results;
98+
}
99+
100+
/**
101+
* Extracts variables from the given Gradle file content.
102+
*
103+
* @param gradleFileContents Contents of a Gradle build file as a String
104+
* @return A map of variable names to their values
105+
*/
106+
private static Map<String, String> extractVariables(String gradleFileContents) {
107+
Map<String, String> variables = new HashMap<>();
108+
Matcher variableMatcher = variablePattern.matcher(gradleFileContents);
109+
110+
while (variableMatcher.find()) {
111+
variables.put(variableMatcher.group(1), variableMatcher.group(2));
112+
}
113+
114+
return variables;
115+
}
116+
117+
/**
118+
* Interpolates variables in the given text using the provided variable map.
119+
*
120+
* @param text Text to interpolate
121+
* @param variables Map of variable names to their values
122+
* @return Interpolated text
123+
*/
124+
private static String interpolate(String text, Map<String, String> variables) {
125+
for (Map.Entry<String, String> entry : variables.entrySet()) {
126+
text = text.replace("$" + entry.getKey(), entry.getValue());
127+
}
128+
return text;
129+
}
130+
131+
/**
132+
* Utility method to extract the first captured group from matching the given regex.
133+
*
134+
* @param text Text to search
135+
* @param regex Regex with a capturing group
136+
* @return The first captured group, or null if not found
137+
*/
138+
private static String extractValue(String text, String regex) {
139+
Pattern pattern = Pattern.compile(regex);
140+
Matcher matcher = pattern.matcher(text);
141+
if (matcher.find()) {
142+
return matcher.group(1);
143+
}
144+
return null;
145+
}
146+
147+
private GradleParser() {}
148+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.docs;
7+
8+
import static io.opentelemetry.instrumentation.docs.GradleParser.parseGradleFile;
9+
10+
import io.opentelemetry.instrumentation.docs.utils.FileManager;
11+
import io.opentelemetry.instrumentation.docs.utils.InstrumentationPath;
12+
import java.util.ArrayList;
13+
import java.util.HashMap;
14+
import java.util.HashSet;
15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.Set;
18+
19+
class InstrumentationAnalyzer {
20+
21+
private final FileManager fileSearch;
22+
23+
InstrumentationAnalyzer(FileManager fileSearch) {
24+
this.fileSearch = fileSearch;
25+
}
26+
27+
/**
28+
* Converts a list of InstrumentationPath objects into a list of InstrumentationEntity objects.
29+
* Each InstrumentationEntity represents a unique combination of group, namespace, and
30+
* instrumentation name. The types of instrumentation (e.g., library, javaagent) are aggregated
31+
* into a list within each entity.
32+
*
33+
* @param paths the list of InstrumentationPath objects to be converted
34+
* @return a list of InstrumentationEntity objects with aggregated types
35+
*/
36+
public static List<InstrumentationEntity> convertToEntities(List<InstrumentationPath> paths) {
37+
Map<String, InstrumentationEntity> entityMap = new HashMap<>();
38+
39+
for (InstrumentationPath path : paths) {
40+
String key = path.group() + ":" + path.namespace() + ":" + path.instrumentationName();
41+
if (!entityMap.containsKey(key)) {
42+
entityMap.put(
43+
key,
44+
new InstrumentationEntity(
45+
path.srcPath().replace("/javaagent", "").replace("/library", ""),
46+
path.instrumentationName(),
47+
path.namespace(),
48+
path.group()));
49+
}
50+
}
51+
52+
return new ArrayList<>(entityMap.values());
53+
}
54+
55+
/**
56+
* Analyzes the given root directory to find all instrumentation paths and then analyze them. -
57+
* Extracts version information from each instrumentation's build.gradle file.
58+
*
59+
* @return a list of InstrumentationEntity objects with target versions
60+
*/
61+
List<InstrumentationEntity> analyze() {
62+
List<InstrumentationPath> paths = fileSearch.getInstrumentationPaths();
63+
List<InstrumentationEntity> entities = convertToEntities(paths);
64+
65+
for (InstrumentationEntity entity : entities) {
66+
List<String> gradleFiles = fileSearch.findBuildGradleFiles(entity.getSrcPath());
67+
analyzeVersions(gradleFiles, entity);
68+
}
69+
return entities;
70+
}
71+
72+
void analyzeVersions(List<String> files, InstrumentationEntity entity) {
73+
Map<InstrumentationType, Set<String>> versions = new HashMap<>();
74+
for (String file : files) {
75+
String fileContents = fileSearch.readFileToString(file);
76+
77+
if (file.contains("/javaagent/")) {
78+
Set<String> results = parseGradleFile(fileContents, InstrumentationType.JAVAAGENT);
79+
versions
80+
.computeIfAbsent(InstrumentationType.JAVAAGENT, k -> new HashSet<>())
81+
.addAll(results);
82+
} else if (file.contains("/library/")) {
83+
Set<String> results = parseGradleFile(fileContents, InstrumentationType.LIBRARY);
84+
versions.computeIfAbsent(InstrumentationType.LIBRARY, k -> new HashSet<>()).addAll(results);
85+
}
86+
}
87+
entity.setTargetVersions(versions);
88+
}
89+
}

0 commit comments

Comments
 (0)