From f342b5189448f689990572b0593f5b2194db553e Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Wed, 17 Sep 2025 13:24:59 -0400 Subject: [PATCH 1/6] Doc auditing --- .../documentation-disable-list-audit.yml | 27 ++- .../reusable-workflow-notification.yml | 16 +- .../docs/DocSynchronization.java | 207 +++++------------- .../docs/auditors/DocumentationAuditor.java | 34 +++ .../auditors/SupportedLibrariesAuditor.java | 194 ++++++++++++++++ .../docs/auditors/SuppressionListAuditor.java | 184 ++++++++++++++++ .../SupportedLibrariesAuditorTest.java | 172 +++++++++++++++ .../auditors/SuppressionListAuditorTest.java | 160 ++++++++++++++ 8 files changed, 835 insertions(+), 159 deletions(-) create mode 100644 instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/DocumentationAuditor.java create mode 100644 instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditor.java create mode 100644 instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditor.java create mode 100644 instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditorTest.java create mode 100644 instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditorTest.java diff --git a/.github/workflows/documentation-disable-list-audit.yml b/.github/workflows/documentation-disable-list-audit.yml index fd43443099d0..8fc5939892bd 100644 --- a/.github/workflows/documentation-disable-list-audit.yml +++ b/.github/workflows/documentation-disable-list-audit.yml @@ -11,6 +11,8 @@ permissions: jobs: crawl: runs-on: ubuntu-latest + outputs: + audit-output: ${{ steps.audit.outputs.AUDIT_OUTPUT }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -26,7 +28,29 @@ jobs: run: ./gradlew :instrumentation-docs:runAnalysis - name: Run doc site audit - run: ./gradlew :instrumentation-docs:docSiteAudit + id: audit + run: | + # Capture both stdout and stderr, and preserve exit code + if ! ./gradlew :instrumentation-docs:docSiteAudit > audit_output.txt 2>&1; then + echo "AUDIT_FAILED=true" >> $GITHUB_OUTPUT + echo "AUDIT_OUTPUT<> $GITHUB_OUTPUT + # Extract only the audit error messages between SEVERE: and FAILURE: + sed -n '/SEVERE: /,/^FAILURE:/p' audit_output.txt | \ + sed '/^FAILURE:/d' | \ + sed 's/^.*SEVERE: //' | \ + sed '/^> Task/d' | \ + sed '/^gradle\/actions:/d' | \ + sed '/^$/d' >> $GITHUB_OUTPUT + # Add guidance on how to fix the issues + echo "" >> $GITHUB_OUTPUT + echo "## How to Fix" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_OUTPUT + echo "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)" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + exit 1 + else + echo "AUDIT_FAILED=false" >> $GITHUB_OUTPUT + fi workflow-notification: permissions: @@ -38,3 +62,4 @@ jobs: uses: ./.github/workflows/reusable-workflow-notification.yml with: success: ${{ needs.crawl.result == 'success' }} + failure-details: ${{ needs.crawl.outputs.audit-output }} diff --git a/.github/workflows/reusable-workflow-notification.yml b/.github/workflows/reusable-workflow-notification.yml index 61e8d6267ccb..d647453c0e2c 100644 --- a/.github/workflows/reusable-workflow-notification.yml +++ b/.github/workflows/reusable-workflow-notification.yml @@ -8,6 +8,9 @@ on: success: type: boolean required: true + failure-details: + type: string + required: false permissions: contents: read @@ -31,14 +34,19 @@ jobs: echo $number echo ${{ inputs.success }} + # Prepare the issue body with failure details if available + if [[ -n "${{ inputs.failure-details }}" ]]; then + issue_body="See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."$'\n\n'"## Failure Details"$'\n\n'"$(echo '```')"$'\n'"${{ inputs.failure-details }}"$'\n'"$(echo '```')" + else + issue_body="See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." + fi + if [[ $number ]]; then if [[ "${{ inputs.success }}" == "true" ]]; then gh issue close $number else - gh issue comment $number \ - --body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." + gh issue comment $number --body "$issue_body" fi elif [[ "${{ inputs.success }}" == "false" ]]; then - gh issue create --title "Workflow failed: $GITHUB_WORKFLOW (#$GITHUB_RUN_NUMBER)" \ - --body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." + gh issue create --title "Workflow failed: $GITHUB_WORKFLOW (#$GITHUB_RUN_NUMBER)" --body "$issue_body" fi diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java index 343a614e8c30..c01cba4f3dce 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java @@ -7,190 +7,89 @@ import static java.lang.System.exit; -import io.opentelemetry.instrumentation.docs.utils.FileManager; +import io.opentelemetry.instrumentation.docs.auditors.DocumentationAuditor; +import io.opentelemetry.instrumentation.docs.auditors.SupportedLibrariesAuditor; +import io.opentelemetry.instrumentation.docs.auditors.SuppressionListAuditor; import java.io.IOException; -import java.net.URI; import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; +import java.util.Optional; import java.util.logging.Logger; -import javax.annotation.Nullable; -import org.yaml.snakeyaml.Yaml; /** - * This class is responsible for auditing and synchronizing documentation using the instrumentation - * list yaml. + * This class is responsible for auditing and synchronizing documentation between the source of + * truth (this repo) and the opentelemetry.io site. */ public class DocSynchronization { private static final Logger logger = Logger.getLogger(DocSynchronization.class.getName()); - private static final String DOCUMENTATION_DISABLE_LIST = - "https://raw.githubusercontent.com/open-telemetry/opentelemetry.io/refs/heads/main/content/en/docs/zero-code/java/agent/disable.md"; + private static final List AUDITORS = + List.of(new SuppressionListAuditor(), new SupportedLibrariesAuditor()); - // Used for consolidating instrumentation groups where we override the key with the value - private static final Map INSTRUMENTATION_DISABLE_OVERRIDES = - Map.of("akka-actor-fork-join", "akka-actor"); - - private static final List INSTRUMENTATION_EXCLUSIONS = - List.of("resources", "spring-boot-resources"); - - private DocSynchronization() {} - - /** - * Retrieves contents of the disable page from the main branch of the documentation site. - * - * @return the file content as a string - */ - private static String getDocumentationDisableList(HttpClient client) - throws IOException, InterruptedException { - HttpRequest request = - HttpRequest.newBuilder().uri(URI.create(DOCUMENTATION_DISABLE_LIST)).build(); - - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() >= 200 && response.statusCode() < 300) { - return response.body(); - } - throw new IOException("Failed to fetch instrumentation list: " + response); - } - - @SuppressWarnings("unchecked") + // visible for testing public static List parseInstrumentationList(String fileContent) { - List instrumentationList = new ArrayList<>(); - Yaml yaml = new Yaml(); - Map data = yaml.load(fileContent); + return SuppressionListAuditor.parseInstrumentationList(fileContent); + } - if (data != null && data.get("libraries") instanceof Map) { - Map>> libraries = - (Map>>) data.get("libraries"); - for (List> libraryGroup : libraries.values()) { - for (Map instrumentation : libraryGroup) { - if (instrumentation.get("name") instanceof String) { - instrumentationList.add((String) instrumentation.get("name")); - } - } - } - } - return instrumentationList; + // visible for testing + public static List parseDocumentationDisabledList(String fileContent) { + return SuppressionListAuditor.parseDocumentationDisabledList(fileContent); } - /** - * Identifies missing items in the instrumentation list that are not present in the documentation - * disable list. Takes into account any overrides specified in INSTRUMENTATION_DISABLE_OVERRIDES - * and excludes items listed in INSTRUMENTATION_EXCLUSIONS. - * - * @param documentationDisabledList a list of items that are documented - * @param instrumentationList a list of instrumentations from the instrumentation list - * @return a list of missing items that should be documented - */ + // visible for testing public static List identifyMissingItems( List documentationDisabledList, List instrumentationList) { - Set documentationDisabledSet = new HashSet<>(documentationDisabledList); - - Set sanitizedInstrumentationItems = new TreeSet<>(); - for (String item : instrumentationList) { - sanitizedInstrumentationItems.add(item.replaceFirst("-[0-9].*$", "")); - } - - List missingItems = new ArrayList<>(); - for (String item : sanitizedInstrumentationItems) { - if (INSTRUMENTATION_EXCLUSIONS.contains(item)) { - continue; // Skip excluded items - } - String itemToCheck = INSTRUMENTATION_DISABLE_OVERRIDES.getOrDefault(item, item); - boolean found = false; - for (String disabledItem : documentationDisabledSet) { - if (itemToCheck.startsWith(disabledItem)) { - found = true; - break; - } - } - if (!found) { - missingItems.add(item); - } - } - return missingItems; - } - - /** - * Retrieves the instrumentation list yaml file. - * - * @return a string representation of the instrumentation list - */ - @Nullable - private static String getInstrumentationList() { - // Identify path to repo so we can use absolute paths - String baseRepoPath = System.getProperty("basePath"); - if (baseRepoPath == null) { - baseRepoPath = "./"; - } else { - baseRepoPath += "/"; - } - - String file = baseRepoPath + "docs/instrumentation-list.yaml"; - return FileManager.readFileToString(file); - } - - /** - * Parses the documentation disabled list from the file content and turns it into a list of - * instrumentation names. - * - * @param fileContent the content of the disable.md documentation file - * @return a list of instrumentation names that are documented - */ - public static List parseDocumentationDisabledList(String fileContent) { - List instrumentationList = new ArrayList<>(); - String[] lines = fileContent.split("\\R"); - for (String line : lines) { - if (line.trim().startsWith("|")) { - String[] parts = line.split("\\|"); - if (parts.length > 2) { - String potentialName = parts[2].trim(); - if (potentialName.startsWith("`") && potentialName.endsWith("`")) { - String name = potentialName.substring(1, potentialName.length() - 1); - instrumentationList.add(name); - } - } - } - } - return instrumentationList; + return SuppressionListAuditor.identifyMissingItems( + documentationDisabledList, instrumentationList); } public static void main(String[] args) { HttpClient client = HttpClient.newHttpClient(); try { - String content = getDocumentationDisableList(client); - List disabledList = parseDocumentationDisabledList(content); - - String instrumentationListContent = Objects.requireNonNull(getInstrumentationList()); - List instrumentationList = parseInstrumentationList(instrumentationListContent); - - List missingItems = identifyMissingItems(disabledList, instrumentationList); + boolean hasFailures = false; + StringBuilder combinedMessage = new StringBuilder(); + + for (DocumentationAuditor auditor : AUDITORS) { + try { + logger.info("Running " + auditor.getAuditorName() + "..."); + Optional result = auditor.performAudit(client); + + if (result.isPresent()) { + hasFailures = true; + if (!combinedMessage.isEmpty()) { + combinedMessage.append("\n\n"); + } + combinedMessage.append(result.get()); + } + } catch (IOException | InterruptedException | RuntimeException e) { + logger.severe("Error running " + auditor.getAuditorName() + ": " + e.getMessage()); + hasFailures = true; + if (!combinedMessage.isEmpty()) { + combinedMessage.append("\n\n"); + } + combinedMessage + .append("Error in ") + .append(auditor.getAuditorName()) + .append(": ") + .append(e.getMessage()); + } + } - if (missingItems.isEmpty()) { - logger.info("No missing items found."); - } else { - StringBuilder sb = new StringBuilder(); - sb.append("Missing Instrumentation List (") - .append(missingItems.size()) - .append(" item(s) missing):\n"); - missingItems.forEach(item -> sb.append(" - ").append(item).append("\n")); - logger.severe(sb.toString()); + if (hasFailures) { + logger.severe(combinedMessage.toString()); exit(1); + } else { + logger.info("All documentation audits passed successfully."); } - } catch (IOException | InterruptedException e) { - logger.severe("Error fetching instrumentation list: " + e.getMessage()); + } catch (RuntimeException e) { + logger.severe("Error running documentation audits: " + e.getMessage()); logger.severe(Arrays.toString(e.getStackTrace())); exit(1); } } + + private DocSynchronization() {} } diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/DocumentationAuditor.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/DocumentationAuditor.java new file mode 100644 index 000000000000..f703397ac186 --- /dev/null +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/DocumentationAuditor.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.auditors; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.util.Optional; + +/** + * Base interface for auditing documentation synchronization between the instrumentation repository + * and the OpenTelemetry.io website. + */ +public interface DocumentationAuditor { + + /** + * Performs an audit by comparing local instrumentation data with remote documentation. + * + * @param client HTTP client for making remote requests + * @return Optional.empty() if successful, or Optional.of(errorMessage) if there are issues + * @throws IOException if there's an error fetching remote content + * @throws InterruptedException if the HTTP request is interrupted + */ + Optional performAudit(HttpClient client) throws IOException, InterruptedException; + + /** + * Returns the name of this auditor for logging and reporting purposes. + * + * @return auditor name + */ + String getAuditorName(); +} diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditor.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditor.java new file mode 100644 index 000000000000..528f1f95ea60 --- /dev/null +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditor.java @@ -0,0 +1,194 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.auditors; + +import io.opentelemetry.instrumentation.docs.utils.FileManager; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +/** + * Audits the supported libraries list on the OpenTelemetry.io documentation site to ensure it + * matches the local supported-libraries.md file. + */ +public class SupportedLibrariesAuditor implements DocumentationAuditor { + + private static final String REMOTE_SUPPORTED_LIBRARIES_URL = + "https://raw.githubusercontent.com/open-telemetry/opentelemetry.io/refs/heads/main/content/en/docs/zero-code/java/agent/supported-libraries.md"; + + @Override + public Optional performAudit(HttpClient client) throws IOException, InterruptedException { + List localLibraries = parseLocalSupportedLibraries(); + List remoteLibraries = getRemoteSupportedLibraries(client); + List missingItems = identifyMissingItems(remoteLibraries, localLibraries); + + if (missingItems.isEmpty()) { + return Optional.empty(); + } + + StringBuilder sb = new StringBuilder(); + sb.append("Missing Supported Libraries (") + .append(missingItems.size()) + .append(" item(s) missing from remote):\n"); + missingItems.forEach(item -> sb.append(" - ").append(item).append("\n")); + + return Optional.of(sb.toString()); + } + + @Override + public String getAuditorName() { + return "Supported Libraries Auditor"; + } + + /** + * Retrieves and parses the supported libraries from the remote OpenTelemetry.io site. + * + * @param client HTTP client for making requests + * @return list of library names from the remote site + */ + private static List getRemoteSupportedLibraries(HttpClient client) + throws IOException, InterruptedException { + HttpRequest request = + HttpRequest.newBuilder().uri(URI.create(REMOTE_SUPPORTED_LIBRARIES_URL)).build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() >= 200 && response.statusCode() < 300) { + return parseLibraryMarkdownTable(response.body()); + } + throw new IOException("Failed to fetch remote supported libraries: " + response); + } + + /** + * Parses the local supported-libraries.md file to extract library names. + * + * @return list of library names from the local file + */ + private static List parseLocalSupportedLibraries() { + String baseRepoPath = System.getProperty("basePath"); + if (baseRepoPath == null) { + baseRepoPath = "./"; + } else { + baseRepoPath += "/"; + } + + String file = baseRepoPath + "docs/supported-libraries.md"; + String fileContent = FileManager.readFileToString(file); + + if (fileContent == null) { + return new ArrayList<>(); + } + + return parseLibraryMarkdownTable(fileContent); + } + + /** + * Parses markdown content to extract library names from the table. + * + * @param content the markdown content + * @return list of library names + */ + private static List parseLibraryMarkdownTable(String content) { + List libraries = new ArrayList<>(); + String[] lines = content.split("\\R"); + + boolean inLibrariesSection = false; + for (String line : lines) { + // Look for the start of the Libraries/Frameworks section (handle both formats) + if (line.contains("## Libraries / Frameworks") + || line.contains("## Libraries and Frameworks")) { + inLibrariesSection = true; + continue; + } + + // Stop when we reach the next major section + if (inLibrariesSection + && line.startsWith("##") + && !line.contains("Libraries / Frameworks") + && !line.contains("Libraries and Frameworks")) { + break; + } + + // Parse table rows in the libraries section + if (inLibrariesSection + && line.trim().startsWith("|") + && !line.contains("Library/Framework") + && !line.contains("----") // Skip separator rows + && !line.trim().equals("|")) { // Skip empty rows + String libraryName = extractLibraryNameFromMarkdownRow(line); + if (!libraryName.isEmpty() && !libraryName.startsWith("-")) { // Skip separator content + libraries.add(libraryName); + } + } + } + + return libraries; + } + + /** + * Extracts the library name from a markdown table row. + * + * @param line the table row line + * @return the library name, or empty string if not found + */ + private static String extractLibraryNameFromMarkdownRow(String line) { + String[] parts = line.split("\\|"); + if (parts.length > 1) { + String firstColumn = parts[1].trim(); + + // Handle markdown links [Text](URL) + if (firstColumn.startsWith("[") && firstColumn.contains("](")) { + int endBracket = firstColumn.indexOf("]("); + return firstColumn.substring(1, endBracket); + } + + return firstColumn; + } + return ""; + } + + /** + * Identifies libraries that are missing from the remote list but present in the local list. + * + * @param remoteLibraries libraries from the remote documentation site + * @param localLibraries libraries from the local supported-libraries.md file + * @return list of missing libraries + */ + private static List identifyMissingItems( + List remoteLibraries, List localLibraries) { + Set remoteSet = new HashSet<>(); + for (String library : remoteLibraries) { + remoteSet.add(normalizeLibraryName(library)); + } + + Set missingItems = new TreeSet<>(); + for (String localLibrary : localLibraries) { + String normalized = normalizeLibraryName(localLibrary); + if (!remoteSet.contains(normalized)) { + missingItems.add(localLibrary); + } + } + + return new ArrayList<>(missingItems); + } + + /** + * Normalizes library names for comparison by removing common variations. + * + * @param libraryName the original library name + * @return normalized library name + */ + private static String normalizeLibraryName(String libraryName) { + return libraryName.trim().toLowerCase(java.util.Locale.ROOT).replaceAll("\\s+", " "); + } +} diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditor.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditor.java new file mode 100644 index 000000000000..bf7054c3c5e8 --- /dev/null +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditor.java @@ -0,0 +1,184 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.auditors; + +import io.opentelemetry.instrumentation.docs.utils.FileManager; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import org.yaml.snakeyaml.Yaml; + +/** + * Audits the suppression/disable list on the OpenTelemetry.io documentation site to ensure it + * includes all instrumentations that should be documented. + */ +public class SuppressionListAuditor implements DocumentationAuditor { + + private static final String DOCUMENTATION_DISABLE_LIST = + "https://raw.githubusercontent.com/open-telemetry/opentelemetry.io/refs/heads/main/content/en/docs/zero-code/java/agent/disable.md"; + + // Used for consolidating instrumentation groups where we override the key with the value + private static final Map INSTRUMENTATION_DISABLE_OVERRIDES = + Map.of("akka-actor-fork-join", "akka-actor"); + + private static final List INSTRUMENTATION_EXCLUSIONS = + List.of("resources", "spring-boot-resources"); + + @Override + public Optional performAudit(HttpClient client) throws IOException, InterruptedException { + String instrumentationListContent = getInstrumentationListContent(); + String disableListContent = getDocumentationDisableList(client); + List disabledList = parseDocumentationDisabledList(disableListContent); + List instrumentationList = parseInstrumentationList(instrumentationListContent); + List missingItems = identifyMissingItems(disabledList, instrumentationList); + + if (missingItems.isEmpty()) { + return Optional.empty(); + } + + StringBuilder sb = new StringBuilder(); + sb.append("Missing Disable List (").append(missingItems.size()).append(" item(s) missing):\n"); + missingItems.forEach(item -> sb.append(" - ").append(item).append("\n")); + + return Optional.of(sb.toString()); + } + + @Override + public String getAuditorName() { + return "Suppression List Auditor"; + } + + /** + * Gets the instrumentation list content from the local file. + * + * @return the content of the instrumentation-list.yaml file + * @throws RuntimeException if the file cannot be read + */ + private static String getInstrumentationListContent() { + String baseRepoPath = System.getProperty("basePath"); + if (baseRepoPath == null) { + baseRepoPath = "./"; + } else { + baseRepoPath += "/"; + } + + String file = baseRepoPath + "docs/instrumentation-list.yaml"; + String content = FileManager.readFileToString(file); + if (content == null) { + throw new IllegalStateException("Failed to read instrumentation list from: " + file); + } + return content; + } + + /** + * Retrieves contents of the disable page from the main branch of the documentation site. + * + * @return the file content as a string + */ + private static String getDocumentationDisableList(HttpClient client) + throws IOException, InterruptedException { + HttpRequest request = + HttpRequest.newBuilder().uri(URI.create(DOCUMENTATION_DISABLE_LIST)).build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() >= 200 && response.statusCode() < 300) { + return response.body(); + } + throw new IOException("Failed to fetch disable list: " + response); + } + + @SuppressWarnings("unchecked") + public static List parseInstrumentationList(String fileContent) { + List instrumentationList = new ArrayList<>(); + Yaml yaml = new Yaml(); + Map data = yaml.load(fileContent); + + if (data != null && data.get("libraries") instanceof Map) { + Map>> libraries = + (Map>>) data.get("libraries"); + for (List> libraryGroup : libraries.values()) { + for (Map instrumentation : libraryGroup) { + if (instrumentation.get("name") instanceof String) { + instrumentationList.add((String) instrumentation.get("name")); + } + } + } + } + return instrumentationList; + } + + /** + * Identifies missing items in the instrumentation list that are not present in the documentation + * disable list. Takes into account any overrides specified in INSTRUMENTATION_DISABLE_OVERRIDES + * and excludes items listed in INSTRUMENTATION_EXCLUSIONS. + * + * @param documentationDisabledList a list of items that are documented + * @param instrumentationList a list of instrumentations from the instrumentation list + * @return a list of missing items that should be documented + */ + public static List identifyMissingItems( + List documentationDisabledList, List instrumentationList) { + Set documentationDisabledSet = new HashSet<>(documentationDisabledList); + + Set sanitizedInstrumentationItems = new TreeSet<>(); + for (String item : instrumentationList) { + sanitizedInstrumentationItems.add(item.replaceFirst("-[0-9].*$", "")); + } + + List missingItems = new ArrayList<>(); + for (String item : sanitizedInstrumentationItems) { + if (INSTRUMENTATION_EXCLUSIONS.contains(item)) { + continue; // Skip excluded items + } + String itemToCheck = INSTRUMENTATION_DISABLE_OVERRIDES.getOrDefault(item, item); + boolean found = false; + for (String disabledItem : documentationDisabledSet) { + if (itemToCheck.startsWith(disabledItem)) { + found = true; + break; + } + } + if (!found) { + missingItems.add(item); + } + } + return missingItems; + } + + /** + * Parses the documentation disabled list from the file content and turns it into a list of + * instrumentation names. + * + * @param fileContent the content of the disable.md documentation file + * @return a list of instrumentation names that are documented + */ + public static List parseDocumentationDisabledList(String fileContent) { + List instrumentationList = new ArrayList<>(); + String[] lines = fileContent.split("\\R"); + for (String line : lines) { + if (line.trim().startsWith("|")) { + String[] parts = line.split("\\|"); + if (parts.length > 2) { + String potentialName = parts[2].trim(); + if (potentialName.startsWith("`") && potentialName.endsWith("`")) { + String name = potentialName.substring(1, potentialName.length() - 1); + instrumentationList.add(name); + } + } + } + } + return instrumentationList; + } +} diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditorTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditorTest.java new file mode 100644 index 000000000000..ee542fa1d613 --- /dev/null +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditorTest.java @@ -0,0 +1,172 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.auditors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.docs.utils.FileManager; +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SupportedLibrariesAuditorTest { + + @Test + void testPerformAudit_noMissingItems() throws IOException, InterruptedException { + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockClient.send(any(), any())).thenReturn(mockResponse); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn(createRemoteSupportedLibrariesContent()); + + try (MockedStatic fileManagerMock = Mockito.mockStatic(FileManager.class)) { + fileManagerMock + .when(() -> FileManager.readFileToString(any())) + .thenReturn(createLocalSupportedLibrariesContent()); + + SupportedLibrariesAuditor auditor = new SupportedLibrariesAuditor(); + Optional result = auditor.performAudit(mockClient); + + assertThat(result).isEmpty(); + } + } + + @Test + void testPerformAudit_withMissingItems() throws IOException, InterruptedException { + // Mock HTTP client to return remote supported libraries content (missing some items) + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockClient.send(any(), any())).thenReturn(mockResponse); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn(createRemoteSupportedLibrariesContentMissing()); + + // Mock local file reading + try (MockedStatic fileManagerMock = Mockito.mockStatic(FileManager.class)) { + fileManagerMock + .when(() -> FileManager.readFileToString(any())) + .thenReturn(createLocalSupportedLibrariesContent()); + + SupportedLibrariesAuditor auditor = new SupportedLibrariesAuditor(); + Optional result = auditor.performAudit(mockClient); + + assertThat(result).isPresent(); + assertThat(result.get()) + .contains("Missing Supported Libraries (1 item(s) missing from remote):"); + assertThat(result.get()).contains("- Apache Camel"); + } + } + + @Test + void testGetAuditorName() { + SupportedLibrariesAuditor auditor = new SupportedLibrariesAuditor(); + assertThat(auditor.getAuditorName()).isEqualTo("Supported Libraries Auditor"); + } + + private String createLocalSupportedLibrariesContent() { + return """ +# Supported libraries, frameworks, application servers, and JVMs + +We automatically instrument and support a huge number of libraries, frameworks, +and application servers... right out of the box! + +## Contents + +- [Libraries / Frameworks](#libraries--frameworks) +- [Application Servers](#application-servers) + +## Libraries / Frameworks + +These are the supported libraries and frameworks: + +| Library/Framework | Auto-instrumented versions | Standalone Library Instrumentation [1] | Semantic Conventions | +|---------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| [ActiveJ](https://activej.io/) | 6.0+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] | +| [Akka Actors](https://doc.akka.io/docs/akka/current/typed/index.html) | 2.3+ | N/A | Context propagation | +| [Apache Camel](https://camel.apache.org/) | 2.20+ (not including 3.0+ yet) | N/A | Dependent on components in use | + +## Application Servers + +These are the application servers that the smoke tests are run against: + +| Application server | Version | JVM | OS | +|---------------------------------------------------------------------------------------|------------------------------------------|--------------------------------------------------------|---------------------------------------| +| [Jetty](https://www.eclipse.org/jetty/) | 9.4.53 | OpenJDK 8, 11, 17, 21, 23
OpenJ9 8, 11, 17, 21, 23 | [`ubuntu-latest`], [`windows-latest`] | +"""; + } + + private String createRemoteSupportedLibrariesContent() { + return """ +# Supported libraries, frameworks, application servers, and JVMs + +We automatically instrument and support a huge number of libraries, frameworks, +and application servers... right out of the box! + +## Contents + +- [Libraries / Frameworks](#libraries--frameworks) +- [Application Servers](#application-servers) + +## Libraries / Frameworks + +These are the supported libraries and frameworks: + +| Library/Framework | Auto-instrumented versions | Standalone Library Instrumentation [1] | Semantic Conventions | +|---------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| [ActiveJ](https://activej.io/) | 6.0+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] | +| [Akka Actors](https://doc.akka.io/docs/akka/current/typed/index.html) | 2.3+ | N/A | Context propagation | +| [Apache Camel](https://camel.apache.org/) | 2.20+ (not including 3.0+ yet) | N/A | Dependent on components in use | + +## Application Servers + +These are the application servers that the smoke tests are run against: + +| Application server | Version | JVM | OS | +|---------------------------------------------------------------------------------------|------------------------------------------|--------------------------------------------------------|---------------------------------------| +| [Jetty](https://www.eclipse.org/jetty/) | 9.4.53 | OpenJDK 8, 11, 17, 21, 23
OpenJ9 8, 11, 17, 21, 23 | [`ubuntu-latest`], [`windows-latest`] | +"""; + } + + private String createRemoteSupportedLibrariesContentMissing() { + return """ +# Supported libraries, frameworks, application servers, and JVMs + +We automatically instrument and support a huge number of libraries, frameworks, +and application servers... right out of the box! + +## Contents + +- [Libraries / Frameworks](#libraries--frameworks) +- [Application Servers](#application-servers) + +## Libraries / Frameworks + +These are the supported libraries and frameworks: + +| Library/Framework | Auto-instrumented versions | Standalone Library Instrumentation [1] | Semantic Conventions | +|---------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| [ActiveJ](https://activej.io/) | 6.0+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] | +| [Akka Actors](https://doc.akka.io/docs/akka/current/typed/index.html) | 2.3+ | N/A | Context propagation | + +## Application Servers + +These are the application servers that the smoke tests are run against: + +| Application server | Version | JVM | OS | +|---------------------------------------------------------------------------------------|------------------------------------------|--------------------------------------------------------|---------------------------------------| +| [Jetty](https://www.eclipse.org/jetty/) | 9.4.53 | OpenJDK 8, 11, 17, 21, 23
OpenJ9 8, 11, 17, 21, 23 | [`ubuntu-latest`], [`windows-latest`] | +"""; + } +} diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditorTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditorTest.java new file mode 100644 index 000000000000..beb00e925914 --- /dev/null +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditorTest.java @@ -0,0 +1,160 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.auditors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.docs.utils.FileManager; +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SuppressionListAuditorTest { + + @Test + void testPerformAudit_noMissingItems() throws IOException, InterruptedException { + // Mock HTTP client to return disable list content + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockClient.send(any(), any())).thenReturn(mockResponse); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn(createDisableListContent()); + + // Mock file reading for instrumentation list + try (MockedStatic fileManagerMock = Mockito.mockStatic(FileManager.class)) { + fileManagerMock + .when(() -> FileManager.readFileToString(any())) + .thenReturn(createInstrumentationListContent()); + + SuppressionListAuditor auditor = new SuppressionListAuditor(); + Optional result = auditor.performAudit(mockClient); + + assertThat(result).isEmpty(); + } + } + + @Test + void testPerformAudit_withMissingItems() throws IOException, InterruptedException { + // Mock HTTP client to return disable list content + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockClient.send(any(), any())).thenReturn(mockResponse); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn(createDisableListContentMissing()); + + // Mock file reading for instrumentation list + try (MockedStatic fileManagerMock = Mockito.mockStatic(FileManager.class)) { + fileManagerMock + .when(() -> FileManager.readFileToString(any())) + .thenReturn(createInstrumentationListContent()); + + SuppressionListAuditor auditor = new SuppressionListAuditor(); + Optional result = auditor.performAudit(mockClient); + + assertThat(result).isPresent(); + assertThat(result.get()).contains("Missing Disable List (1 item(s) missing):"); + assertThat(result.get()).contains("- activej-http"); + } + } + + @Test + void testGetAuditorName() { + SuppressionListAuditor auditor = new SuppressionListAuditor(); + assertThat(auditor.getAuditorName()).isEqualTo("Suppression List Auditor"); + } + + private String createDisableListContent() { + return """ +## Enable manual instrumentation only + +You can suppress all auto instrumentations but have support for manual +instrumentation with `@WithSpan` and normal API interactions by using +`-Dotel.instrumentation.common.default-enabled=false -Dotel.instrumentation.opentelemetry-api.enabled=true -Dotel.instrumentation.opentelemetry-instrumentation-annotations.enabled=true` + +## Suppressing specific agent instrumentation + +You can suppress agent instrumentation of specific libraries. + +{{% config_option name="otel.instrumentation.[name].enabled" %}} Set to `false` +to suppress agent instrumentation of specific libraries, where [name] is the +corresponding instrumentation name: {{% /config_option %}} + +| Library/Framework | Instrumentation name | +| ------------------------------------------------ | ------------------------------------------- | +| Additional methods tracing | `methods` | +| ActiveJ | `activej-http` | +| Akka Actor | `akka-actor` | +| Akka HTTP | `akka-http` | +"""; + } + + private String createDisableListContentMissing() { + return """ +## Enable manual instrumentation only + +You can suppress all auto instrumentations but have support for manual +instrumentation with `@WithSpan` and normal API interactions by using +`-Dotel.instrumentation.common.default-enabled=false -Dotel.instrumentation.opentelemetry-api.enabled=true -Dotel.instrumentation.opentelemetry-instrumentation-annotations.enabled=true` + +## Suppressing specific agent instrumentation + +You can suppress agent instrumentation of specific libraries. + +{{% config_option name="otel.instrumentation.[name].enabled" %}} Set to `false` +to suppress agent instrumentation of specific libraries, where [name] is the +corresponding instrumentation name: {{% /config_option %}} + +| Library/Framework | Instrumentation name | +| ------------------------------------------------ | ------------------------------------------- | +| Additional methods tracing | `methods` | +| Akka Actor | `akka-actor` | +| Akka HTTP | `akka-http` | +"""; + } + + private String createInstrumentationListContent() { + return """ +libraries: + activej: + - name: activej-http-6.0 + description: This instrumentation enables SERVER spans and metrics for the ActiveJ + HTTP server. + source_path: instrumentation/activej-http-6.0 + minimum_java_version: 17 + scope: + name: io.opentelemetry.activej-http-6.0 + target_versions: + javaagent: + - io.activej:activej-http:[6.0,) + akka: + - name: akka-actor-2.3 + source_path: instrumentation/akka/akka-actor-2.3 + scope: + name: io.opentelemetry.akka-actor-2.3 + target_versions: + javaagent: + - com.typesafe.akka:akka-actor_2.11:[2.3,) + - com.typesafe.akka:akka-actor_2.12:[2.3,) + - com.typesafe.akka:akka-actor_2.13:[2.3,) + - name: akka-http-10.0 + description: This instrumentation enables CLIENT and SERVER spans and metrics + for the Akka HTTP client and server. + source_path: instrumentation/akka/akka-http-10.0 + scope: + name: io.opentelemetry.akka-http-10.0 +"""; + } +} From f0ae8b2d6e5f62ebb90ca5afa304fc6ad91f95ca Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Wed, 17 Sep 2025 16:53:46 -0400 Subject: [PATCH 2/6] cleanup tests --- .../documentation-disable-list-audit.yml | 2 +- .../reusable-workflow-notification.yml | 2 +- .../docs/DocSynchronization.java | 17 -- .../docs/DocSynchronizationTest.java | 146 ------------------ .../SupportedLibrariesAuditorTest.java | 27 ++-- .../auditors/SuppressionListAuditorTest.java | 136 ++++++++++++++-- 6 files changed, 139 insertions(+), 191 deletions(-) delete mode 100644 instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/DocSynchronizationTest.java diff --git a/.github/workflows/documentation-disable-list-audit.yml b/.github/workflows/documentation-disable-list-audit.yml index 8fc5939892bd..468bad777a1e 100644 --- a/.github/workflows/documentation-disable-list-audit.yml +++ b/.github/workflows/documentation-disable-list-audit.yml @@ -34,7 +34,7 @@ jobs: if ! ./gradlew :instrumentation-docs:docSiteAudit > audit_output.txt 2>&1; then echo "AUDIT_FAILED=true" >> $GITHUB_OUTPUT echo "AUDIT_OUTPUT<> $GITHUB_OUTPUT - # Extract only the audit error messages between SEVERE: and FAILURE: + # Extract only the relevant lines with details about failures sed -n '/SEVERE: /,/^FAILURE:/p' audit_output.txt | \ sed '/^FAILURE:/d' | \ sed 's/^.*SEVERE: //' | \ diff --git a/.github/workflows/reusable-workflow-notification.yml b/.github/workflows/reusable-workflow-notification.yml index d647453c0e2c..41b6f113546a 100644 --- a/.github/workflows/reusable-workflow-notification.yml +++ b/.github/workflows/reusable-workflow-notification.yml @@ -36,7 +36,7 @@ jobs: # Prepare the issue body with failure details if available if [[ -n "${{ inputs.failure-details }}" ]]; then - issue_body="See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."$'\n\n'"## Failure Details"$'\n\n'"$(echo '```')"$'\n'"${{ inputs.failure-details }}"$'\n'"$(echo '```')" + issue_body="See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."$'\n\n'"## Failure Details"$'\n\n''```'$'\n'"${{ inputs.failure-details }}"$'\n''```' else issue_body="See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." fi diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java index c01cba4f3dce..2ce719ede850 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java @@ -27,23 +27,6 @@ public class DocSynchronization { private static final List AUDITORS = List.of(new SuppressionListAuditor(), new SupportedLibrariesAuditor()); - // visible for testing - public static List parseInstrumentationList(String fileContent) { - return SuppressionListAuditor.parseInstrumentationList(fileContent); - } - - // visible for testing - public static List parseDocumentationDisabledList(String fileContent) { - return SuppressionListAuditor.parseDocumentationDisabledList(fileContent); - } - - // visible for testing - public static List identifyMissingItems( - List documentationDisabledList, List instrumentationList) { - return SuppressionListAuditor.identifyMissingItems( - documentationDisabledList, instrumentationList); - } - public static void main(String[] args) { HttpClient client = HttpClient.newHttpClient(); diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/DocSynchronizationTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/DocSynchronizationTest.java deleted file mode 100644 index 510c7721e9cc..000000000000 --- a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/DocSynchronizationTest.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.docs; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class DocSynchronizationTest { - - @Test - void testGetDocumentationInstrumentationList() { - String testFile = - """ -## Enable manual instrumentation only - -You can suppress all auto instrumentations but have support for manual -instrumentation with `@WithSpan` and normal API interactions by using -`-Dotel.instrumentation.common.default-enabled=false -Dotel.instrumentation.opentelemetry-api.enabled=true -Dotel.instrumentation.opentelemetry-instrumentation-annotations.enabled=true` - -## Suppressing specific agent instrumentation - -You can suppress agent instrumentation of specific libraries. - -{{% config_option name="otel.instrumentation.[name].enabled" %}} Set to `false` -to suppress agent instrumentation of specific libraries, where [name] is the -corresponding instrumentation name: {{% /config_option %}} - -| Library/Framework | Instrumentation name | -| ------------------------------------------------ | ------------------------------------------- | -| Additional methods tracing | `methods` | -| Additional tracing annotations | `external-annotations` | -| Akka Actor | `akka-actor` | -| Akka HTTP | `akka-http` | -| Apache Axis2 | `axis2` | -| Apache Camel | `camel` | -"""; - - List result = DocSynchronization.parseDocumentationDisabledList(testFile); - assertThat(result.size()).isEqualTo(6); - assertThat(result) - .containsExactlyInAnyOrder( - "methods", "external-annotations", "akka-actor", "akka-http", "axis2", "camel"); - } - - @Test - void testParseInstrumentationList() { - String testList = - """ -libraries: - activej: - - name: activej-http-6.0 - description: This instrumentation enables SERVER spans and metrics for the ActiveJ - HTTP server. - source_path: instrumentation/activej-http-6.0 - minimum_java_version: 17 - scope: - name: io.opentelemetry.activej-http-6.0 - target_versions: - javaagent: - - io.activej:activej-http:[6.0,) - telemetry: - - when: default - metrics: - - name: http.server.request.duration - description: Duration of HTTP server requests. - type: HISTOGRAM - unit: s - attributes: - - name: http.request.method - type: STRING - - name: http.response.status_code - type: LONG - - name: network.protocol.version - type: STRING - - name: url.scheme - type: STRING - akka: - - name: akka-actor-2.3 - source_path: instrumentation/akka/akka-actor-2.3 - scope: - name: io.opentelemetry.akka-actor-2.3 - target_versions: - javaagent: - - com.typesafe.akka:akka-actor_2.11:[2.3,) - - com.typesafe.akka:akka-actor_2.12:[2.3,) - - com.typesafe.akka:akka-actor_2.13:[2.3,) - - name: akka-actor-fork-join-2.5 - source_path: instrumentation/akka/akka-actor-fork-join-2.5 - scope: - name: io.opentelemetry.akka-actor-fork-join-2.5 - target_versions: - javaagent: - - com.typesafe.akka:akka-actor_2.12:[2.5,2.6) - - com.typesafe.akka:akka-actor_2.13:[2.5.23,2.6) - - com.typesafe.akka:akka-actor_2.11:[2.5,) - - name: akka-http-10.0 - description: This instrumentation enables CLIENT and SERVER spans and metrics - for the Akka HTTP client and server. - source_path: instrumentation/akka/akka-http-10.0 - scope: - name: io.opentelemetry.akka-http-10.0 -"""; - List result = DocSynchronization.parseInstrumentationList(testList); - - assertThat(result.size()).isEqualTo(4); - assertThat(result) - .containsExactlyInAnyOrder( - "activej-http-6.0", "akka-actor-2.3", "akka-actor-fork-join-2.5", "akka-http-10.0"); - } - - @Test - void identifyMissingItems() { - List documentationDisabledList = List.of("methods", "akka-actor", "akka-http"); - - List instrumentationList = - List.of( - "methods", - "akka-actor-2.3", - "activej-http-6.0", - "akka-actor-fork-join-2.5", - "camel-2.20"); - - List missingItems = - DocSynchronization.identifyMissingItems(documentationDisabledList, instrumentationList); - assertThat(missingItems.size()).isEqualTo(2); - assertThat(missingItems).containsExactlyInAnyOrder("camel", "activej-http"); - } - - @Test - void testIdentifyMissingItemsWithHyphenatedMatch() { - List documentationDisabledList = List.of("clickhouse"); - List instrumentationList = List.of("clickhouse-client-0.5"); - - List missingItems = - DocSynchronization.identifyMissingItems(documentationDisabledList, instrumentationList); - assertThat(missingItems).isEmpty(); - } -} diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditorTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditorTest.java index ee542fa1d613..d344b39b5e96 100644 --- a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditorTest.java +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditorTest.java @@ -7,12 +7,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import io.opentelemetry.instrumentation.docs.utils.FileManager; import java.io.IOException; import java.net.http.HttpClient; +import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Optional; import org.junit.jupiter.api.Test; @@ -22,13 +24,15 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) +@SuppressWarnings("unchecked") class SupportedLibrariesAuditorTest { @Test - void testPerformAudit_noMissingItems() throws IOException, InterruptedException { + void testPerformAuditWithNoMissingItems() throws IOException, InterruptedException { HttpClient mockClient = mock(HttpClient.class); HttpResponse mockResponse = mock(HttpResponse.class); - when(mockClient.send(any(), any())).thenReturn(mockResponse); + when(mockClient.send(any(HttpRequest.class), eq(HttpResponse.BodyHandlers.ofString()))) + .thenReturn(mockResponse); when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn(createRemoteSupportedLibrariesContent()); @@ -45,15 +49,14 @@ void testPerformAudit_noMissingItems() throws IOException, InterruptedException } @Test - void testPerformAudit_withMissingItems() throws IOException, InterruptedException { - // Mock HTTP client to return remote supported libraries content (missing some items) + void testPerformAuditWithMissingItems() throws IOException, InterruptedException { HttpClient mockClient = mock(HttpClient.class); HttpResponse mockResponse = mock(HttpResponse.class); - when(mockClient.send(any(), any())).thenReturn(mockResponse); + when(mockClient.send(any(HttpRequest.class), eq(HttpResponse.BodyHandlers.ofString()))) + .thenReturn(mockResponse); when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn(createRemoteSupportedLibrariesContentMissing()); - // Mock local file reading try (MockedStatic fileManagerMock = Mockito.mockStatic(FileManager.class)) { fileManagerMock .when(() -> FileManager.readFileToString(any())) @@ -69,13 +72,7 @@ void testPerformAudit_withMissingItems() throws IOException, InterruptedExceptio } } - @Test - void testGetAuditorName() { - SupportedLibrariesAuditor auditor = new SupportedLibrariesAuditor(); - assertThat(auditor.getAuditorName()).isEqualTo("Supported Libraries Auditor"); - } - - private String createLocalSupportedLibrariesContent() { + private static String createLocalSupportedLibrariesContent() { return """ # Supported libraries, frameworks, application servers, and JVMs @@ -107,7 +104,7 @@ private String createLocalSupportedLibrariesContent() { """; } - private String createRemoteSupportedLibrariesContent() { + private static String createRemoteSupportedLibrariesContent() { return """ # Supported libraries, frameworks, application servers, and JVMs @@ -139,7 +136,7 @@ private String createRemoteSupportedLibrariesContent() { """; } - private String createRemoteSupportedLibrariesContentMissing() { + private static String createRemoteSupportedLibrariesContentMissing() { return """ # Supported libraries, frameworks, application servers, and JVMs diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditorTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditorTest.java index beb00e925914..cc73bc6d2584 100644 --- a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditorTest.java +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/auditors/SuppressionListAuditorTest.java @@ -7,13 +7,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import io.opentelemetry.instrumentation.docs.utils.FileManager; import java.io.IOException; import java.net.http.HttpClient; +import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,18 +25,18 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) +@SuppressWarnings("unchecked") class SuppressionListAuditorTest { @Test - void testPerformAudit_noMissingItems() throws IOException, InterruptedException { - // Mock HTTP client to return disable list content + void testPerformAuditWithNoMissingItems() throws IOException, InterruptedException { HttpClient mockClient = mock(HttpClient.class); HttpResponse mockResponse = mock(HttpResponse.class); - when(mockClient.send(any(), any())).thenReturn(mockResponse); + when(mockClient.send(any(HttpRequest.class), eq(HttpResponse.BodyHandlers.ofString()))) + .thenReturn(mockResponse); when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn(createDisableListContent()); - // Mock file reading for instrumentation list try (MockedStatic fileManagerMock = Mockito.mockStatic(FileManager.class)) { fileManagerMock .when(() -> FileManager.readFileToString(any())) @@ -47,15 +50,14 @@ void testPerformAudit_noMissingItems() throws IOException, InterruptedException } @Test - void testPerformAudit_withMissingItems() throws IOException, InterruptedException { - // Mock HTTP client to return disable list content + void testPerformAuditWithMissingItems() throws IOException, InterruptedException { HttpClient mockClient = mock(HttpClient.class); HttpResponse mockResponse = mock(HttpResponse.class); - when(mockClient.send(any(), any())).thenReturn(mockResponse); + when(mockClient.send(any(HttpRequest.class), eq(HttpResponse.BodyHandlers.ofString()))) + .thenReturn(mockResponse); when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn(createDisableListContentMissing()); - // Mock file reading for instrumentation list try (MockedStatic fileManagerMock = Mockito.mockStatic(FileManager.class)) { fileManagerMock .when(() -> FileManager.readFileToString(any())) @@ -76,7 +78,119 @@ void testGetAuditorName() { assertThat(auditor.getAuditorName()).isEqualTo("Suppression List Auditor"); } - private String createDisableListContent() { + @Test + void testParseDocumentationDisabledList() { + String testFile = + """ +## Enable manual instrumentation only + +You can suppress all auto instrumentations but have support for manual +instrumentation with `@WithSpan` and normal API interactions by using +`-Dotel.instrumentation.common.default-enabled=false -Dotel.instrumentation.opentelemetry-api.enabled=true -Dotel.instrumentation.opentelemetry-instrumentation-annotations.enabled=true` + +## Suppressing specific agent instrumentation + +You can suppress agent instrumentation of specific libraries. + +{{% config_option name="otel.instrumentation.[name].enabled" %}} Set to `false` +to suppress agent instrumentation of specific libraries, where [name] is the +corresponding instrumentation name: {{% /config_option %}} + +| Library/Framework | Instrumentation name | +| ------------------------------------------------ | ------------------------------------------- | +| Additional methods tracing | `methods` | +| Additional tracing annotations | `external-annotations` | +| Akka Actor | `akka-actor` | +| Akka HTTP | `akka-http` | +| Apache Axis2 | `axis2` | +| Apache Camel | `camel` | +"""; + + var result = SuppressionListAuditor.parseDocumentationDisabledList(testFile); + assertThat(result).hasSize(6); + assertThat(result) + .containsExactlyInAnyOrder( + "methods", "external-annotations", "akka-actor", "akka-http", "axis2", "camel"); + } + + @Test + void testParseInstrumentationList() { + String testList = + """ +libraries: + activej: + - name: activej-http-6.0 + description: This instrumentation enables SERVER spans and metrics for the ActiveJ + HTTP server. + source_path: instrumentation/activej-http-6.0 + minimum_java_version: 17 + scope: + name: io.opentelemetry.activej-http-6.0 + target_versions: + javaagent: + - io.activej:activej-http:[6.0,) + akka: + - name: akka-actor-2.3 + source_path: instrumentation/akka/akka-actor-2.3 + scope: + name: io.opentelemetry.akka-actor-2.3 + target_versions: + javaagent: + - com.typesafe.akka:akka-actor_2.11:[2.3,) + - com.typesafe.akka:akka-actor_2.12:[2.3,) + - com.typesafe.akka:akka-actor_2.13:[2.3,) + - name: akka-actor-fork-join-2.5 + source_path: instrumentation/akka/akka-actor-fork-join-2.5 + scope: + name: io.opentelemetry.akka-actor-fork-join-2.5 + target_versions: + javaagent: + - com.typesafe.akka:akka-actor_2.12:[2.5,2.6) + - com.typesafe.akka:akka-actor_2.13:[2.5.23,2.6) + - com.typesafe.akka:akka-actor_2.11:[2.5,) + - name: akka-http-10.0 + description: This instrumentation enables CLIENT and SERVER spans and metrics + for the Akka HTTP client and server. + source_path: instrumentation/akka/akka-http-10.0 + scope: + name: io.opentelemetry.akka-http-10.0 +"""; + var result = SuppressionListAuditor.parseInstrumentationList(testList); + + assertThat(result).hasSize(4); + assertThat(result) + .containsExactlyInAnyOrder( + "activej-http-6.0", "akka-actor-2.3", "akka-actor-fork-join-2.5", "akka-http-10.0"); + } + + @Test + void testIdentifyMissingItems() { + var documentationDisabledList = List.of("methods", "akka-actor", "akka-http"); + var instrumentationList = + List.of( + "methods", + "akka-actor-2.3", + "activej-http-6.0", + "akka-actor-fork-join-2.5", + "camel-2.20"); + + var missingItems = + SuppressionListAuditor.identifyMissingItems(documentationDisabledList, instrumentationList); + assertThat(missingItems).hasSize(2); + assertThat(missingItems).containsExactlyInAnyOrder("camel", "activej-http"); + } + + @Test + void testIdentifyMissingItemsWithHyphenatedMatch() { + var documentationDisabledList = List.of("clickhouse"); + var instrumentationList = List.of("clickhouse-client-0.5"); + + var missingItems = + SuppressionListAuditor.identifyMissingItems(documentationDisabledList, instrumentationList); + assertThat(missingItems).isEmpty(); + } + + private static String createDisableListContent() { return """ ## Enable manual instrumentation only @@ -101,7 +215,7 @@ private String createDisableListContent() { """; } - private String createDisableListContentMissing() { + private static String createDisableListContentMissing() { return """ ## Enable manual instrumentation only @@ -125,7 +239,7 @@ private String createDisableListContentMissing() { """; } - private String createInstrumentationListContent() { + private static String createInstrumentationListContent() { return """ libraries: activej: From ffde0dfcbaa3659e90b74b461ef77a07b8d1188a Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Wed, 17 Sep 2025 17:03:01 -0400 Subject: [PATCH 3/6] simplify action --- .../documentation-disable-list-audit.yml | 19 +++++-------------- .../docs/DocSynchronization.java | 11 ++++++++++- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/documentation-disable-list-audit.yml b/.github/workflows/documentation-disable-list-audit.yml index 468bad777a1e..e651fc66a740 100644 --- a/.github/workflows/documentation-disable-list-audit.yml +++ b/.github/workflows/documentation-disable-list-audit.yml @@ -30,22 +30,13 @@ jobs: - name: Run doc site audit id: audit run: | - # Capture both stdout and stderr, and preserve exit code - if ! ./gradlew :instrumentation-docs:docSiteAudit > audit_output.txt 2>&1; then + if ! output=$(./gradlew :instrumentation-docs:docSiteAudit 2>&1); then echo "AUDIT_FAILED=true" >> $GITHUB_OUTPUT echo "AUDIT_OUTPUT<> $GITHUB_OUTPUT - # Extract only the relevant lines with details about failures - sed -n '/SEVERE: /,/^FAILURE:/p' audit_output.txt | \ - sed '/^FAILURE:/d' | \ - sed 's/^.*SEVERE: //' | \ - sed '/^> Task/d' | \ - sed '/^gradle\/actions:/d' | \ - sed '/^$/d' >> $GITHUB_OUTPUT - # Add guidance on how to fix the issues - echo "" >> $GITHUB_OUTPUT - echo "## How to Fix" >> $GITHUB_OUTPUT - echo "" >> $GITHUB_OUTPUT - echo "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)" >> $GITHUB_OUTPUT + # Extract only the content between our custom markers + echo "$output" | sed -n '/=== AUDIT_FAILURE_START ===/,/=== AUDIT_FAILURE_END ===/p' | \ + sed '/=== AUDIT_FAILURE_START ===/d' | \ + sed '/=== AUDIT_FAILURE_END ===/d' >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT exit 1 else diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java index 2ce719ede850..af9867b1aed8 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java @@ -61,7 +61,16 @@ public static void main(String[] args) { } if (hasFailures) { - logger.severe(combinedMessage.toString()); + // Add custom markers and "How to Fix" section for GitHub workflow extraction + StringBuilder finalMessage = new StringBuilder(); + finalMessage.append("=== AUDIT_FAILURE_START ===\n"); + finalMessage.append(combinedMessage.toString()); + finalMessage.append("\n\n## How to Fix\n\n"); + finalMessage.append( + "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)"); + finalMessage.append("\n=== AUDIT_FAILURE_END ==="); + + logger.severe(finalMessage.toString()); exit(1); } else { logger.info("All documentation audits passed successfully."); From 5e6d48d8d4e19f5ee21ed6c730a7336edf166a18 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Wed, 17 Sep 2025 17:14:21 -0400 Subject: [PATCH 4/6] rename action --- docs/contributing/documenting-instrumentation.md | 2 +- .../docs/auditors/SupportedLibrariesAuditor.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/contributing/documenting-instrumentation.md b/docs/contributing/documenting-instrumentation.md index ce1f7bb4cd5d..b0e461ead26d 100644 --- a/docs/contributing/documenting-instrumentation.md +++ b/docs/contributing/documenting-instrumentation.md @@ -238,5 +238,5 @@ page lists the instrumentations in the context of the keys needed for using the `otel.instrumentation.[name].enabled` configuration. All new instrumentations should be added to this list. There is a -[Github action](../../.github/workflows/documentation-disable-list-audit.yml) that runs nightly to check +[Github action](../../.github/workflows/documentation-synchronization-audit.yml) that runs nightly to check for any missing instrumentations, and will open an issue if any are found. diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditor.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditor.java index 528f1f95ea60..ffc6a9d7fafa 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditor.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/auditors/SupportedLibrariesAuditor.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.TreeSet; @@ -189,6 +190,6 @@ private static List identifyMissingItems( * @return normalized library name */ private static String normalizeLibraryName(String libraryName) { - return libraryName.trim().toLowerCase(java.util.Locale.ROOT).replaceAll("\\s+", " "); + return libraryName.trim().toLowerCase(Locale.ROOT).replaceAll("\\s+", " "); } } From adcabb12dc10a5d8345e86a53253353b0c99c423 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Wed, 17 Sep 2025 17:28:09 -0400 Subject: [PATCH 5/6] fix --- .github/workflows/reusable-workflow-notification.yml | 2 +- docs/contributing/documenting-instrumentation.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-workflow-notification.yml b/.github/workflows/reusable-workflow-notification.yml index 41b6f113546a..47bf2207a49b 100644 --- a/.github/workflows/reusable-workflow-notification.yml +++ b/.github/workflows/reusable-workflow-notification.yml @@ -36,7 +36,7 @@ jobs: # Prepare the issue body with failure details if available if [[ -n "${{ inputs.failure-details }}" ]]; then - issue_body="See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."$'\n\n'"## Failure Details"$'\n\n''```'$'\n'"${{ inputs.failure-details }}"$'\n''```' + 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 }}" else issue_body="See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." fi diff --git a/docs/contributing/documenting-instrumentation.md b/docs/contributing/documenting-instrumentation.md index b0e461ead26d..01b7acebd582 100644 --- a/docs/contributing/documenting-instrumentation.md +++ b/docs/contributing/documenting-instrumentation.md @@ -227,7 +227,9 @@ All of our instrumentation modules are listed on the opentelemetry.io website in The [Supported Libraries](https://opentelemetry.io/docs/zero-code/java/agent/supported-libraries/) page lists all the library instrumentations that are included in the OpenTelemetry Java agent. It mostly mirrors the information from the [supported libraries](../supported-libraries.md) page in -this repo, and should be updated when adding or removing library instrumentations. +this repo, and should be updated when adding or removing library instrumentations. There is a +[Github action](../../.github/workflows/documentation-synchronization-audit.yml) that runs nightly +to check for any missing instrumentations, and will open an issue if any are found. This page may be automatically generated in the future, but for now it is manually maintained. From 7d0011a759965aab995a904cb61bd187fbb338f0 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Wed, 17 Sep 2025 17:37:33 -0400 Subject: [PATCH 6/6] rename action --- ...e-list-audit.yml => documentation-synchronization-audit.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{documentation-disable-list-audit.yml => documentation-synchronization-audit.yml} (96%) diff --git a/.github/workflows/documentation-disable-list-audit.yml b/.github/workflows/documentation-synchronization-audit.yml similarity index 96% rename from .github/workflows/documentation-disable-list-audit.yml rename to .github/workflows/documentation-synchronization-audit.yml index e651fc66a740..8a9077e3c1e8 100644 --- a/.github/workflows/documentation-disable-list-audit.yml +++ b/.github/workflows/documentation-synchronization-audit.yml @@ -1,4 +1,4 @@ -name: opentelemetry.io documentation disable list audit +name: Documentation Synchronization Audit (opentelemetry.io) on: schedule: