diff --git a/.github/workflows/documentation-disable-list-audit.yml b/.github/workflows/documentation-disable-list-audit.yml new file mode 100644 index 000000000000..3504309eb049 --- /dev/null +++ b/.github/workflows/documentation-disable-list-audit.yml @@ -0,0 +1,40 @@ +name: opentelemetry.io documentation disable list audit + +on: + schedule: + - cron: "30 1 * * *" # daily at 1:30 UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + crawl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: temurin + java-version: 17 + + - name: Set up gradle + uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1 + + - name: Run instrumentation analyzer (identify any module changes) + run: ./gradlew :instrumentation-docs:runAnalysis + + - name: Run doc site audit + run: ./gradlew :instrumentation-docs:docSiteAudit + + workflow-notification: + permissions: + contents: read + issues: write + needs: + - crawl + if: always() + uses: ./.github/workflows/reusable-workflow-notification.yml + with: + success: ${{ needs.crawl.result == 'success' }} diff --git a/instrumentation-docs/build.gradle.kts b/instrumentation-docs/build.gradle.kts index 61da79361ca2..f2e4aec67e9f 100644 --- a/instrumentation-docs/build.gradle.kts +++ b/instrumentation-docs/build.gradle.kts @@ -26,4 +26,12 @@ tasks { mainClass.set("io.opentelemetry.instrumentation.docs.DocGeneratorApplication") classpath(sourceSets["main"].runtimeClasspath) } + + val docSiteAudit by registering(JavaExec::class) { + dependsOn(classes) + + systemProperty("basePath", project.rootDir) + mainClass.set("io.opentelemetry.instrumentation.docs.DocSynchronization") + classpath(sourceSets["main"].runtimeClasspath) + } } diff --git a/instrumentation-docs/readme.md b/instrumentation-docs/readme.md index 4772b83bd886..c4d76f7706b2 100644 --- a/instrumentation-docs/readme.md +++ b/instrumentation-docs/readme.md @@ -196,3 +196,17 @@ data will be excluded from git and just generated on demand. Each file has a `when` value along with the list of metrics that indicates whether the telemetry is emitted by default or via a configuration option. + +## Doc Synchronization + +The documentation site has a section that lists all the instrumentations in the context of +documenting how to disable them. + +We have a class `DocSynchronization` that runs a check against our instrumentation-list.yaml file to +identify when we have missing entries, so we know to go update them. + +You can run this via: + +`./gradlew :instrumentation-docs:docSiteAudit` + +This is setup to run nightly in a github action. 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 new file mode 100644 index 000000000000..343a614e8c30 --- /dev/null +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocSynchronization.java @@ -0,0 +1,196 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs; + +import static java.lang.System.exit; + +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.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.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. + */ +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"; + + // 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") + 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; + } + + /** + * 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; + } + + 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); + + 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()); + exit(1); + } + + } catch (IOException | InterruptedException e) { + logger.severe("Error fetching instrumentation list: " + e.getMessage()); + logger.severe(Arrays.toString(e.getStackTrace())); + exit(1); + } + } +} 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 new file mode 100644 index 000000000000..510c7721e9cc --- /dev/null +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/DocSynchronizationTest.java @@ -0,0 +1,146 @@ +/* + * 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(); + } +}