-
Notifications
You must be signed in to change notification settings - Fork 969
Automatically Generate Instrumentation Documentation #13449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
758e0bc
basic instrumentation analysis
jaydeluca eb25986
account for variables and library versions
jaydeluca b907bbe
markdown fix
jaydeluca 3f0a5d0
remove system out
jaydeluca 6ada5ff
reformat target types
jaydeluca 7ac4181
fix tests
jaydeluca 38eeb7a
use snakeyml instead of creating yaml manually, improve library parsi…
jaydeluca c6808f2
lowercase javaagent and library
jaydeluca 2865b05
Merge branch 'main' of github.com:jaydeluca/opentelemetry-java-instru…
jaydeluca File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
plugins { | ||
id("otel.java-conventions") | ||
} | ||
|
||
otelJava { | ||
minJavaVersionSupported.set(JavaVersion.VERSION_17) | ||
} | ||
|
||
dependencies { | ||
testImplementation(enforcedPlatform("org.junit:junit-bom:5.12.0")) | ||
testImplementation("org.assertj:assertj-core:3.27.3") | ||
testImplementation("org.junit.jupiter:junit-jupiter-api") | ||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") | ||
} | ||
|
||
tasks { | ||
val generateDocs by registering(JavaExec::class) { | ||
dependsOn(classes) | ||
|
||
mainClass.set("io.opentelemetry.instrumentation.docs.DocGeneratorApplication") | ||
classpath(sourceSets["main"].runtimeClasspath) | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# Doc Generator | ||
|
||
Runs analysis on instrumentation modules in order to generate documentation. | ||
|
||
## Instrumentation Hierarchy | ||
|
||
An "InstrumentationEntity" represents a module that that targets specific code in a framework/library/technology. | ||
Each instrumentation uses muzzle to determine which versions of the target code it supports. | ||
|
||
Using these structures as examples: | ||
|
||
``` | ||
├── instrumentation | ||
│ ├── clickhouse-client-05 | ||
│ ├── jaxrs | ||
│ │ ├── jaxrs-1.0 | ||
│ │ ├── jaxrs-2.0 | ||
│ ├── spring | ||
│ │ ├── spring-cloud-gateway | ||
│ │ │ ├── spring-cloud-gateway-2.0 | ||
│ │ │ ├── spring-cloud-gateway-2.2 | ||
│ │ │ └── spring-cloud-gateway-common | ||
``` | ||
|
||
* Name | ||
* Ex: `clickhouse-client-05`, `jaxrs-1.0`, `spring-cloud-gateway-2.0` | ||
* Namespace - direct parent. if none, use name and strip version | ||
* `clickhouse-client`, `jaxrs`, `spring-cloud-gateway` | ||
* Group - top most parent | ||
* `clickhouse-client`, `jaxrs`, `spring` | ||
|
||
This information is also referenced in `InstrumentationModule` code for each module: | ||
|
||
```java | ||
public class SpringWebInstrumentationModule extends InstrumentationModule | ||
implements ExperimentalInstrumentationModule { | ||
public SpringWebInstrumentationModule() { | ||
super("spring-web", "spring-web-3.1"); | ||
} | ||
``` | ||
|
||
## Instrumentation meta-data | ||
|
||
* name | ||
* Identifier for instrumentation module, used to enable/disable | ||
* Configured in `InstrumentationModule` code for each module | ||
* versions | ||
* List of supported versions by the module | ||
* type | ||
* List of instrumentation types, options of either `library` or `javaagent` | ||
|
||
## Methodology | ||
|
||
### Versions targeted | ||
|
||
Javaagent versions are determined by the `muzzle` plugin, so we can attempt to parse the gradle files | ||
|
||
Library versions are determined by the library versions used in the gradle files. | ||
|
||
### TODO / Notes | ||
|
||
- Is the `library` dependency actually the target version? Is there a better way to present the information? | ||
- How to handle oshi target version with a conditional? |
81 changes: 81 additions & 0 deletions
81
...ion-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocGeneratorApplication.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.docs; | ||
|
||
import io.opentelemetry.instrumentation.docs.utils.FileManager; | ||
import java.io.BufferedWriter; | ||
import java.io.IOException; | ||
import java.nio.charset.Charset; | ||
import java.nio.file.Files; | ||
import java.nio.file.Paths; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.TreeMap; | ||
import java.util.logging.Logger; | ||
import java.util.stream.Collectors; | ||
|
||
public class DocGeneratorApplication { | ||
|
||
private static final Logger logger = Logger.getLogger(DocGeneratorApplication.class.getName()); | ||
|
||
public static void main(String[] args) { | ||
FileManager fileManager = new FileManager("instrumentation/"); | ||
List<InstrumentationEntity> entities = new InstrumentationAnalyzer(fileManager).analyze(); | ||
printInstrumentationList(entities); | ||
} | ||
|
||
private static void printInstrumentationList(List<InstrumentationEntity> list) { | ||
Map<String, List<InstrumentationEntity>> groupedByGroup = | ||
list.stream() | ||
.collect( | ||
Collectors.groupingBy( | ||
InstrumentationEntity::getGroup, TreeMap::new, Collectors.toList())); | ||
|
||
try (BufferedWriter writer = | ||
Files.newBufferedWriter( | ||
Paths.get("docs/instrumentation-list.yaml"), Charset.defaultCharset())) { | ||
groupedByGroup.forEach( | ||
(group, entities) -> { | ||
try { | ||
String groupHeader = group + ":\n instrumentations:\n"; | ||
writer.write(groupHeader); | ||
|
||
for (InstrumentationEntity entity : entities) { | ||
String entityDetails = | ||
String.format( | ||
" - name: %s\n srcPath: %s\n", | ||
entity.getInstrumentationName(), entity.getSrcPath()); | ||
writer.write(entityDetails); | ||
|
||
if (entity.getTargetVersions() == null || entity.getTargetVersions().isEmpty()) { | ||
String targetVersions = " target_versions: {}\n"; | ||
writer.write(targetVersions); | ||
} else { | ||
String targetVersions = " target_versions:\n"; | ||
writer.write(targetVersions); | ||
for (Map.Entry<InstrumentationType, Set<String>> entry : | ||
entity.getTargetVersions().entrySet()) { | ||
String typeHeader = " " + entry.getKey() + ":\n"; | ||
writer.write(typeHeader); | ||
for (String version : entry.getValue()) { | ||
String versionDetail = " - " + version + "\n"; | ||
writer.write(versionDetail); | ||
} | ||
} | ||
} | ||
} | ||
} catch (IOException e) { | ||
logger.severe("Error writing instrumentation list: " + e.getMessage()); | ||
} | ||
}); | ||
} catch (IOException e) { | ||
logger.severe("Error writing instrumentation list: " + e.getMessage()); | ||
} | ||
} | ||
|
||
private DocGeneratorApplication() {} | ||
} |
110 changes: 110 additions & 0 deletions
110
instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/GradleParser.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.docs; | ||
|
||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
class GradleParser { | ||
private static final Pattern passBlockPattern = | ||
Pattern.compile("pass\\s*\\{(.*?)\\}", Pattern.DOTALL); | ||
|
||
private static final Pattern libraryPattern = | ||
Pattern.compile("library\\(\"([^\"]+:[^\"]+:[^\"]+)\"\\)"); | ||
|
||
private static final Pattern variablePattern = | ||
Pattern.compile("val\\s+(\\w+)\\s*=\\s*\"([^\"]+)\""); | ||
|
||
/** | ||
* Parses the "muzzle" block from the given Gradle file content and extracts information about | ||
* each "pass { ... }" entry, returning a set of version summary strings. | ||
* | ||
* @param gradleFileContents Contents of a Gradle build file as a String | ||
* @return A set of strings summarizing the group, module, and version ranges | ||
*/ | ||
public static Set<String> parseMuzzleBlock(String gradleFileContents, InstrumentationType type) { | ||
Set<String> results = new HashSet<>(); | ||
Map<String, String> variables = extractVariables(gradleFileContents); | ||
|
||
if (type.equals(InstrumentationType.JAVAAGENT)) { | ||
Matcher passBlockMatcher = passBlockPattern.matcher(gradleFileContents); | ||
|
||
while (passBlockMatcher.find()) { | ||
String passBlock = passBlockMatcher.group(1); | ||
|
||
String group = extractValue(passBlock, "group\\.set\\(\"([^\"]+)\"\\)"); | ||
String module = extractValue(passBlock, "module\\.set\\(\"([^\"]+)\"\\)"); | ||
String versionRange = extractValue(passBlock, "versions\\.set\\(\"([^\"]+)\"\\)"); | ||
|
||
if (group != null && module != null && versionRange != null) { | ||
String summary = group + ":" + module + ":" + interpolate(versionRange, variables); | ||
results.add(summary); | ||
} | ||
} | ||
} else { | ||
Matcher dependencyMatcher = libraryPattern.matcher(gradleFileContents); | ||
while (dependencyMatcher.find()) { | ||
String dependency = dependencyMatcher.group(1); | ||
results.add(interpolate(dependency, variables)); | ||
} | ||
} | ||
|
||
return results; | ||
} | ||
|
||
/** | ||
* Extracts variables from the given Gradle file content. | ||
* | ||
* @param gradleFileContents Contents of a Gradle build file as a String | ||
* @return A map of variable names to their values | ||
*/ | ||
private static Map<String, String> extractVariables(String gradleFileContents) { | ||
Map<String, String> variables = new HashMap<>(); | ||
Matcher variableMatcher = variablePattern.matcher(gradleFileContents); | ||
|
||
while (variableMatcher.find()) { | ||
variables.put(variableMatcher.group(1), variableMatcher.group(2)); | ||
} | ||
|
||
return variables; | ||
} | ||
|
||
/** | ||
* Interpolates variables in the given text using the provided variable map. | ||
* | ||
* @param text Text to interpolate | ||
* @param variables Map of variable names to their values | ||
* @return Interpolated text | ||
*/ | ||
private static String interpolate(String text, Map<String, String> variables) { | ||
for (Map.Entry<String, String> entry : variables.entrySet()) { | ||
text = text.replace("$" + entry.getKey(), entry.getValue()); | ||
} | ||
return text; | ||
} | ||
|
||
/** | ||
* Utility method to extract the first captured group from matching the given regex. | ||
* | ||
* @param text Text to search | ||
* @param regex Regex with a capturing group | ||
* @return The first captured group, or null if not found | ||
*/ | ||
private static String extractValue(String text, String regex) { | ||
Pattern pattern = Pattern.compile(regex); | ||
Matcher matcher = pattern.matcher(text); | ||
if (matcher.find()) { | ||
return matcher.group(1); | ||
} | ||
return null; | ||
} | ||
|
||
private GradleParser() {} | ||
} |
91 changes: 91 additions & 0 deletions
91
...ion-docs/src/main/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.docs; | ||
|
||
import static io.opentelemetry.instrumentation.docs.GradleParser.parseMuzzleBlock; | ||
|
||
import io.opentelemetry.instrumentation.docs.utils.FileManager; | ||
import io.opentelemetry.instrumentation.docs.utils.InstrumentationPath; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
class InstrumentationAnalyzer { | ||
|
||
private final FileManager fileSearch; | ||
|
||
InstrumentationAnalyzer(FileManager fileSearch) { | ||
this.fileSearch = fileSearch; | ||
} | ||
|
||
/** | ||
* Converts a list of InstrumentationPath objects into a list of InstrumentationEntity objects. | ||
* Each InstrumentationEntity represents a unique combination of group, namespace, and | ||
* instrumentation name. The types of instrumentation (e.g., library, javaagent) are aggregated | ||
* into a list within each entity. | ||
* | ||
* @param paths the list of InstrumentationPath objects to be converted | ||
* @return a list of InstrumentationEntity objects with aggregated types | ||
*/ | ||
public static List<InstrumentationEntity> convertToEntities(List<InstrumentationPath> paths) { | ||
Map<String, InstrumentationEntity> entityMap = new HashMap<>(); | ||
|
||
for (InstrumentationPath path : paths) { | ||
String key = path.group() + ":" + path.namespace() + ":" + path.instrumentationName(); | ||
if (!entityMap.containsKey(key)) { | ||
entityMap.put( | ||
key, | ||
new InstrumentationEntity( | ||
path.srcPath().replace("/javaagent", "").replace("/library", ""), | ||
path.instrumentationName(), | ||
path.namespace(), | ||
path.group(), | ||
new ArrayList<>())); | ||
} | ||
entityMap.get(key).getTypes().add(path.type()); | ||
} | ||
|
||
return new ArrayList<>(entityMap.values()); | ||
} | ||
|
||
/** | ||
* Analyzes the given root directory to find all instrumentation paths and then analyze them. - | ||
* Extracts version information from each instrumentation's build.gradle file. | ||
* | ||
* @return a list of InstrumentationEntity objects with target versions | ||
*/ | ||
List<InstrumentationEntity> analyze() { | ||
List<InstrumentationPath> paths = fileSearch.getInstrumentationPaths(); | ||
List<InstrumentationEntity> entities = convertToEntities(paths); | ||
|
||
for (InstrumentationEntity entity : entities) { | ||
List<String> gradleFiles = fileSearch.findBuildGradleFiles(entity.getSrcPath()); | ||
analyzeVersions(gradleFiles, entity); | ||
} | ||
return entities; | ||
} | ||
|
||
void analyzeVersions(List<String> files, InstrumentationEntity entity) { | ||
Map<InstrumentationType, Set<String>> versions = new HashMap<>(); | ||
for (String file : files) { | ||
String fileContents = fileSearch.readFileToString(file); | ||
|
||
if (file.contains("/javaagent/")) { | ||
Set<String> results = parseMuzzleBlock(fileContents, InstrumentationType.JAVAAGENT); | ||
versions | ||
.computeIfAbsent(InstrumentationType.JAVAAGENT, k -> new HashSet<>()) | ||
.addAll(results); | ||
} else if (file.contains("/library/")) { | ||
Set<String> results = parseMuzzleBlock(fileContents, InstrumentationType.LIBRARY); | ||
versions.computeIfAbsent(InstrumentationType.LIBRARY, k -> new HashSet<>()).addAll(results); | ||
} | ||
} | ||
entity.setTargetVersions(versions); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.