Skip to content

Commit 975a4fb

Browse files
committed
documentation site disable list audit
1 parent 744474a commit 975a4fb

File tree

5 files changed

+394
-0
lines changed

5 files changed

+394
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: opentelemetry.io documentation disable list audit
2+
3+
on:
4+
schedule:
5+
- cron: "30 1 * * *" # daily at 1:30 UTC
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
crawl:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
16+
17+
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
18+
with:
19+
distribution: temurin
20+
java-version: 17
21+
22+
- name: Set up gradle
23+
uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
24+
25+
- name: Run Doc site audit
26+
run: ./gradlew :instrumentation-docs:docSiteAudit

instrumentation-docs/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,12 @@ tasks {
2626
mainClass.set("io.opentelemetry.instrumentation.docs.DocGeneratorApplication")
2727
classpath(sourceSets["main"].runtimeClasspath)
2828
}
29+
30+
val docSiteAudit by registering(JavaExec::class) {
31+
dependsOn(classes)
32+
33+
systemProperty("basePath", project.rootDir)
34+
mainClass.set("io.opentelemetry.instrumentation.docs.DocSynchronization")
35+
classpath(sourceSets["main"].runtimeClasspath)
36+
}
2937
}

instrumentation-docs/readme.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,17 @@ data will be excluded from git and just generated on demand.
196196

