Skip to content

Commit a6f96a4

Browse files
authored
Documentation Synchronization Audit job (#14697)
1 parent 1b2ae93 commit a6f96a4

File tree

10 files changed

+942
-315
lines changed

10 files changed

+942
-315
lines changed

.github/workflows/documentation-disable-list-audit.yml renamed to .github/workflows/documentation-synchronization-audit.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: opentelemetry.io documentation disable list audit
1+
name: Documentation Synchronization Audit (opentelemetry.io)
22

33
on:
44
schedule:
@@ -11,6 +11,8 @@ permissions:
1111
jobs:
1212
crawl:
1313
runs-on: ubuntu-latest
14+
outputs:
15+
audit-output: ${{ steps.audit.outputs.AUDIT_OUTPUT }}
1416
steps:
1517
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
1618

@@ -26,7 +28,20 @@ jobs:
2628
run: ./gradlew :instrumentation-docs:runAnalysis
2729

2830
- name: Run doc site audit
29-
run: ./gradlew :instrumentation-docs:docSiteAudit
31+
id: audit
32+
run: |
33+
if ! output=$(./gradlew :instrumentation-docs:docSiteAudit 2>&1); then
34+
echo "AUDIT_FAILED=true" >> $GITHUB_OUTPUT
35+
echo "AUDIT_OUTPUT<<EOF" >> $GITHUB_OUTPUT
36+
# Extract only the content between our custom markers
37+
echo "$output" | sed -n '/=== AUDIT_FAILURE_START ===/,/=== AUDIT_FAILURE_END ===/p' | \
38+
sed '/=== AUDIT_FAILURE_START ===/d' | \
39+
sed '/=== AUDIT_FAILURE_END ===/d' >> $GITHUB_OUTPUT
40+
echo "EOF" >> $GITHUB_OUTPUT
41+
exit 1
42+
else
43+
echo "AUDIT_FAILED=false" >> $GITHUB_OUTPUT
44+
fi
3045
3146
workflow-notification:
3247
permissions:
@@ -38,3 +53,4 @@ jobs:
3853
uses: ./.github/workflows/reusable-workflow-notification.yml
3954
with:
4055
success: ${{ needs.crawl.result == 'success' }}
56+
failure-details: ${{ needs.crawl.outputs.audit-output }}

.github/workflows/reusable-workflow-notification.yml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ on:
88
success:
99
type: boolean
1010
required: true
11+
failure-details:
12+
type: string
13+
required: false
1114

1215
permissions:
1316
contents: read
@@ -31,14 +34,19 @@ jobs:
3134
echo $number
3235
echo ${{ inputs.success }}
3336
37+
# Prepare the issue body with failure details if available
38+
if [[ -n "${{ inputs.failure-details }}" ]]; then
39+
issue_body="See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."$'\n\n'"## Failure Details"$'\n\n'"${{ inputs.failure-details }}"
40+
else
41+
issue_body="See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."
42+
fi
43+
3444
if [[ $number ]]; then
3545
if [[ "${{ inputs.success }}" == "true" ]]; then
3646
gh issue close $number
3747
else
38-
gh issue comment $number \
39-
--body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."
48+
gh issue comment $number --body "$issue_body"
4049
fi
4150
elif [[ "${{ inputs.success }}" == "false" ]]; then
42-
gh issue create --title "Workflow failed: $GITHUB_WORKFLOW (#$GITHUB_RUN_NUMBER)" \
43-
--body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."
51+
gh issue create --title "Workflow failed: $GITHUB_WORKFLOW (#$GITHUB_RUN_NUMBER)" --body "$issue_body"
4452
fi

docs/contributing/documenting-instrumentation.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ All of our instrumentation modules are listed on the opentelemetry.io website in
227227
The [Supported Libraries](https://opentelemetry.io/docs/zero-code/java/agent/supported-libraries/)
228228
page lists all the library instrumentations that are included in the OpenTelemetry Java agent. It
229229
mostly mirrors the information from the [supported libraries](../supported-libraries.md) page in
230-
this repo, and should be updated when adding or removing library instrumentations.
230+
this repo, and should be updated when adding or removing library instrumentations. There is a
231+
[Github action](../../.github/workflows/documentation-synchronization-audit.yml) that runs nightly
232+
to check for any missing instrumentations, and will open an issue if any are found.
231233

232234
This page may be automatically generated in the future, but for now it is manually maintained.
233235

@@ -238,5 +240,5 @@ page lists the instrumentations in the context of the keys needed for using
238240
the `otel.instrumentation.[name].enabled` configuration.
239241

240242
All new instrumentations should be added to this list. There is a
241-
[Github action](../../.github/workflows/documentation-disable-list-audit.yml) that runs nightly to check
243+
[Github action](../../.github/workflows/documentation-synchronization-audit.yml) that runs nightly to check
242244
for any missing instrumentations, and will open an issue if any are found.

instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java

Lines changed: 52 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -7,190 +7,81 @@
77

88
import static java.lang.System.exit;
99

10-
import io.opentelemetry.instrumentation.docs.utils.FileManager;
10+
import io.opentelemetry.instrumentation.docs.auditors.DocumentationAuditor;
11+
import io.opentelemetry.instrumentation.docs.auditors.SupportedLibrariesAuditor;
12+
import io.opentelemetry.instrumentation.docs.auditors.SuppressionListAuditor;
1113
import java.io.IOException;
12-
import java.net.URI;
1314
import java.net.http.HttpClient;
14-
import java.net.http.HttpRequest;
15-
import java.net.http.HttpResponse;
16-
import java.util.ArrayList;
1715
import java.util.Arrays;
18-
import java.util.HashSet;
1916
import java.util.List;
20-
import java.util.Map;
21-
import java.util.Objects;
22-
import java.util.Set;
23-
import java.util.TreeSet;
17+
import java.util.Optional;
2418
import java.util.logging.Logger;
25-
import javax.annotation.Nullable;
26-
import org.yaml.snakeyaml.Yaml;
2719

2820
/**
29-
* This class is responsible for auditing and synchronizing documentation using the instrumentation
30-
* list yaml.
21+
* This class is responsible for auditing and synchronizing documentation between the source of
22+
* truth (this repo) and the opentelemetry.io site.
3123
*/
3224
public class DocSynchronization {
3325
private static final Logger logger = Logger.getLogger(DocSynchronization.class.getName());
3426

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";
27+
private static final List<DocumentationAuditor> AUDITORS =
28+
List.of(new SuppressionListAuditor(), new SupportedLibrariesAuditor());
3729

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 DocSynchronization() {}
46-
47-
/**
48-
* Retrieves contents of the disable page from the main branch of the documentation site.
49-
*
50-
* @return the file content as a string
51-
*/
52-
private static String getDocumentationDisableList(HttpClient client)
53-
throws IOException, InterruptedException {
54-
HttpRequest request =
55-
HttpRequest.newBuilder().uri(URI.create(DOCUMENTATION_DISABLE_LIST)).build();
56-
57-
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
58-
if (response.statusCode() >= 200 && response.statusCode() < 300) {
59-
return response.body();
60-
}
61-
throw new IOException("Failed to fetch instrumentation list: " + response);
62-
}
63-
64-
@SuppressWarnings("unchecked")
65-
public static List<String> parseInstrumentationList(String fileContent) {
66-
List<String> instrumentationList = new ArrayList<>();
67-
Yaml yaml = new Yaml();
68-
Map<String, Object> data = yaml.load(fileContent);
30+
public static void main(String[] args) {
31+
HttpClient client = HttpClient.newHttpClient();
6932

70-
if (data != null && data.get("libraries") instanceof Map) {
71-
Map<String, List<Map<String, Object>>> libraries =
72-
(Map<String, List<Map<String, Object>>>) data.get("libraries");
73-
for (List<Map<String, Object>> libraryGroup : libraries.values()) {
74-
for (Map<String, Object> instrumentation : libraryGroup) {
75-
if (instrumentation.get("name") instanceof String) {
76-
instrumentationList.add((String) instrumentation.get("name"));
33+
try {
34+
boolean hasFailures = false;
35+
StringBuilder combinedMessage = new StringBuilder();
36+
37+
for (DocumentationAuditor auditor : AUDITORS) {
38+
try {
39+
logger.info("Running " + auditor.getAuditorName() + "...");
40+
Optional<String> result = auditor.performAudit(client);
41+
42+
if (result.isPresent()) {
43+
hasFailures = true;
44+
if (!combinedMessage.isEmpty()) {
45+
combinedMessage.append("\n\n");
46+
}
47+
combinedMessage.append(result.get());
7748
}
78-
}
79-
}
80-
}
81-
return instrumentationList;
82-
}
83-
84-
/**
85-
* Identifies missing items in the instrumentation list that are not present in the documentation
86-
* disable list. Takes into account any overrides specified in INSTRUMENTATION_DISABLE_OVERRIDES
87-
* and excludes items listed in INSTRUMENTATION_EXCLUSIONS.
88-
*
89-
* @param documentationDisabledList a list of items that are documented
90-
* @param instrumentationList a list of instrumentations from the instrumentation list
91-
* @return a list of missing items that should be documented
92-
*/
93-
public static List<String> identifyMissingItems(
94-
List<String> documentationDisabledList, List<String> instrumentationList) {
95-
Set<String> documentationDisabledSet = new HashSet<>(documentationDisabledList);
96-
97-
Set<String> sanitizedInstrumentationItems = new TreeSet<>();
98-
for (String item : instrumentationList) {
99-
sanitizedInstrumentationItems.add(item.replaceFirst("-[0-9].*$", ""));
100-
}
101-
102-
List<String> missingItems = new ArrayList<>();
103-
for (String item : sanitizedInstrumentationItems) {
104-
if (INSTRUMENTATION_EXCLUSIONS.contains(item)) {
105-
continue; // Skip excluded items
106-
}
107-
String itemToCheck = INSTRUMENTATION_DISABLE_OVERRIDES.getOrDefault(item, item);
108-
boolean found = false;
109-
for (String disabledItem : documentationDisabledSet) {
110-
if (itemToCheck.startsWith(disabledItem)) {
111-
found = true;
112-
break;
113-
}
114-
}
115-
if (!found) {
116-
missingItems.add(item);
117-
}
118-
}
119-
return missingItems;
120-
}
121-
122-
/**
123-
* Retrieves the instrumentation list yaml file.
124-
*
125-
* @return a string representation of the instrumentation list
126-
*/
127-
@Nullable
128-
private static String getInstrumentationList() {
129-
// Identify path to repo so we can use absolute paths
130-
String baseRepoPath = System.getProperty("basePath");
131-
if (baseRepoPath == null) {
132-
baseRepoPath = "./";
133-
} else {
134-
baseRepoPath += "/";
135-
}
136-
137-
String file = baseRepoPath + "docs/instrumentation-list.yaml";
138-
return FileManager.readFileToString(file);
139-
}
140-
141-
/**
142-
* Parses the documentation disabled list from the file content and turns it into a list of
143-
* instrumentation names.
144-
*
145-
* @param fileContent the content of the disable.md documentation file
146-
* @return a list of instrumentation names that are documented
147-
*/
148-
public static List<String> parseDocumentationDisabledList(String fileContent) {
149-
List<String> instrumentationList = new ArrayList<>();
150-
String[] lines = fileContent.split("\\R");
151-
for (String line : lines) {
152-
if (line.trim().startsWith("|")) {
153-
String[] parts = line.split("\\|");
154-
if (parts.length > 2) {
155-
String potentialName = parts[2].trim();
156-
if (potentialName.startsWith("`") && potentialName.endsWith("`")) {
157-
String name = potentialName.substring(1, potentialName.length() - 1);
158-
instrumentationList.add(name);
49+
} catch (IOException | InterruptedException | RuntimeException e) {
50+
logger.severe("Error running " + auditor.getAuditorName() + ": " + e.getMessage());
51+
hasFailures = true;
52+
if (!combinedMessage.isEmpty()) {
53+
combinedMessage.append("\n\n");
15954
}
55+
combinedMessage
56+
.append("Error in ")
57+
.append(auditor.getAuditorName())
58+
.append(": ")
59+
.append(e.getMessage());
16060
}
16161
}
162-
}
163-
return instrumentationList;
164-
}
16562

166-
public static void main(String[] args) {
167-
HttpClient client = HttpClient.newHttpClient();
168-
169-
try {
170-
String content = getDocumentationDisableList(client);
171-
List<String> disabledList = parseDocumentationDisabledList(content);
172-
173-
String instrumentationListContent = Objects.requireNonNull(getInstrumentationList());
174-
List<String> instrumentationList = parseInstrumentationList(instrumentationListContent);
175-
176-
List<String> missingItems = identifyMissingItems(disabledList, instrumentationList);
177-
178-
if (missingItems.isEmpty()) {
179-
logger.info("No missing items found.");
180-
} else {
181-
StringBuilder sb = new StringBuilder();
182-
sb.append("Missing Instrumentation List (")
183-
.append(missingItems.size())
184-
.append(" item(s) missing):\n");
185-
missingItems.forEach(item -> sb.append(" - ").append(item).append("\n"));
186-
logger.severe(sb.toString());
63+
if (hasFailures) {
64+
// Add custom markers and "How to Fix" section for GitHub workflow extraction
65+
StringBuilder finalMessage = new StringBuilder();
66+
finalMessage.append("=== AUDIT_FAILURE_START ===\n");
67+
finalMessage.append(combinedMessage.toString());
68+
finalMessage.append("\n\n## How to Fix\n\n");
69+
finalMessage.append(
70+
"For guidance on updating the OpenTelemetry.io documentation, see: [Documenting Instrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/contributing/documenting-instrumentation.md#opentelemetryio)");
71+
finalMessage.append("\n=== AUDIT_FAILURE_END ===");
72+
73+
logger.severe(finalMessage.toString());
18774
exit(1);
75+
} else {
76+
logger.info("All documentation audits passed successfully.");
18877
}
18978

190-
} catch (IOException | InterruptedException e) {
191-
logger.severe("Error fetching instrumentation list: " + e.getMessage());
79+
} catch (RuntimeException e) {
80+
logger.severe("Error running documentation audits: " + e.getMessage());
19281
logger.severe(Arrays.toString(e.getStackTrace()));
19382
exit(1);
19483
}
19584
}
85+
86+
private DocSynchronization() {}
19687
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.docs.auditors;
7+
8+
import java.io.IOException;
9+
import java.net.http.HttpClient;
10+
import java.util.Optional;
11+
12+
/**
13+
* Base interface for auditing documentation synchronization between the instrumentation repository
14+
* and the OpenTelemetry.io website.
15+
*/
16+
public interface DocumentationAuditor {
17+
18+
/**
19+
* Performs an audit by comparing local instrumentation data with remote documentation.
20+
*
21+
* @param client HTTP client for making remote requests
22+
* @return Optional.empty() if successful, or Optional.of(errorMessage) if there are issues
23+
* @throws IOException if there's an error fetching remote content
24+
* @throws InterruptedException if the HTTP request is interrupted
25+
*/
26+
Optional<String> performAudit(HttpClient client) throws IOException, InterruptedException;
27+
28+
/**
29+
* Returns the name of this auditor for logging and reporting purposes.
30+
*
31+
* @return auditor name
32+
*/
33+
String getAuditorName();
34+
}

0 commit comments

Comments
 (0)