Skip to content

Commit 1f9c136

Browse files
mhlidddevflow.devflow-routing-intake
andauthored
Add Telemetry for Extension Finder SPIs (#11298)
init init remove iterating jar files w/ static list of SPIs Merge branch 'master' into mhlidd/add_extension_finder_telemetry Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent f0a8854 commit 1f9c136

2 files changed

Lines changed: 217 additions & 0 deletions

File tree

dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/ExtensionFinder.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static datadog.opentelemetry.tooling.OtelExtensionHandler.OPENTELEMETRY;
44
import static datadog.trace.agent.tooling.ExtensionHandler.DATADOG;
55

6+
import datadog.trace.api.telemetry.OtelSpiCollector;
67
import de.thetaphi.forbiddenapis.SuppressForbidden;
78
import java.io.File;
89
import java.io.FileNotFoundException;
@@ -26,6 +27,33 @@ public final class ExtensionFinder {
2627

2728
private static final ExtensionHandler[] handlers = {OPENTELEMETRY, DATADOG};
2829

30+
private static final String EXTENSIONS_PATH_SOURCE = "extensions_path";
31+
private static final String SERVICES_PREFIX = "META-INF/services/";
32+
33+
private static final String[] OTEL_SPI_FQNS = {
34+
"io.opentelemetry.context.ContextStorageProvider",
35+
"io.opentelemetry.exporter.internal.compression.CompressorProvider",
36+
"io.opentelemetry.exporter.internal.grpc.GrpcSenderProvider",
37+
"io.opentelemetry.exporter.internal.http.HttpSenderProvider",
38+
"io.opentelemetry.javaagent.extension.AgentListener",
39+
"io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer",
40+
"io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule",
41+
"io.opentelemetry.javaagent.tooling.BeforeAgentListener",
42+
"io.opentelemetry.javaagent.tooling.LoggingCustomizer",
43+
"io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesConfigurer",
44+
"io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider",
45+
"io.opentelemetry.sdk.autoconfigure.spi.AutoConfigureListener",
46+
"io.opentelemetry.sdk.autoconfigure.spi.ConditionalResourceProvider",
47+
"io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider",
48+
"io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider",
49+
"io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider",
50+
"io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider",
51+
"io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider",
52+
"io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider",
53+
"io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider",
54+
"io.opentelemetry.sdk.autoconfigure.spi.traces.SpanExporterCustomizer",
55+
};
56+
2957
/**
3058
* Discovers extensions on the configured path and creates a classloader for each extension.
3159
* Registers the combined classloader with {@link Utils#setExtendedClassLoader(ClassLoader)}.
@@ -40,6 +68,7 @@ public static boolean findExtensions(String extensionsPath, Class<?>... extensio
4068
String[] descriptors = descriptors(extensionTypes);
4169

4270
for (JarFile jar : findExtensionJars(extensionsPath)) {
71+
recordOtelSpiTelemetry(jar);
4372
URL extensionURL = findExtensionURL(jar, descriptors);
4473
if (null != extensionURL) {
4574
log.debug("Found extension jar {}", jar.getName());
@@ -60,6 +89,15 @@ public static boolean findExtensions(String extensionsPath, Class<?>... extensio
6089
return !classLoaders.isEmpty();
6190
}
6291

92+
/** Reports telemetry for any tracked OpenTelemetry SPI service descriptors present in the jar. */
93+
static void recordOtelSpiTelemetry(JarFile jar) {
94+
for (String fqn : OTEL_SPI_FQNS) {
95+
if (null != jar.getJarEntry(SERVICES_PREFIX + fqn)) {
96+
OtelSpiCollector.getInstance().recordSpiDetected(fqn, EXTENSIONS_PATH_SOURCE);
97+
}
98+
}
99+
}
100+
63101
/** Closes jar resources from the extension path which did not contain any extensions. */
64102
private static void close(List<JarFile> unusedJars) {
65103
for (JarFile jar : unusedJars) {
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package datadog.trace.agent.tooling;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import datadog.trace.api.telemetry.OtelSpiCollector;
7+
import java.io.IOException;
8+
import java.io.OutputStream;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.Collection;
12+
import java.util.HashSet;
13+
import java.util.Set;
14+
import java.util.jar.JarEntry;
15+
import java.util.jar.JarFile;
16+
import java.util.jar.JarOutputStream;
17+
import org.junit.jupiter.api.BeforeEach;
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.api.io.TempDir;
20+
21+
public class ExtensionFinderTest {
22+
23+
private static final String AUTOCONFIGURE_PROPAGATOR =
24+
"io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider";
25+
private static final String AUTOCONFIGURE_RESOURCE =
26+
"io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider";
27+
private static final String AUTOCONFIGURE_SAMPLER =
28+
"io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider";
29+
private static final String AUTOCONFIGURE_EXPORTER =
30+
"io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider";
31+
private static final String JAVAAGENT_INSTRUMENTATION_MODULE =
32+
"io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule";
33+
private static final String JAVAAGENT_AGENT_LISTENER =
34+
"io.opentelemetry.javaagent.extension.AgentListener";
35+
private static final String SHADED_AUTOCONFIGURE_SAMPLER =
36+
"io.opentelemetry.javaagent.shaded.io.opentelemetry.sdk.autoconfigure.spi.ConfigurableSamplerProvider";
37+
38+
private final OtelSpiCollector collector = OtelSpiCollector.getInstance();
39+
40+
@BeforeEach
41+
public void clearCollector() {
42+
collector.drain();
43+
}
44+
45+
@Test
46+
public void singleOtelSpiIsReported(@TempDir Path tempDir) throws IOException {
47+
Path jarPath = buildJar(tempDir, "ext.jar", AUTOCONFIGURE_PROPAGATOR);
48+
49+
try (JarFile jar = new JarFile(jarPath.toFile(), false)) {
50+
ExtensionFinder.recordOtelSpiTelemetry(jar);
51+
}
52+
53+
Collection<OtelSpiCollector.OtelSpiMetric> drained = collector.drain();
54+
assertEquals(1, drained.size());
55+
OtelSpiCollector.OtelSpiMetric metric = drained.iterator().next();
56+
assertEquals("otel.spi.detected", metric.metricName);
57+
assertTrue(metric.tags.contains("spi_class:" + AUTOCONFIGURE_PROPAGATOR));
58+
assertTrue(metric.tags.contains("source:extensions_path"));
59+
}
60+
61+
@Test
62+
public void allFourAutoconfigureSpisAreReported(@TempDir Path tempDir) throws IOException {
63+
Path jarPath =
64+
buildJar(
65+
tempDir,
66+
"ext.jar",
67+
AUTOCONFIGURE_PROPAGATOR,
68+
AUTOCONFIGURE_RESOURCE,
69+
AUTOCONFIGURE_SAMPLER,
70+
AUTOCONFIGURE_EXPORTER);
71+
72+
try (JarFile jar = new JarFile(jarPath.toFile(), false)) {
73+
ExtensionFinder.recordOtelSpiTelemetry(jar);
74+
}
75+
76+
assertEquals(
77+
new HashSet<>(
78+
java.util.Arrays.asList(
79+
AUTOCONFIGURE_PROPAGATOR,
80+
AUTOCONFIGURE_RESOURCE,
81+
AUTOCONFIGURE_SAMPLER,
82+
AUTOCONFIGURE_EXPORTER)),
83+
reportedFqns(collector.drain()));
84+
}
85+
86+
@Test
87+
public void javaagentExtensionSpisAreReported(@TempDir Path tempDir) throws IOException {
88+
Path jarPath =
89+
buildJar(tempDir, "ext.jar", JAVAAGENT_INSTRUMENTATION_MODULE, JAVAAGENT_AGENT_LISTENER);
90+
91+
try (JarFile jar = new JarFile(jarPath.toFile(), false)) {
92+
ExtensionFinder.recordOtelSpiTelemetry(jar);
93+
}
94+
95+
assertEquals(
96+
new HashSet<>(
97+
java.util.Arrays.asList(JAVAAGENT_INSTRUMENTATION_MODULE, JAVAAGENT_AGENT_LISTENER)),
98+
reportedFqns(collector.drain()));
99+
}
100+
101+
@Test
102+
public void nonOtelSpiIsIgnored(@TempDir Path tempDir) throws IOException {
103+
Path jarPath =
104+
buildJar(
105+
tempDir,
106+
"ext.jar",
107+
"com.example.MyService",
108+
"org.springframework.context.ApplicationContextInitializer",
109+
"java.sql.Driver");
110+
111+
try (JarFile jar = new JarFile(jarPath.toFile(), false)) {
112+
ExtensionFinder.recordOtelSpiTelemetry(jar);
113+
}
114+
115+
assertEquals(0, collector.drain().size());
116+
}
117+
118+
@Test
119+
public void jarWithoutAnyServiceDescriptorsEmitsNothing(@TempDir Path tempDir)
120+
throws IOException {
121+
Path jarPath = tempDir.resolve("empty.jar");
122+
try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(jarPath))) {
123+
jos.putNextEntry(new JarEntry("README.txt"));
124+
jos.write("not an extension".getBytes());
125+
jos.closeEntry();
126+
}
127+
128+
try (JarFile jar = new JarFile(jarPath.toFile(), false)) {
129+
ExtensionFinder.recordOtelSpiTelemetry(jar);
130+
}
131+
132+
assertEquals(0, collector.drain().size());
133+
}
134+
135+
@Test
136+
public void mixedOtelAndNonOtelReportsOnlyOtel(@TempDir Path tempDir) throws IOException {
137+
Path jarPath =
138+
buildJar(
139+
tempDir,
140+
"ext.jar",
141+
AUTOCONFIGURE_PROPAGATOR,
142+
"com.example.MyService",
143+
JAVAAGENT_AGENT_LISTENER,
144+
"java.sql.Driver");
145+
146+
try (JarFile jar = new JarFile(jarPath.toFile(), false)) {
147+
ExtensionFinder.recordOtelSpiTelemetry(jar);
148+
}
149+
150+
assertEquals(
151+
new HashSet<>(java.util.Arrays.asList(AUTOCONFIGURE_PROPAGATOR, JAVAAGENT_AGENT_LISTENER)),
152+
reportedFqns(collector.drain()));
153+
}
154+
155+
private static Set<String> reportedFqns(Collection<OtelSpiCollector.OtelSpiMetric> drained) {
156+
Set<String> fqns = new HashSet<>();
157+
for (OtelSpiCollector.OtelSpiMetric metric : drained) {
158+
for (String tag : metric.tags) {
159+
if (tag.startsWith("spi_class:")) {
160+
fqns.add(tag.substring("spi_class:".length()));
161+
}
162+
}
163+
}
164+
return fqns;
165+
}
166+
167+
/** Builds a jar with empty {@code META-INF/services/<fqn>} entries for each given FQN. */
168+
private static Path buildJar(Path dir, String name, String... serviceFqns) throws IOException {
169+
Path jarPath = dir.resolve(name);
170+
try (OutputStream out = Files.newOutputStream(jarPath);
171+
JarOutputStream jos = new JarOutputStream(out)) {
172+
for (String fqn : serviceFqns) {
173+
jos.putNextEntry(new JarEntry("META-INF/services/" + fqn));
174+
jos.closeEntry();
175+
}
176+
}
177+
return jarPath;
178+
}
179+
}

0 commit comments

Comments
 (0)