Skip to content
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

Automatically Generate Instrumentation Documentation #13449

Merged
merged 9 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,598 changes: 1,598 additions & 0 deletions docs/instrumentation-list.yaml

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions instrumentation-docs/build.gradle.kts
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)
}
}
63 changes: 63 additions & 0 deletions instrumentation-docs/readme.md
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?
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() {}
}
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() {}
}
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);
}
}
Loading
Loading