197197
Each file has a `when` value along with the list of metrics that indicates whether the telemetry is
198198
emitted by default or via a configuration option.
199+
200+
# Doc Synchronization
201+
202+
The documentation site has a section that lists all the instrumentations in the context of
203+
documenting how to disable them.
204+
205+
We have a class `DocSynchronization` that runs a check against our instrumentation-list.yaml file to
206+
identify when we have missing entries, so we know to go update them.
207+
208+
You can run this via:
209+
210+
`./gradlew :instrumentation-docs:docSiteAudit`
211+
212+
This is setup to run nightly in a github action.
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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 java.lang.System.exit;
9+
10+
import io.opentelemetry.instrumentation.docs.utils.FileManager;
11+
import java.io.IOException;
12+
import java.net.URI;
13+
import java.net.http.HttpClient;
14+
import java.net.http.HttpRequest;
15+
import java.net.http.HttpResponse;
16+
import java.util.ArrayList;
17+
import java.util.Arrays;
18+
import java.util.HashSet;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.Objects;
22+
import java.util.Set;
23+
import java.util.TreeSet;
24+
import java.util.logging.Logger;
25+
import javax.annotation.Nullable;
26+
import org.yaml.snakeyaml.Yaml;
27+
28+
/**
29+
* This class is responsible for auditing and synchronizing documentation using the instrumentation
30+
* list yaml.
31+
*/
32+
public class DocSynchronization {
33+
private static final Logger logger = Logger.getLogger(DocSynchronization.class.getName());
34+
35+
private static final String DOCUMENTATION_DISABLE_LIST =
36+
"https://raw.githubusercontent.com/open-telemetry/opentelemetry.io/refs/heads/main/content/en/docs/zero-code/java/agent/disable.md";
37+
38+
// Used for consolidating instrumentation groups where we override the key with the value
39+
private static final Map<String, String> INSTRUMENTATION_DISABLE_OVERRIDES =
40+
Map.of("akka-actor-fork-join", "akka-actor");
41+
42+
private static final List<String> INSTRUMENTATION_EXCLUSIONS =
43+
List.of("resources", "spring-boot-resources");
44+
45+
private final HttpClient client;
46+
47+
public DocSynchronization(HttpClient client) {
48+
this.client = client;
49+
}
50+
51+
/**
52+
* Retrieves contents of the disable page from the main branch of the documentation site.
53+
*
54+
* @return the file content as a string
55+
*/
56+
private String getDocumentationDisableList() throws IOException, InterruptedException {
57+
HttpRequest request =
58+
HttpRequest.newBuilder().uri(URI.create(DOCUMENTATION_DISABLE_LIST)).build();
59+
60+
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
61+
if (response.statusCode() >= 200 && response.statusCode() < 300) {
62+
return response.body();
63+
}
64+
throw new IOException("Failed to fetch instrumentation list: " + response);
65+
}
66+
67+
@SuppressWarnings("unchecked")
68+
public static List<String> parseInstrumentationList(String fileContent) {
69+
List<String> instrumentationList = new ArrayList<>();
70+
Yaml yaml = new Yaml();
71+
Map<String, Object> data = yaml.load(fileContent);
72+
73+
if (data != null && data.get("libraries") instanceof Map) {
74+
Map<String, List<Map<String, Object>>> libraries =
75+
(Map<String, List<Map<String, Object>>>) data.get("libraries");
76+
for (List<Map<String, Object>> libraryGroup : libraries.values()) {
77+
for (Map<String, Object> instrumentation : libraryGroup) {
78+
if (instrumentation.get("name") instanceof String) {
79+
instrumentationList.add((String) instrumentation.get("name"));
80+
}
81+
}
82+
}
83+
}
84+
return instrumentationList;
85+
}
86+
87+
/**
88+
* Identifies missing items in the instrumentation list that are not present in the documentation
89+
* disable list. Takes into account any overrides specified in INSTRUMENTATION_DISABLE_OVERRIDES
90+
* and excludes items listed in INSTRUMENTATION_EXCLUSIONS.
91+
*
92+
* @param documentationDisabledList a list of items that are documented
93+
* @param instrumentationList a list of instrumentations from the instrumentation list
94+
* @return a list of missing items that should be documented
95+
*/
96+
public static List<String> identifyMissingItems(
97+
List<String> documentationDisabledList, List<String> instrumentationList) {
98+
Set<String> documentationDisabledSet = new HashSet<>(documentationDisabledList);
99+
100+
Set<String> sanitizedInstrumentationItems = new TreeSet<>();
101+
for (String item : instrumentationList) {
102+
sanitizedInstrumentationItems.add(item.replaceFirst("-[0-9].*$", ""));
103+
}
104+
105+
List<String> missingItems = new ArrayList<>();
106+
for (String item : sanitizedInstrumentationItems) {
107+
if (INSTRUMENTATION_EXCLUSIONS.contains(item)) {
108+
continue; // Skip excluded items
109+
}
110+
String itemToCheck = INSTRUMENTATION_DISABLE_OVERRIDES.getOrDefault(item, item);
111+
boolean found = false;
112+
for (String disabledItem : documentationDisabledSet) {
113+
if (itemToCheck.startsWith(disabledItem)) {
114+
found = true;
115+
break;
116+
}
117+
}
118+
if (!found) {
119+
missingItems.add(item);
120+
}
121+
}
122+
return missingItems;
123+
}
124+
125+
/**
126+
* Retrieves the instrumentation list yaml file.
127+
*
128+
* @return a string representation of the instrumentation list
129+
*/
130+
@Nullable
131+
private static String getInstrumentationList() {
132+
// Identify path to repo so we can use absolute paths
133+
String baseRepoPath = System.getProperty("basePath");
134+
if (baseRepoPath == null) {
135+
baseRepoPath = "./";
136+
} else {
137+
baseRepoPath += "/";
138+
}
139+
140+
String file = baseRepoPath + "docs/instrumentation-list.yaml";
141+
return FileManager.readFileToString(file);
142+
}
143+
144+
/**
145+
* Parses the documentation disabled list from the file content and turns it into a list of
146+
* instrumentation names.
147+
*
148+
* @param fileContent the content of the disable.md documentation file
149+
* @return a list of instrumentation names that are documented
150+
*/
151+
public static List<String> parseDocumentationDisabledList(String fileContent) {
152+
List<String> instrumentationList = new ArrayList<>();
153+
String[] lines = fileContent.split("\\R");
154+
for (String line : lines) {
155+
if (line.trim().startsWith("|")) {
156+
String[] parts = line.split("\\|");
157+
if (parts.length > 2) {
158+
String potentialName = parts[2].trim();
159+
if (potentialName.startsWith("`") && potentialName.endsWith("`")) {
160+
String name = potentialName.substring(1, potentialName.length() - 1);
161+
instrumentationList.add(name);
162+
}
163+
}
164+
}
165+
}
166+
return instrumentationList;
167+
}
168+
169+
public static void main(String[] args) {
170+
HttpClient client = HttpClient.newHttpClient();
171+
DocSynchronization synchronization = new DocSynchronization(client);
172+
173+
try {
174+
String content = synchronization.getDocumentationDisableList();
175+
List<String> disabledList = parseDocumentationDisabledList(content);
176+
177+
String instrumentationListContent = Objects.requireNonNull(getInstrumentationList());
178+
List<String> instrumentationList = parseInstrumentationList(instrumentationListContent);
179+
180+
List<String> missingItems = identifyMissingItems(disabledList, instrumentationList);
181+
182+
if (missingItems.isEmpty()) {
183+
logger.info("No missing items found.");
184+
} else {
185+
StringBuilder sb = new StringBuilder();
186+
sb.append("Missing Instrumentation List (")
187+
.append(missingItems.size())
188+
.append(" item(s) missing):\n");
189+
missingItems.forEach(item -> sb.append(" - ").append(item).append("\n"));
190+
logger.severe(sb.toString());
191+
exit(1);
192+
}
193+
194+
} catch (IOException | InterruptedException e) {
195+
logger.severe("Error fetching instrumentation list: " + e.getMessage());
196+
logger.severe(Arrays.toString(e.getStackTrace()));
197+
exit(1);
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)