diff --git a/instrumentation-docs/readme.md b/instrumentation-docs/readme.md index c0a1fe5c93c2..1055427f21c8 100644 --- a/instrumentation-docs/readme.md +++ b/instrumentation-docs/readme.md @@ -155,8 +155,13 @@ public class SpringWebInstrumentationModule extends InstrumentationModule * Short description of what the instrumentation does * target_versions * List of supported versions by the module, broken down by `library` or `javaagent` support -* scope - * Name: The scope name of the instrumentation, `io.opentelemetry.{instrumentation name}` +* scope (See [instrumentation-scope](https://opentelemetry.io/docs/specs/otel/common/instrumentation-scope/) + docs) + * name: The scope name of the instrumentation, `io.opentelemetry.{instrumentation name}` + * schema_url: Location of the telemetry schema that the instrumentation’s emitted telemetry + conforms to. (See [telemetry schema docs](https://opentelemetry.io/docs/specs/otel/schemas/#schema-url)) + * attributes: The instrumentation scope’s optional attributes provide additional information + about the scope. * configuration settings * List of settings that are available for the instrumentation module * Each setting has a name, description, type, and default value 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 5e3d26f3380f..a14021d36a3c 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 @@ -14,6 +14,7 @@ import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule; import io.opentelemetry.instrumentation.docs.internal.InstrumentationType; import io.opentelemetry.instrumentation.docs.internal.TelemetryMerger; +import io.opentelemetry.instrumentation.docs.parsers.EmittedScopeParser; import io.opentelemetry.instrumentation.docs.parsers.GradleParser; import io.opentelemetry.instrumentation.docs.parsers.MetricParser; import io.opentelemetry.instrumentation.docs.parsers.ModuleParser; @@ -21,6 +22,7 @@ import io.opentelemetry.instrumentation.docs.utils.FileManager; import io.opentelemetry.instrumentation.docs.utils.InstrumentationPath; import io.opentelemetry.instrumentation.docs.utils.YamlHelper; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import java.io.IOException; import java.util.List; import java.util.Map; @@ -70,6 +72,11 @@ private void enrichModule(InstrumentationModule module) throws IOException { // Handle telemetry merging (manual + emitted) setMergedTelemetry(module, metaData); + + InstrumentationScopeInfo scopeInfo = EmittedScopeParser.getScope(fileManager, module); + if (scopeInfo != null) { + module.setScopeInfo(scopeInfo); + } } @Nullable diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/EmittedScope.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/EmittedScope.java new file mode 100644 index 000000000000..17f832f2a088 --- /dev/null +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/EmittedScope.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.internal; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * Representation of Scopes emitted the tests in a module. This class is internal and is hence not + * for public use. Its APIs are unstable and can change at any time. + */ +public class EmittedScope { + @Nullable private List scopes; + + public EmittedScope() {} + + public EmittedScope(List scopes) { + this.scopes = scopes; + } + + @Nullable + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + public static class Scope { + @Nullable private String name; + @Nullable private String version; + @Nullable private String schemaUrl; + @Nullable private Map attributes; + + public Scope() {} + + public Scope(String name, String version, String schemaUrl, Map attributes) { + this.name = name; + this.version = version; + this.schemaUrl = schemaUrl; + this.attributes = attributes; + } + + @Nullable + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Nullable + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Nullable + public String getSchemaUrl() { + return schemaUrl; + } + + public void setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + } + + @Nullable + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Scope scope)) { + return false; + } + + if (!Objects.equals(name, scope.name)) { + return false; + } + if (!Objects.equals(version, scope.version)) { + return false; + } + if (!Objects.equals(schemaUrl, scope.schemaUrl)) { + return false; + } + return Objects.equals(attributes, scope.attributes); + } + + @Override + public int hashCode() { + return Objects.hash(name, version, schemaUrl, attributes); + } + } +} diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/InstrumentationModule.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/InstrumentationModule.java index 1b13cb8eb55e..67568690b499 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/InstrumentationModule.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/InstrumentationModule.java @@ -27,7 +27,7 @@ public class InstrumentationModule { private final String instrumentationName; private final String namespace; private final String group; - private final InstrumentationScopeInfo scopeInfo; + private InstrumentationScopeInfo scopeInfo; private Map> metrics; private Map> spans; @@ -56,7 +56,10 @@ public InstrumentationModule(Builder builder) { this.metadata = builder.metadata; this.targetVersions = builder.targetVersions; this.minJavaVersion = builder.minJavaVersion; - this.scopeInfo = InstrumentationScopeInfo.create("io.opentelemetry." + instrumentationName); + this.scopeInfo = + Objects.requireNonNullElseGet( + builder.scopeInfo, + () -> InstrumentationScopeInfo.create("io.opentelemetry." + instrumentationName)); } public String getSrcPath() { @@ -109,6 +112,10 @@ public void setTargetVersions(Map> targetVersio this.targetVersions = targetVersions; } + public void setScopeInfo(InstrumentationScopeInfo scopeInfo) { + this.scopeInfo = scopeInfo; + } + public void setMetadata(InstrumentationMetadata metadata) { this.metadata = metadata; } @@ -136,6 +143,7 @@ public static class Builder { @Nullable private String group; @Nullable private Integer minJavaVersion; @Nullable private InstrumentationMetadata metadata; + @Nullable private InstrumentationScopeInfo scopeInfo; @Nullable private Map> targetVersions; @Nullable private Map> metrics; @Nullable private Map> spans; @@ -158,6 +166,12 @@ public Builder namespace(String namespace) { return this; } + @CanIgnoreReturnValue + public Builder scope(InstrumentationScopeInfo scope) { + this.scopeInfo = scope; + return this; + } + @CanIgnoreReturnValue public Builder minJavaVersion(Integer minJavaVersion) { this.minJavaVersion = minJavaVersion; diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/EmittedScopeParser.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/EmittedScopeParser.java new file mode 100644 index 000000000000..2a740f3b9ba8 --- /dev/null +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/EmittedScopeParser.java @@ -0,0 +1,151 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.parsers; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.docs.internal.EmittedScope; +import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule; +import io.opentelemetry.instrumentation.docs.utils.FileManager; +import io.opentelemetry.instrumentation.docs.utils.YamlHelper; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.common.InstrumentationScopeInfoBuilder; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +/** + * This class is responsible for parsing scope files from the `.telemetry` directory of an + * instrumentation module and collecting all unique scopes. + */ +public class EmittedScopeParser { + private static final Logger logger = Logger.getLogger(EmittedScopeParser.class.getName()); + + @Nullable + public static InstrumentationScopeInfo getScope( + FileManager fileManager, InstrumentationModule module) { + Set scopes = + EmittedScopeParser.getScopesFromFiles(fileManager.rootDir(), module.getSrcPath()); + if (scopes.isEmpty()) { + return null; + } + + EmittedScope.Scope scope = + scopes.stream() + .filter( + item -> + item.getName() != null + && item.getName().contains(module.getInstrumentationName())) + .findFirst() + .orElse(null); + if (scope == null) { + return null; + } + + String instrumentationName = "io.opentelemetry." + module.getInstrumentationName(); + InstrumentationScopeInfoBuilder builder = InstrumentationScopeInfo.builder(instrumentationName); + + // This will identify any module that might deviate from the standard naming convention + if (scope.getName() != null && !scope.getName().equals(instrumentationName)) { + logger.severe( + "Scope name mismatch. Expected: " + instrumentationName + ", got: " + scope.getName()); + } + + if (scope.getSchemaUrl() != null) { + builder.setSchemaUrl(scope.getSchemaUrl()); + } + if (scope.getAttributes() != null) { + builder.setAttributes(convertMapToAttributes(scope.getAttributes())); + } + + return builder.build(); + } + + /** + * Converts a {@code Map} to Attributes. + * + * @param attributeMap the map of attributes from YAML + * @return Attributes + */ + private static Attributes convertMapToAttributes(Map attributeMap) { + AttributesBuilder builder = Attributes.builder(); + for (Map.Entry entry : attributeMap.entrySet()) { + Object value = entry.getValue(); + if (value instanceof String string) { + builder.put(stringKey(entry.getKey()), string); + } else if (value instanceof Long longValue) { + builder.put(longKey(entry.getKey()), longValue); + } else if (value instanceof Integer intValue) { + builder.put(longKey(entry.getKey()), intValue.longValue()); + } else if (value instanceof Double doubleValue) { + builder.put(doubleKey(entry.getKey()), doubleValue); + } else if (value instanceof Boolean boolValue) { + builder.put(booleanKey(entry.getKey()), boolValue); + } else { + // Fallback to string representation for unknown types + builder.put(stringKey(entry.getKey()), String.valueOf(value)); + } + } + return builder.build(); + } + + /** + * Looks for scope files in the .telemetry directory and collects all unique scopes. + * + * @param rootDir the root directory + * @param instrumentationDirectory the instrumentation directory relative to root + * @return set of all unique scopes found in scope files + */ + public static Set getScopesFromFiles( + String rootDir, String instrumentationDirectory) { + Path telemetryDir = Paths.get(rootDir + "/" + instrumentationDirectory, ".telemetry"); + + Set allScopes = new HashSet<>(); + + if (Files.exists(telemetryDir) && Files.isDirectory(telemetryDir)) { + try (Stream files = Files.list(telemetryDir)) { + files + .filter(path -> path.getFileName().toString().startsWith("scope-")) + .forEach( + path -> { + String content = FileManager.readFileToString(path.toString()); + if (content != null) { + EmittedScope parsed; + try { + parsed = YamlHelper.emittedScopeParser(content); + } catch (RuntimeException e) { + logger.severe("Error parsing scope file (" + path + "): " + e.getMessage()); + return; + } + + List scopes = parsed.getScopes(); + if (scopes != null) { + allScopes.addAll(scopes); + } + } + }); + } catch (IOException e) { + logger.severe("Error reading scope files: " + e.getMessage()); + } + } + return allScopes; + } + + private EmittedScopeParser() {} +} diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/YamlHelper.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/YamlHelper.java index e34dee20ffd6..4708879376b3 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/YamlHelper.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/utils/YamlHelper.java @@ -11,6 +11,7 @@ import io.opentelemetry.instrumentation.docs.internal.ConfigurationOption; import io.opentelemetry.instrumentation.docs.internal.ConfigurationType; import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics; +import io.opentelemetry.instrumentation.docs.internal.EmittedScope; import io.opentelemetry.instrumentation.docs.internal.EmittedSpans; import io.opentelemetry.instrumentation.docs.internal.InstrumentationClassification; import io.opentelemetry.instrumentation.docs.internal.InstrumentationMetadata; @@ -228,6 +229,18 @@ private static void addMetadataProperties( private static Map getScopeMap(InstrumentationModule module) { Map scopeMap = new LinkedHashMap<>(); scopeMap.put("name", module.getScopeInfo().getName()); + if (module.getScopeInfo().getSchemaUrl() != null) { + scopeMap.put("schema_url", module.getScopeInfo().getSchemaUrl()); + } + if (module.getScopeInfo().getAttributes() != null + && !module.getScopeInfo().getAttributes().isEmpty()) { + Map attributesMap = new LinkedHashMap<>(); + module + .getScopeInfo() + .getAttributes() + .forEach((key, value) -> attributesMap.put(key.getKey(), value)); + scopeMap.put("attributes", attributesMap); + } return scopeMap; } @@ -313,6 +326,10 @@ public static InstrumentationMetadata metaDataParser(String input) return mapper.readValue(input, InstrumentationMetadata.class); } + public static EmittedScope emittedScopeParser(String input) { + return new Yaml().loadAs(input, EmittedScope.class); + } + public static EmittedMetrics emittedMetricsParser(String input) throws JsonProcessingException { return mapper.readValue(input, EmittedMetrics.class); } diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/parsers/EmittedScopeParserTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/parsers/EmittedScopeParserTest.java new file mode 100644 index 000000000000..ba31c8fb9af0 --- /dev/null +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/parsers/EmittedScopeParserTest.java @@ -0,0 +1,439 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.parsers; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.docs.internal.EmittedScope; +import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule; +import io.opentelemetry.instrumentation.docs.utils.FileManager; +import io.opentelemetry.instrumentation.docs.utils.YamlHelper; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class EmittedScopeParserTest { + + @Test + void testEmittedScopeParser() { + String yamlContent = + """ + scopes: + - name: io.opentelemetry.alibaba-druid-1.0 + version: 2.14.0-alpha-SNAPSHOT + schemaUrl: http://schema.org + - name: io.opentelemetry.another-scope + version: 1.0.0 + schemaUrl: null + """; + + EmittedScope emittedScope = YamlHelper.emittedScopeParser(yamlContent); + + assertThat(emittedScope.getScopes()).isNotNull(); + assertThat(emittedScope.getScopes()).hasSize(2); + + assertThat(emittedScope.getScopes().get(0).getName()) + .isEqualTo("io.opentelemetry.alibaba-druid-1.0"); + assertThat(emittedScope.getScopes().get(0).getVersion()).isEqualTo("2.14.0-alpha-SNAPSHOT"); + assertThat(emittedScope.getScopes().get(0).getSchemaUrl()).isEqualTo("http://schema.org"); + + assertThat(emittedScope.getScopes().get(1).getName()) + .isEqualTo("io.opentelemetry.another-scope"); + assertThat(emittedScope.getScopes().get(1).getVersion()).isEqualTo("1.0.0"); + assertThat(emittedScope.getScopes().get(1).getSchemaUrl()).isNull(); + } + + @Test + void testGetScopesFromFilesSingleFile(@TempDir Path tempDir) throws IOException { + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + String scopeContent = + """ + scopes: + - name: io.opentelemetry.test-1.0 + version: 1.0.0 + schemaUrl: https://opentelemetry.io/schemas/1.0.0 + - name: io.opentelemetry.common + version: 2.0.0 + schemaUrl: null + """; + + Files.writeString(telemetryDir.resolve("scope-abc123.yaml"), scopeContent); + + Set scopes = + EmittedScopeParser.getScopesFromFiles(tempDir.toString(), "test-instrumentation"); + + assertThat(scopes).hasSize(2); + assertThat(scopes) + .extracting(EmittedScope.Scope::getName) + .containsExactlyInAnyOrder("io.opentelemetry.test-1.0", "io.opentelemetry.common"); + } + + @Test + void testGetScopesFromFilesMultipleFiles(@TempDir Path tempDir) throws IOException { + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + String scopeContent1 = + """ + scopes: + - name: io.opentelemetry.test-1.0 + version: 1.0.0 + schemaUrl: https://opentelemetry.io/schemas/1.0.0 + """; + + // has overlapping and new scopes + String scopeContent2 = + """ + scopes: + - name: io.opentelemetry.test-1.0 + version: 1.0.0 + schemaUrl: https://opentelemetry.io/schemas/1.0.0 + - name: io.opentelemetry.another-2.0 + version: 2.0.0 + schemaUrl: null + """; + + Files.writeString(telemetryDir.resolve("scope-file1.yaml"), scopeContent1); + Files.writeString(telemetryDir.resolve("scope-file2.yaml"), scopeContent2); + + Set scopes = + EmittedScopeParser.getScopesFromFiles(tempDir.toString(), "test-instrumentation"); + + // duplicates should be removed + assertThat(scopes).hasSize(2); + assertThat(scopes) + .extracting(EmittedScope.Scope::getName) + .containsExactlyInAnyOrder("io.opentelemetry.test-1.0", "io.opentelemetry.another-2.0"); + } + + @Test + void testGetScopesFromFilesNoScopeFiles(@TempDir Path tempDir) throws IOException { + // Create instrumentation directory but no scope files + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + // non-scope file + Files.writeString(telemetryDir.resolve("metrics-abc123.yaml"), "some: content"); + + // Parse should return empty set + Set scopes = + EmittedScopeParser.getScopesFromFiles(tempDir.toString(), "test-instrumentation"); + + assertThat(scopes).isEmpty(); + } + + @Test + void testGetScopeWithMatchingName(@TempDir Path tempDir) throws IOException { + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + String scopeContent = + """ + scopes: + - name: io.opentelemetry.test-lib-1.0 + version: 1.0.0 + schemaUrl: null + """; + + Files.writeString(telemetryDir.resolve("scope-abc123.yaml"), scopeContent); + + FileManager fileManager = new FileManager(tempDir + "/"); + InstrumentationModule module = + new InstrumentationModule.Builder() + .srcPath("test-instrumentation") + .instrumentationName("test-lib-1.0") + .namespace("test-lib") + .group("test-lib") + .build(); + + InstrumentationScopeInfo scopeInfo = EmittedScopeParser.getScope(fileManager, module); + + assertThat(scopeInfo).isNotNull(); + assertThat(scopeInfo.getName()).isEqualTo("io.opentelemetry.test-lib-1.0"); + assertThat(scopeInfo.getSchemaUrl()).isNull(); + } + + @Test + void testGetScopeWithSchemaUrl(@TempDir Path tempDir) throws IOException { + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + String scopeContent = + """ + scopes: + - name: io.opentelemetry.spring-web-6.0 + version: 2.14.0 + schemaUrl: https://opentelemetry.io/schemas/1.21.0 + """; + + Files.writeString(telemetryDir.resolve("scope-test.yaml"), scopeContent); + + FileManager fileManager = new FileManager(tempDir + "/"); + InstrumentationModule module = + new InstrumentationModule.Builder() + .srcPath("test-instrumentation") + .instrumentationName("spring-web-6.0") + .namespace("spring") + .group("spring") + .build(); + + InstrumentationScopeInfo scopeInfo = EmittedScopeParser.getScope(fileManager, module); + + assertThat(scopeInfo).isNotNull(); + assertThat(scopeInfo.getName()).isEqualTo("io.opentelemetry.spring-web-6.0"); + assertThat(scopeInfo.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.21.0"); + } + + @Test + void testGetScopeNoTelemetryDirectory(@TempDir Path tempDir) { + FileManager fileManager = new FileManager(tempDir.toString() + "/"); + InstrumentationModule module = + new InstrumentationModule.Builder() + .srcPath("test-instrumentation") + .instrumentationName("test-lib-1.0") + .namespace("test-lib") + .group("test-lib") + .build(); + + InstrumentationScopeInfo scopeInfo = EmittedScopeParser.getScope(fileManager, module); + + assertThat(scopeInfo).isNull(); + } + + @Test + void testGetScopeNoMatchingScope(@TempDir Path tempDir) throws IOException { + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + String scopeContent = + """ + scopes: + - name: io.opentelemetry.other-lib + version: 1.0.0 + schemaUrl: null + - name: io.opentelemetry.another-lib + version: 2.0.0 + schemaUrl: null + """; + + Files.writeString(telemetryDir.resolve("scope-abc123.yaml"), scopeContent); + + FileManager fileManager = new FileManager(tempDir + "/"); + InstrumentationModule module = + new InstrumentationModule.Builder() + .srcPath("test-instrumentation") + .instrumentationName("test-lib-1.0") + .namespace("test-lib") + .group("test-lib") + .build(); + + InstrumentationScopeInfo scopeInfo = EmittedScopeParser.getScope(fileManager, module); + + assertThat(scopeInfo).isNull(); + } + + @Test + void testGetScopeMultipleScopesOneMatches(@TempDir Path tempDir) throws IOException { + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + String scopeContent = + """ + scopes: + - name: io.opentelemetry.common + version: 1.0.0 + schemaUrl: null + - name: io.opentelemetry.hibernate-6.0 + version: 2.14.0 + schemaUrl: https://opentelemetry.io/schemas/1.21.0 + - name: io.opentelemetry.another + version: 3.0.0 + schemaUrl: null + """; + + Files.writeString(telemetryDir.resolve("scope-multi.yaml"), scopeContent); + + FileManager fileManager = new FileManager(tempDir + "/"); + InstrumentationModule module = + new InstrumentationModule.Builder() + .srcPath("test-instrumentation") + .instrumentationName("hibernate-6.0") + .namespace("hibernate") + .group("hibernate") + .build(); + + InstrumentationScopeInfo scopeInfo = EmittedScopeParser.getScope(fileManager, module); + + assertThat(scopeInfo).isNotNull(); + assertThat(scopeInfo.getName()).isEqualTo("io.opentelemetry.hibernate-6.0"); + assertThat(scopeInfo.getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.21.0"); + } + + @Test + void testGetScopeWithStringAttributes(@TempDir Path tempDir) throws IOException { + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + String scopeContent = + """ + scopes: + - name: io.opentelemetry.jdbc + version: 2.14.0 + schemaUrl: null + attributes: + test.key: test-value + another.key: another-value + """; + + Files.writeString(telemetryDir.resolve("scope-with-attrs.yaml"), scopeContent); + + FileManager fileManager = new FileManager(tempDir + "/"); + InstrumentationModule module = + new InstrumentationModule.Builder() + .srcPath("test-instrumentation") + .instrumentationName("jdbc") + .namespace("jdbc") + .group("jdbc") + .build(); + + InstrumentationScopeInfo scopeInfo = EmittedScopeParser.getScope(fileManager, module); + + assertThat(scopeInfo).isNotNull(); + assertThat(scopeInfo.getName()).isEqualTo("io.opentelemetry.jdbc"); + assertThat(scopeInfo.getAttributes()).isNotNull(); + assertThat(scopeInfo.getAttributes().get(stringKey("test.key"))).isEqualTo("test-value"); + assertThat(scopeInfo.getAttributes().get(stringKey("another.key"))).isEqualTo("another-value"); + } + + @Test + void testGetScopeWithMixedTypeAttributes(@TempDir Path tempDir) throws IOException { + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + String scopeContent = + """ + scopes: + - name: io.opentelemetry.test-lib + version: 1.0.0 + schemaUrl: null + attributes: + string.key: string-value + int.key: 123 + long.key: 9876543210 + double.key: 3.14 + bool.key: true + """; + + Files.writeString(telemetryDir.resolve("scope-mixed-attrs.yaml"), scopeContent); + + FileManager fileManager = new FileManager(tempDir + "/"); + InstrumentationModule module = + new InstrumentationModule.Builder() + .srcPath("test-instrumentation") + .instrumentationName("test-lib") + .namespace("test-lib") + .group("test-lib") + .build(); + + InstrumentationScopeInfo scopeInfo = EmittedScopeParser.getScope(fileManager, module); + + assertThat(scopeInfo).isNotNull(); + assertThat(scopeInfo.getAttributes()).isNotNull(); + assertThat(scopeInfo.getAttributes().get(stringKey("string.key"))).isEqualTo("string-value"); + assertThat(scopeInfo.getAttributes().get(AttributeKey.longKey("int.key"))).isEqualTo(123L); + assertThat(scopeInfo.getAttributes().get(AttributeKey.longKey("long.key"))) + .isEqualTo(9876543210L); + assertThat(scopeInfo.getAttributes().get(AttributeKey.doubleKey("double.key"))).isEqualTo(3.14); + assertThat(scopeInfo.getAttributes().get(AttributeKey.booleanKey("bool.key"))).isTrue(); + } + + @Test + void testScopeEqualityWithAttributes(@TempDir Path tempDir) throws IOException { + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + String scopeContent1 = + """ + scopes: + - name: io.opentelemetry.test + version: 1.0.0 + schemaUrl: null + attributes: + key1: value1 + """; + + String scopeContent2 = + """ + scopes: + - name: io.opentelemetry.test + version: 1.0.0 + schemaUrl: null + attributes: + key1: value1 + """; + + Files.writeString(telemetryDir.resolve("scope-1.yaml"), scopeContent1); + Files.writeString(telemetryDir.resolve("scope-2.yaml"), scopeContent2); + + Set scopes = + EmittedScopeParser.getScopesFromFiles(tempDir.toString(), "test-instrumentation"); + + assertThat(scopes).hasSize(1); + } + + @Test + void testScopeInequalityWithDifferentAttributes(@TempDir Path tempDir) throws IOException { + Path instrumentationDir = tempDir.resolve("test-instrumentation"); + Path telemetryDir = instrumentationDir.resolve(".telemetry"); + Files.createDirectories(telemetryDir); + + String scopeContent1 = + """ + scopes: + - name: io.opentelemetry.test + version: 1.0.0 + schemaUrl: null + attributes: + key1: value1 + """; + + String scopeContent2 = + """ + scopes: + - name: io.opentelemetry.test + version: 1.0.0 + schemaUrl: null + attributes: + key1: value2 + """; + + Files.writeString(telemetryDir.resolve("scope-1.yaml"), scopeContent1); + Files.writeString(telemetryDir.resolve("scope-2.yaml"), scopeContent2); + + Set scopes = + EmittedScopeParser.getScopesFromFiles(tempDir.toString(), "test-instrumentation"); + + assertThat(scopes).hasSize(2); + } +} diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/YamlHelperTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/YamlHelperTest.java index 1aa1ca1185a7..d66c44118f8a 100644 --- a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/YamlHelperTest.java +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/utils/YamlHelperTest.java @@ -11,6 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.core.JsonProcessingException; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.instrumentation.docs.internal.ConfigurationOption; import io.opentelemetry.instrumentation.docs.internal.ConfigurationType; import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics; @@ -21,6 +22,7 @@ import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule; import io.opentelemetry.instrumentation.docs.internal.InstrumentationType; import io.opentelemetry.instrumentation.docs.internal.TelemetryAttribute; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import java.io.BufferedWriter; import java.io.StringWriter; import java.util.ArrayList; @@ -54,6 +56,16 @@ void testPrintInstrumentationList() throws Exception { new InstrumentationModule.Builder() .srcPath("instrumentation/spring/spring-web/spring-web-6.0") .instrumentationName("spring-web-6.0") + .scope( + InstrumentationScopeInfo.builder("io.opentelemetry.spring-web-6.0") + .setVersion("2.14.0") + .setSchemaUrl("http:://www.schema.org") + .setAttributes( + Attributes.builder() + .put("instrumentation.type", "library") + .put("version.major", 6L) + .build()) + .build()) .namespace("spring") .group("spring") .targetVersions(targetVersions1) @@ -96,6 +108,10 @@ void testPrintInstrumentationList() throws Exception { minimum_java_version: 11 scope: name: io.opentelemetry.spring-web-6.0 + schema_url: http:://www.schema.org + attributes: + instrumentation.type: library + version.major: 6 target_versions: javaagent: - org.springframework:spring-web:[6.0.0,) diff --git a/smoke-tests/src/main/java/io/opentelemetry/smoketest/SmokeTestRunner.java b/smoke-tests/src/main/java/io/opentelemetry/smoketest/SmokeTestRunner.java index 5be8fcd0163c..7cd19e90575f 100644 --- a/smoke-tests/src/main/java/io/opentelemetry/smoketest/SmokeTestRunner.java +++ b/smoke-tests/src/main/java/io/opentelemetry/smoketest/SmokeTestRunner.java @@ -47,7 +47,8 @@ public void afterTestClass() throws IOException { if (Boolean.getBoolean("collectMetadata")) { String path = new File("").getAbsolutePath(); - MetaDataCollector.writeTelemetryToFiles(path, metricsByScope, tracesByScope); + MetaDataCollector.writeTelemetryToFiles( + path, metricsByScope, tracesByScope, instrumentationScopes); } } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java index 834815ea17fb..12dba1219478 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java @@ -68,7 +68,8 @@ public void afterTestClass() throws IOException { if (Boolean.getBoolean("collectMetadata")) { String path = new File("").getAbsolutePath(); - MetaDataCollector.writeTelemetryToFiles(path, metricsByScope, tracesByScope); + MetaDataCollector.writeTelemetryToFiles( + path, metricsByScope, tracesByScope, instrumentationScopes); } // additional library ignores are ignored during tests, because they can make it really diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java index b63bb7bd3550..245bc30dcb81 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java @@ -30,8 +30,10 @@ import java.util.Collection; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; @@ -52,6 +54,7 @@ public abstract class InstrumentationTestRunner { private final TestInstrumenters testInstrumenters; protected Map> metricsByScope = new HashMap<>(); + protected Set instrumentationScopes = new HashSet<>(); /** * Stores traces by scope, where each scope contains a map of span kinds to a map of attribute @@ -212,6 +215,11 @@ private void collectEmittedMetrics(List metrics) { if (!scopeMap.containsKey(metric.getName())) { scopeMap.put(metric.getName(), metric); } + + InstrumentationScopeInfo scopeInfo = metric.getInstrumentationScopeInfo(); + if (!scopeInfo.getName().equals("test")) { + instrumentationScopes.add(scopeInfo); + } } } @@ -235,6 +243,11 @@ private void collectEmittedSpans(List> spans) { spanKindMap.put(keyImpl, key.getValue() != null ? key.getKey().getType() : null); } } + + InstrumentationScopeInfo scopeInfo = span.getInstrumentationScopeInfo(); + if (!scopeInfo.getName().equals("test")) { + instrumentationScopes.add(scopeInfo); + } } } } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java index d21c95ba0135..e5ad63bf2f85 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java @@ -134,7 +134,8 @@ public void afterTestClass() throws IOException { if (Boolean.getBoolean("collectMetadata")) { String path = new File("").getAbsolutePath(); - MetaDataCollector.writeTelemetryToFiles(path, metricsByScope, tracesByScope); + MetaDataCollector.writeTelemetryToFiles( + path, metricsByScope, tracesByScope, instrumentationScopes); } } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/internal/MetaDataCollector.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/internal/MetaDataCollector.java index e352fbf935ad..e94a0a4e677f 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/internal/MetaDataCollector.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/internal/MetaDataCollector.java @@ -19,6 +19,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -41,12 +42,14 @@ public static void writeTelemetryToFiles( String path, Map> metricsByScope, Map, AttributeType>>> - spansByScopeAndKind) + spansByScopeAndKind, + java.util.Set instrumentationScopes) throws IOException { String moduleRoot = extractInstrumentationPath(path); writeMetricData(moduleRoot, metricsByScope); writeSpanData(moduleRoot, spansByScopeAndKind); + writeScopeData(moduleRoot, instrumentationScopes); } private static String extractInstrumentationPath(String path) { @@ -180,6 +183,39 @@ private static void writeMetricData( } } + private static void writeScopeData( + String instrumentationPath, Set instrumentationScopes) + throws IOException { + + if (instrumentationScopes == null || instrumentationScopes.isEmpty()) { + return; + } + + Path outputPath = + Paths.get(instrumentationPath, TMP_DIR, "scope-" + UUID.randomUUID() + ".yaml"); + try (BufferedWriter writer = Files.newBufferedWriter(outputPath.toFile().toPath(), UTF_8)) { + writer.write("scopes:\n"); + for (InstrumentationScopeInfo scope : instrumentationScopes) { + writer.write(" - name: " + scope.getName() + "\n"); + writer.write(" version: " + scope.getVersion() + "\n"); + writer.write(" schemaUrl: " + scope.getSchemaUrl() + "\n"); + if (scope.getAttributes() != null && !scope.getAttributes().isEmpty()) { + writer.write(" attributes:\n"); + scope + .getAttributes() + .forEach( + (key, value) -> { + try { + writer.write(" " + key + ": " + value + "\n"); + } catch (IOException e) { + throw new IllegalStateException(e); + } + }); + } + } + } + } + private static String sanitizeUnit(String unit) { return unit == null ? null : unit.replace("{", "").replace("}", ""); }