Skip to content

Commit eac36fb

Browse files
authored
Add scope schema url support to metadata (#15222)
1 parent 56a7c12 commit eac36fb

File tree

13 files changed

+825
-8
lines changed

13 files changed

+825
-8
lines changed

instrumentation-docs/readme.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,13 @@ public class SpringWebInstrumentationModule extends InstrumentationModule
155155
* Short description of what the instrumentation does
156156
* target_versions
157157
* List of supported versions by the module, broken down by `library` or `javaagent` support
158-
* scope
159-
* Name: The scope name of the instrumentation, `io.opentelemetry.{instrumentation name}`
158+
* scope (See [instrumentation-scope](https://opentelemetry.io/docs/specs/otel/common/instrumentation-scope/)
159+
docs)
160+
* name: The scope name of the instrumentation, `io.opentelemetry.{instrumentation name}`
161+
* schema_url: Location of the telemetry schema that the instrumentation’s emitted telemetry
162+
conforms to. (See [telemetry schema docs](https://opentelemetry.io/docs/specs/otel/schemas/#schema-url))
163+
* attributes: The instrumentation scope’s optional attributes provide additional information
164+
about the scope.
160165
* configuration settings
161166
* List of settings that are available for the instrumentation module
162167
* Each setting has a name, description, type, and default value

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule;
1515
import io.opentelemetry.instrumentation.docs.internal.InstrumentationType;
1616
import io.opentelemetry.instrumentation.docs.internal.TelemetryMerger;
17+
import io.opentelemetry.instrumentation.docs.parsers.EmittedScopeParser;
1718
import io.opentelemetry.instrumentation.docs.parsers.GradleParser;
1819
import io.opentelemetry.instrumentation.docs.parsers.MetricParser;
1920
import io.opentelemetry.instrumentation.docs.parsers.ModuleParser;
2021
import io.opentelemetry.instrumentation.docs.parsers.SpanParser;
2122
import io.opentelemetry.instrumentation.docs.utils.FileManager;
2223
import io.opentelemetry.instrumentation.docs.utils.InstrumentationPath;
2324
import io.opentelemetry.instrumentation.docs.utils.YamlHelper;
25+
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
2426
import java.io.IOException;
2527
import java.util.List;
2628
import java.util.Map;
@@ -70,6 +72,11 @@ private void enrichModule(InstrumentationModule module) throws IOException {
7072

7173
// Handle telemetry merging (manual + emitted)
7274
setMergedTelemetry(module, metaData);
75+
76+
InstrumentationScopeInfo scopeInfo = EmittedScopeParser.getScope(fileManager, module);
77+
if (scopeInfo != null) {
78+
module.setScopeInfo(scopeInfo);
79+
}
7380
}
7481

7582
@Nullable
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.docs.internal;
7+
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Objects;
11+
import javax.annotation.Nullable;
12+
13+
/**
14+
* Representation of Scopes emitted the tests in a module. This class is internal and is hence not
15+
* for public use. Its APIs are unstable and can change at any time.
16+
*/
17+
public class EmittedScope {
18+
@Nullable private List<Scope> scopes;
19+
20+
public EmittedScope() {}
21+
22+
public EmittedScope(List<Scope> scopes) {
23+
this.scopes = scopes;
24+
}
25+
26+
@Nullable
27+
public List<Scope> getScopes() {
28+
return scopes;
29+
}
30+
31+
public void setScopes(List<Scope> scopes) {
32+
this.scopes = scopes;
33+
}
34+
35+
/**
36+
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
37+
* any time.
38+
*/
39+
public static class Scope {
40+
@Nullable private String name;
41+
@Nullable private String version;
42+
@Nullable private String schemaUrl;
43+
@Nullable private Map<String, Object> attributes;
44+
45+
public Scope() {}
46+
47+
public Scope(String name, String version, String schemaUrl, Map<String, Object> attributes) {
48+
this.name = name;
49+
this.version = version;
50+
this.schemaUrl = schemaUrl;
51+
this.attributes = attributes;
52+
}
53+
54+
@Nullable
55+
public String getName() {
56+
return name;
57+
}
58+
59+
public void setName(String name) {
60+
this.name = name;
61+
}
62+
63+
@Nullable
64+
public String getVersion() {
65+
return version;
66+
}
67+
68+
public void setVersion(String version) {
69+
this.version = version;
70+
}
71+
72+
@Nullable
73+
public String getSchemaUrl() {
74+
return schemaUrl;
75+
}
76+
77+
public void setSchemaUrl(String schemaUrl) {
78+
this.schemaUrl = schemaUrl;
79+
}
80+
81+
@Nullable
82+
public Map<String, Object> getAttributes() {
83+
return attributes;
84+
}
85+
86+
public void setAttributes(Map<String, Object> attributes) {
87+
this.attributes = attributes;
88+
}
89+
90+
@Override
91+
public boolean equals(Object o) {
92+
if (this == o) {
93+
return true;
94+
}
95+
if (!(o instanceof Scope scope)) {
96+
return false;
97+
}
98+
99+
if (!Objects.equals(name, scope.name)) {
100+
return false;
101+
}
102+
if (!Objects.equals(version, scope.version)) {
103+
return false;
104+
}
105+
if (!Objects.equals(schemaUrl, scope.schemaUrl)) {
106+
return false;
107+
}
108+
return Objects.equals(attributes, scope.attributes);
109+
}
110+
111+
@Override
112+
public int hashCode() {
113+
return Objects.hash(name, version, schemaUrl, attributes);
114+
}
115+
}
116+
}

instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/InstrumentationModule.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class InstrumentationModule {
2727
private final String instrumentationName;
2828
private final String namespace;
2929
private final String group;
30-
private final InstrumentationScopeInfo scopeInfo;
30+
private InstrumentationScopeInfo scopeInfo;
3131
private Map<String, List<EmittedMetrics.Metric>> metrics;
3232
private Map<String, List<EmittedSpans.Span>> spans;
3333

@@ -56,7 +56,10 @@ public InstrumentationModule(Builder builder) {
5656
this.metadata = builder.metadata;
5757
this.targetVersions = builder.targetVersions;
5858
this.minJavaVersion = builder.minJavaVersion;
59-
this.scopeInfo = InstrumentationScopeInfo.create("io.opentelemetry." + instrumentationName);
59+
this.scopeInfo =
60+
Objects.requireNonNullElseGet(
61+
builder.scopeInfo,
62+
() -> InstrumentationScopeInfo.create("io.opentelemetry." + instrumentationName));
6063
}
6164

6265
public String getSrcPath() {
@@ -109,6 +112,10 @@ public void setTargetVersions(Map<InstrumentationType, Set<String>> targetVersio
109112
this.targetVersions = targetVersions;
110113
}
111114

115+
public void setScopeInfo(InstrumentationScopeInfo scopeInfo) {
116+
this.scopeInfo = scopeInfo;
117+
}
118+
112119
public void setMetadata(InstrumentationMetadata metadata) {
113120
this.metadata = metadata;
114121
}
@@ -136,6 +143,7 @@ public static class Builder {
136143
@Nullable private String group;
137144
@Nullable private Integer minJavaVersion;
138145
@Nullable private InstrumentationMetadata metadata;
146+
@Nullable private InstrumentationScopeInfo scopeInfo;
139147
@Nullable private Map<InstrumentationType, Set<String>> targetVersions;
140148
@Nullable private Map<String, List<EmittedMetrics.Metric>> metrics;
141149
@Nullable private Map<String, List<EmittedSpans.Span>> spans;
@@ -158,6 +166,12 @@ public Builder namespace(String namespace) {
158166
return this;
159167
}
160168

169+
@CanIgnoreReturnValue
170+
public Builder scope(InstrumentationScopeInfo scope) {
171+
this.scopeInfo = scope;
172+
return this;
173+
}
174+
161175
@CanIgnoreReturnValue
162176
public Builder minJavaVersion(Integer minJavaVersion) {
163177
this.minJavaVersion = minJavaVersion;
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.docs.parsers;
7+
8+
import static io.opentelemetry.api.common.AttributeKey.booleanKey;
9+
import static io.opentelemetry.api.common.AttributeKey.doubleKey;
10+
import static io.opentelemetry.api.common.AttributeKey.longKey;
11+
import static io.opentelemetry.api.common.AttributeKey.stringKey;
12+
13+
import io.opentelemetry.api.common.Attributes;
14+
import io.opentelemetry.api.common.AttributesBuilder;
15+
import io.opentelemetry.instrumentation.docs.internal.EmittedScope;
16+
import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule;
17+
import io.opentelemetry.instrumentation.docs.utils.FileManager;
18+
import io.opentelemetry.instrumentation.docs.utils.YamlHelper;
19+
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
20+
import io.opentelemetry.sdk.common.InstrumentationScopeInfoBuilder;
21+
import java.io.IOException;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.Paths;
25+
import java.util.HashSet;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Set;
29+
import java.util.logging.Logger;
30+
import java.util.stream.Stream;
31+
import javax.annotation.Nullable;
32+
33+
/**
34+
* This class is responsible for parsing scope files from the `.telemetry` directory of an
35+
* instrumentation module and collecting all unique scopes.
36+
*/
37+
public class EmittedScopeParser {
38+
private static final Logger logger = Logger.getLogger(EmittedScopeParser.class.getName());
39+
40+
@Nullable
41+
public static InstrumentationScopeInfo getScope(
42+
FileManager fileManager, InstrumentationModule module) {
43+
Set<EmittedScope.Scope> scopes =
44+
EmittedScopeParser.getScopesFromFiles(fileManager.rootDir(), module.getSrcPath());
45+
if (scopes.isEmpty()) {
46+
return null;
47+
}
48+
49+
EmittedScope.Scope scope =
50+
scopes.stream()
51+
.filter(
52+
item ->
53+
item.getName() != null
54+
&& item.getName().contains(module.getInstrumentationName()))
55+
.findFirst()
56+
.orElse(null);
57+
if (scope == null) {
58+
return null;
59+
}
60+
61+
String instrumentationName = "io.opentelemetry." + module.getInstrumentationName();
62+
InstrumentationScopeInfoBuilder builder = InstrumentationScopeInfo.builder(instrumentationName);
63+
64+
// This will identify any module that might deviate from the standard naming convention
65+
if (scope.getName() != null && !scope.getName().equals(instrumentationName)) {
66+
logger.severe(
67+
"Scope name mismatch. Expected: " + instrumentationName + ", got: " + scope.getName());
68+
}
69+
70+
if (scope.getSchemaUrl() != null) {
71+
builder.setSchemaUrl(scope.getSchemaUrl());
72+
}
73+
if (scope.getAttributes() != null) {
74+
builder.setAttributes(convertMapToAttributes(scope.getAttributes()));
75+
}
76+
77+
return builder.build();
78+
}
79+
80+
/**
81+
* Converts a {@code Map<String, Object>} to Attributes.
82+
*
83+
* @param attributeMap the map of attributes from YAML
84+
* @return Attributes
85+
*/
86+
private static Attributes convertMapToAttributes(Map<String, Object> attributeMap) {
87+
AttributesBuilder builder = Attributes.builder();
88+
for (Map.Entry<String, Object> entry : attributeMap.entrySet()) {
89+
Object value = entry.getValue();
90+
if (value instanceof String string) {
91+
builder.put(stringKey(entry.getKey()), string);
92+
} else if (value instanceof Long longValue) {
93+
builder.put(longKey(entry.getKey()), longValue);
94+
} else if (value instanceof Integer intValue) {
95+
builder.put(longKey(entry.getKey()), intValue.longValue());
96+
} else if (value instanceof Double doubleValue) {
97+
builder.put(doubleKey(entry.getKey()), doubleValue);
98+
} else if (value instanceof Boolean boolValue) {
99+
builder.put(booleanKey(entry.getKey()), boolValue);
100+
} else {
101+
// Fallback to string representation for unknown types
102+
builder.put(stringKey(entry.getKey()), String.valueOf(value));
103+
}
104+
}
105+
return builder.build();
106+
}
107+
108+
/**
109+
* Looks for scope files in the .telemetry directory and collects all unique scopes.
110+
*
111+
* @param rootDir the root directory
112+
* @param instrumentationDirectory the instrumentation directory relative to root
113+
* @return set of all unique scopes found in scope files
114+
*/
115+
public static Set<EmittedScope.Scope> getScopesFromFiles(
116+
String rootDir, String instrumentationDirectory) {
117+
Path telemetryDir = Paths.get(rootDir + "/" + instrumentationDirectory, ".telemetry");
118+
119+
Set<EmittedScope.Scope> allScopes = new HashSet<>();
120+
121+
if (Files.exists(telemetryDir) && Files.isDirectory(telemetryDir)) {
122+
try (Stream<Path> files = Files.list(telemetryDir)) {
123+
files
124+
.filter(path -> path.getFileName().toString().startsWith("scope-"))
125+
.forEach(
126+
path -> {
127+
String content = FileManager.readFileToString(path.toString());
128+
if (content != null) {
129+
EmittedScope parsed;
130+
try {
131+
parsed = YamlHelper.emittedScopeParser(content);
132+
} catch (RuntimeException e) {
133+
logger.severe("Error parsing scope file (" + path + "): " + e.getMessage());
134+
return;
135+
}
136+
137+
List<EmittedScope.Scope> scopes = parsed.getScopes();
138+
if (scopes != null) {
139+
allScopes.addAll(scopes);
140+
}
141+
}
142+
});
143+
} catch (IOException e) {
144+
logger.severe("Error reading scope files: " + e.getMessage());
145+
}
146+
}
147+
return allScopes;
148+
}
149+
150+
private EmittedScopeParser() {}
151+
}

0 commit comments

Comments
 (0)