diff --git a/instrumentation-docs/build.gradle.kts b/instrumentation-docs/build.gradle.kts index b733a3ce8d8a..b9f5a0634b48 100644 --- a/instrumentation-docs/build.gradle.kts +++ b/instrumentation-docs/build.gradle.kts @@ -22,6 +22,7 @@ tasks { val runAnalysis by registering(JavaExec::class) { dependsOn(classes) + systemProperty("basePath", project.rootDir) mainClass.set("io.opentelemetry.instrumentation.docs.DocGeneratorApplication") classpath(sourceSets["main"].runtimeClasspath) } diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocGeneratorApplication.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocGeneratorApplication.java index 44a38ab8bea9..adf17c29cfcd 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocGeneratorApplication.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/DocGeneratorApplication.java @@ -7,13 +7,11 @@ import static java.util.Locale.Category.FORMAT; -import com.fasterxml.jackson.core.JsonProcessingException; import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule; import io.opentelemetry.instrumentation.docs.utils.FileManager; import io.opentelemetry.instrumentation.docs.utils.YamlHelper; import java.io.BufferedWriter; import java.io.IOException; -import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; @@ -26,20 +24,25 @@ public class DocGeneratorApplication { private static final Logger logger = Logger.getLogger(DocGeneratorApplication.class.getName()); - public static void main(String[] args) throws JsonProcessingException { - FileManager fileManager = new FileManager("instrumentation/"); + public static void main(String[] args) throws IOException { + // Identify path to repo so we can use absolute paths + String baseRepoPath = System.getProperty("basePath"); + if (baseRepoPath == null) { + baseRepoPath = "./"; + } else { + baseRepoPath += "/"; + } + + FileManager fileManager = new FileManager(baseRepoPath); List modules = new InstrumentationAnalyzer(fileManager).analyze(); try (BufferedWriter writer = - Files.newBufferedWriter( - Paths.get("docs/instrumentation-list.yaml"), Charset.defaultCharset())) { + Files.newBufferedWriter(Paths.get(baseRepoPath + "docs/instrumentation-list.yaml"))) { writer.write("# This file is generated and should not be manually edited.\n"); writer.write("# The structure and contents are a work in progress and subject to change.\n"); writer.write( "# For more information see: https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13468\n\n"); YamlHelper.generateInstrumentationYaml(modules, writer); - } catch (IOException e) { - logger.severe("Error writing instrumentation list: " + e.getMessage()); } printStats(modules); diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzer.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzer.java index 5489c61256eb..771b905d2330 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzer.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzer.java @@ -7,15 +7,16 @@ import static io.opentelemetry.instrumentation.docs.parsers.GradleParser.parseGradleFile; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.exc.ValueInstantiationException; import io.opentelemetry.instrumentation.docs.internal.DependencyInfo; import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics; import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule; import io.opentelemetry.instrumentation.docs.internal.InstrumentationType; +import io.opentelemetry.instrumentation.docs.parsers.MetricParser; import io.opentelemetry.instrumentation.docs.utils.FileManager; import io.opentelemetry.instrumentation.docs.utils.InstrumentationPath; import io.opentelemetry.instrumentation.docs.utils.YamlHelper; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -41,7 +42,7 @@ class InstrumentationAnalyzer { * @return a list of {@link InstrumentationModule} objects with aggregated types */ public static List convertToInstrumentationModules( - List paths) { + String rootPath, List paths) { Map moduleMap = new HashMap<>(); for (InstrumentationPath path : paths) { @@ -50,7 +51,7 @@ public static List convertToInstrumentationModules( moduleMap.put( key, new InstrumentationModule.Builder() - .srcPath(path.srcPath().replace("/javaagent", "").replace("/library", "")) + .srcPath(sanitizePathName(rootPath, path.srcPath())) .instrumentationName(path.instrumentationName()) .namespace(path.namespace()) .group(path.group()) @@ -61,16 +62,21 @@ public static List convertToInstrumentationModules( return new ArrayList<>(moduleMap.values()); } + private static String sanitizePathName(String rootPath, String path) { + return path.replace(rootPath, "").replace("/javaagent", "").replace("/library", ""); + } + /** * Traverses the given root directory to find all instrumentation paths and then analyzes them. - * Extracts version information from each instrumentation's build.gradle file, and other - * information from metadata.yaml files. + * Extracts version information from each instrumentation's build.gradle file, metric data from + * files in the .telemetry directories, and other information from metadata.yaml files. * * @return a list of {@link InstrumentationModule} */ - List analyze() throws JsonProcessingException { + List analyze() throws IOException { List paths = fileManager.getInstrumentationPaths(); - List modules = convertToInstrumentationModules(paths); + List modules = + convertToInstrumentationModules(fileManager.rootDir(), paths); for (InstrumentationModule module : modules) { List gradleFiles = fileManager.findBuildGradleFiles(module.getSrcPath()); @@ -86,12 +92,10 @@ List analyze() throws JsonProcessingException { } } - String emittedMetrics = fileManager.getMetrics(module.getSrcPath()); - if (emittedMetrics != null) { - EmittedMetrics metrics = YamlHelper.emittedMetricsParser(emittedMetrics); - if (metrics != null && metrics.getMetrics() != null) { - module.setMetrics(metrics.getMetrics()); - } + EmittedMetrics metrics = + MetricParser.getMetricsFromFiles(fileManager.rootDir(), module.getSrcPath()); + if (!metrics.getMetrics().isEmpty()) { + module.setMetrics(metrics.getMetrics()); } } return modules; @@ -100,7 +104,7 @@ List analyze() throws JsonProcessingException { void analyzeVersions(List files, InstrumentationModule module) { Map> versions = new HashMap<>(); for (String file : files) { - String fileContents = fileManager.readFileToString(file); + String fileContents = FileManager.readFileToString(file); if (fileContents == null) { continue; } diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/MetricParser.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/MetricParser.java new file mode 100644 index 000000000000..32fc6cf3e09c --- /dev/null +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/MetricParser.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.parsers; + +import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics; +import io.opentelemetry.instrumentation.docs.utils.FileManager; +import io.opentelemetry.instrumentation.docs.utils.YamlHelper; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.util.stream.Stream; + +public class MetricParser { + private static final Logger logger = Logger.getLogger(MetricParser.class.getName()); + + /** + * Looks for metric files in the .telemetry directory, and combines them into a single list of + * metrics. + * + * @param instrumentationDirectory the directory to traverse + * @return contents of aggregated files + */ + public static EmittedMetrics getMetricsFromFiles( + String rootDir, String instrumentationDirectory) { + StringBuilder metricsContent = new StringBuilder("metrics:\n"); + Path telemetryDir = Paths.get(rootDir + "/" + instrumentationDirectory, ".telemetry"); + + if (Files.exists(telemetryDir) && Files.isDirectory(telemetryDir)) { + try (Stream files = Files.list(telemetryDir)) { + files + .filter(path -> path.getFileName().toString().startsWith("metrics-")) + .forEach( + path -> { + String content = FileManager.readFileToString(path.toString()); + if (content != null) { + // Skip the first line of yaml ("metrics:") so we can aggregate into one list + int firstNewline = content.indexOf('\n'); + if (firstNewline != -1) { + String contentWithoutFirstLine = content.substring(firstNewline + 1); + metricsContent.append(contentWithoutFirstLine); + } + } + }); + } catch (IOException e) { + logger.severe("Error reading metrics files: " + e.getMessage()); + } + } + + return parseMetrics(metricsContent.toString()); + } + + /** + * Takes in a raw string representation of the aggregated EmittedMetrics yaml, deduplicates the + * metrics by name and then returns a new EmittedMetrics object. + * + * @param input raw string representation of EmittedMetrics yaml + * @return EmittedMetrics + */ + // visible for testing + public static EmittedMetrics parseMetrics(String input) { + EmittedMetrics metrics = YamlHelper.emittedMetricsParser(input); + if (metrics.getMetrics() == null) { + return new EmittedMetrics(Collections.emptyList()); + } + + // deduplicate metrics by name + Map deduplicatedMetrics = new HashMap<>(); + for (EmittedMetrics.Metric metric : metrics.getMetrics()) { + deduplicatedMetrics.put(metric.getName(), metric); + } + + List uniqueMetrics = new ArrayList<>(deduplicatedMetrics.values()); + return new EmittedMetrics(uniqueMetrics); + } + + private MetricParser() {} +} diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/FileManager.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/FileManager.java index 602c65abb129..f142cded4274 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/FileManager.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/FileManager.java @@ -17,25 +17,17 @@ import java.util.stream.Stream; import javax.annotation.Nullable; -public class FileManager { +public record FileManager(String rootDir) { private static final Logger logger = Logger.getLogger(FileManager.class.getName()); - private final String rootDir; - public FileManager(String rootDir) { - this.rootDir = rootDir; - } - - public List getInstrumentationPaths() { - Path rootPath = Paths.get(rootDir); + public List getInstrumentationPaths() throws IOException { + Path rootPath = Paths.get(rootDir + "instrumentation"); try (Stream walk = Files.walk(rootPath)) { return walk.filter(Files::isDirectory) .filter(dir -> isValidInstrumentationPath(dir.toString())) .map(dir -> parseInstrumentationPath(dir.toString())) .collect(Collectors.toList()); - } catch (IOException e) { - logger.severe("Error traversing directory: " + e.getMessage()); - return new ArrayList<>(); } } @@ -85,7 +77,7 @@ public static boolean isValidInstrumentationPath(String filePath) { } public List findBuildGradleFiles(String instrumentationDirectory) { - Path rootPath = Paths.get(instrumentationDirectory); + Path rootPath = Paths.get(rootDir + instrumentationDirectory); try (Stream walk = Files.walk(rootPath)) { return walk.filter(Files::isRegularFile) @@ -103,7 +95,7 @@ public List findBuildGradleFiles(String instrumentationDirectory) { @Nullable public String getMetaDataFile(String instrumentationDirectory) { - String metadataFile = instrumentationDirectory + "/metadata.yaml"; + String metadataFile = rootDir + instrumentationDirectory + "/metadata.yaml"; if (Files.exists(Paths.get(metadataFile))) { return readFileToString(metadataFile); } @@ -111,7 +103,7 @@ public String getMetaDataFile(String instrumentationDirectory) { } @Nullable - public String readFileToString(String filePath) { + public static String readFileToString(String filePath) { try { return Files.readString(Paths.get(filePath)); } catch (IOException e) { @@ -119,33 +111,4 @@ public String readFileToString(String filePath) { return null; } } - - /** - * Looks for metric files in the .telemetry directory - * - * @param instrumentationDirectory the directory to traverse - * @return contents of file - */ - public String getMetrics(String instrumentationDirectory) { - StringBuilder metricsContent = new StringBuilder(); - Path telemetryDir = Paths.get(instrumentationDirectory, ".telemetry"); - - if (Files.exists(telemetryDir) && Files.isDirectory(telemetryDir)) { - try (Stream files = Files.list(telemetryDir)) { - files - .filter(path -> path.getFileName().toString().startsWith("metrics-")) - .forEach( - path -> { - String content = readFileToString(path.toString()); - if (content != null) { - metricsContent.append(content).append("\n"); - } - }); - } catch (IOException e) { - logger.severe("Error reading metrics files: " + e.getMessage()); - } - } - - return metricsContent.toString(); - } } diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzerTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzerTest.java index 4d1f312255d8..ee39e6c1d84c 100644 --- a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzerTest.java +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzerTest.java @@ -41,7 +41,7 @@ void testConvertToInstrumentationModule() { InstrumentationType.LIBRARY)); List modules = - InstrumentationAnalyzer.convertToInstrumentationModules(paths); + InstrumentationAnalyzer.convertToInstrumentationModules("test", paths); assertThat(modules.size()).isEqualTo(2); diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/parsers/MetricParserTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/parsers/MetricParserTest.java new file mode 100644 index 000000000000..531d40e720fc --- /dev/null +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/parsers/MetricParserTest.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.parsers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mockStatic; + +import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics; +import io.opentelemetry.instrumentation.docs.utils.FileManager; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; + +class MetricParserTest { + + @Test + void parseMetricsDeduplicatesMetricsByName() { + String input = + """ + metrics: + - name: metric1 + type: counter + - name: metric1 + type: counter + - name: metric2 + type: gauge + """; + + EmittedMetrics result = MetricParser.parseMetrics(input); + List metricNames = + result.getMetrics().stream().map(EmittedMetrics.Metric::getName).sorted().toList(); + + assertThat(metricNames).hasSize(2); + assertThat(metricNames).containsExactly("metric1", "metric2"); + } + + @Test + void parseMetricsHandlesEmptyInput() { + String input = "metrics:\n"; + EmittedMetrics result = MetricParser.parseMetrics(input); + assertThat(result.getMetrics()).isEmpty(); + } + + @Test + void getMetricsFromFilesCombinesFilesCorrectly(@TempDir Path tempDir) throws IOException { + Path telemetryDir = Files.createDirectories(tempDir.resolve(".telemetry")); + + String file1Content = "metrics:\n - name: metric1\n type: counter\n"; + String file2Content = "metrics:\n - name: metric2\n type: gauge\n"; + + Files.writeString(telemetryDir.resolve("metrics-1.yaml"), file1Content); + Files.writeString(telemetryDir.resolve("metrics-2.yaml"), file2Content); + + // Create a non-metrics file that should be ignored + Files.writeString(telemetryDir.resolve("other-file.yaml"), "some content"); + + try (MockedStatic fileManagerMock = mockStatic(FileManager.class)) { + fileManagerMock + .when( + () -> FileManager.readFileToString(telemetryDir.resolve("metrics-1.yaml").toString())) + .thenReturn(file1Content); + fileManagerMock + .when( + () -> FileManager.readFileToString(telemetryDir.resolve("metrics-2.yaml").toString())) + .thenReturn(file2Content); + + EmittedMetrics result = MetricParser.getMetricsFromFiles(tempDir.toString(), ""); + + assertThat(result.getMetrics()).hasSize(2); + List metricNames = + result.getMetrics().stream().map(EmittedMetrics.Metric::getName).sorted().toList(); + assertThat(metricNames).containsExactly("metric1", "metric2"); + } + } + + @Test + void getMetricsFromFilesHandlesNonexistentDirectory() { + EmittedMetrics result = MetricParser.getMetricsFromFiles("/nonexistent", "path"); + assertThat(result.getMetrics()).isEmpty(); + } +} diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/FileManagerTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/FileManagerTest.java index 9e248a0c3eb7..feeacee17fda 100644 --- a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/FileManagerTest.java +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/FileManagerTest.java @@ -24,7 +24,7 @@ class FileManagerTest { @BeforeEach void setUp() { - fileManager = new FileManager(tempDir.toString()); + fileManager = new FileManager(tempDir.toString() + "/"); } @Test @@ -52,13 +52,4 @@ void testExcludesCommonModules() { "instrumentation/elasticsearch/elasticsearch-rest-common-5.0")) .isFalse(); } - - @Test - void testFindBuildGradleFiles() throws IOException { - Path gradleFile = Files.createFile(tempDir.resolve("build.gradle.kts")); - Path nonGradleFile = Files.createFile(tempDir.resolve("gradle.properties")); - List gradleFiles = fileManager.findBuildGradleFiles(tempDir.toString()); - assertThat(gradleFiles).contains(gradleFile.toString()); - assertThat(gradleFiles).doesNotContain(nonGradleFile.toString()); - } }