diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/build.gradle.kts b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/build.gradle.kts index 67c18d98913b..610cf61305d4 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/build.gradle.kts +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/build.gradle.kts @@ -4,6 +4,13 @@ plugins { dependencies { implementation(project(":instrumentation:runtime-telemetry:runtime-telemetry-java8:library")) + implementation("io.opentelemetry:opentelemetry-api-events") compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") } + +tasks { + test { + jvmArgs("-Dotel.instrumentation.runtime-telemetry.package-emitter.enabled=true") + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzer.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzer.java new file mode 100644 index 000000000000..ad3f2ea8cda0 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzer.java @@ -0,0 +1,229 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; + +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarDetails.EAR_EXTENSION; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarDetails.JAR_EXTENSION; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarDetails.WAR_EXTENSION; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.events.EventEmitter; +import io.opentelemetry.api.events.GlobalEventEmitterProvider; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.internal.DaemonThreadFactory; +import java.io.IOException; +import java.lang.instrument.ClassFileTransformer; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * {@link JarAnalyzer} is a {@link ClassFileTransformer} which processes the {@link + * ProtectionDomain} of each class loaded and emits an event with metadata about each distinct + * archive location identified. + */ +final class JarAnalyzer implements ClassFileTransformer { + + private static final Logger logger = Logger.getLogger(JarAnalyzer.class.getName()); + + private static final String EVENT_DOMAIN_PACKAGE = "package"; + private static final String EVENT_NAME_INFO = "info"; + static final AttributeKey PACKAGE_NAME = AttributeKey.stringKey("package.name"); + static final AttributeKey PACKAGE_VERSION = AttributeKey.stringKey("package.version"); + static final AttributeKey PACKAGE_TYPE = AttributeKey.stringKey("package.type"); + static final AttributeKey PACKAGE_DESCRIPTION = + AttributeKey.stringKey("package.description"); + static final AttributeKey PACKAGE_CHECKSUM = AttributeKey.stringKey("package.checksum"); + static final AttributeKey PACKAGE_CHECKSUM_ALGORITHM = + AttributeKey.stringKey("package.checksum_algorithm"); + static final AttributeKey PACKAGE_PATH = AttributeKey.stringKey("package.path"); + + private final Set seenUris = new HashSet<>(); + private final BlockingQueue toProcess = new LinkedBlockingDeque<>(); + + private JarAnalyzer(OpenTelemetry unused, int jarsPerSecond) { + // TODO(jack-berg): Use OpenTelemetry to obtain EventEmitter when event API is stable + EventEmitter eventEmitter = + GlobalEventEmitterProvider.get() + .eventEmitterBuilder(JmxRuntimeMetricsUtil.getInstrumentationName()) + .setInstrumentationVersion(JmxRuntimeMetricsUtil.getInstrumentationVersion()) + .setEventDomain(EVENT_DOMAIN_PACKAGE) + .build(); + Worker worker = new Worker(eventEmitter, toProcess, jarsPerSecond); + Thread workerThread = + new DaemonThreadFactory(JarAnalyzer.class.getSimpleName() + "_WorkerThread") + .newThread(worker); + workerThread.start(); + } + + /** Create {@link JarAnalyzer} and start the worker thread. */ + public static JarAnalyzer create(OpenTelemetry unused, int jarsPerSecond) { + return new JarAnalyzer(unused, jarsPerSecond); + } + + /** + * Identify the archive (JAR or WAR) associated with the {@code protectionDomain} and queue it to + * be processed if its the first time we've seen it. + */ + @Override + public byte[] transform( + ClassLoader loader, + String className, + Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) { + handle(protectionDomain); + return null; + } + + private void handle(ProtectionDomain protectionDomain) { + if (protectionDomain == null) { + return; + } + CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource == null) { + return; + } + URL archiveUrl = codeSource.getLocation(); + if (archiveUrl == null) { + return; + } + URI locationUri; + try { + locationUri = archiveUrl.toURI(); + } catch (URISyntaxException e) { + logger.log(Level.WARNING, "Unable to get URI for code location URL: " + archiveUrl, e); + return; + } + + if (!seenUris.add(locationUri)) { + return; + } + if ("jrt".equals(archiveUrl.getProtocol())) { + logger.log(Level.FINEST, "Skipping processing for java runtime module: {0}", archiveUrl); + return; + } + String file = archiveUrl.getFile(); + if (file.endsWith("/")) { + logger.log(Level.FINEST, "Skipping processing non-archive code location: {0}", archiveUrl); + return; + } + if (!file.endsWith(JAR_EXTENSION) + && !file.endsWith(WAR_EXTENSION) + && !file.endsWith(EAR_EXTENSION)) { + logger.log(Level.INFO, "Skipping processing unrecognized code location: {0}", archiveUrl); + return; + } + + // Only code locations with .jar and .war extension should make it here + toProcess.add(archiveUrl); + } + + private static final class Worker implements Runnable { + + private final EventEmitter eventEmitter; + private final BlockingQueue toProcess; + private final io.opentelemetry.sdk.internal.RateLimiter rateLimiter; + + private Worker(EventEmitter eventEmitter, BlockingQueue toProcess, int jarsPerSecond) { + this.eventEmitter = eventEmitter; + this.toProcess = toProcess; + this.rateLimiter = + new io.opentelemetry.sdk.internal.RateLimiter( + jarsPerSecond, jarsPerSecond, Clock.getDefault()); + } + + /** + * Continuously poll the {@link #toProcess} for archive {@link URL}s, and process each wit + * {@link #processUrl(EventEmitter, URL)}. + */ + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + URL archiveUrl = null; + try { + if (!rateLimiter.trySpend(1.0)) { + Thread.sleep(100); + continue; + } + archiveUrl = toProcess.poll(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + if (archiveUrl == null) { + continue; + } + try { + // TODO(jack-berg): add ability to optionally re-process urls periodically to re-emit + // events + processUrl(eventEmitter, archiveUrl); + } catch (Throwable e) { + logger.log(Level.WARNING, "Unexpected error processing archive URL: " + archiveUrl, e); + } + } + logger.warning("JarAnalyzer stopped"); + } + } + + /** + * Process the {@code archiveUrl}, extracting metadata from it and emitting an event with the + * content. + */ + static void processUrl(EventEmitter eventEmitter, URL archiveUrl) { + JarDetails jarDetails; + try { + jarDetails = JarDetails.forUrl(archiveUrl); + } catch (IOException e) { + logger.log(Level.WARNING, "Error reading package for archive URL: " + archiveUrl, e); + return; + } + AttributesBuilder builder = Attributes.builder(); + + String packagePath = jarDetails.packagePath(); + if (packagePath != null) { + builder.put(PACKAGE_PATH, packagePath); + } + + String packageType = jarDetails.packageType(); + if (packageType != null) { + builder.put(PACKAGE_TYPE, packageType); + } + + String packageName = jarDetails.packageName(); + if (packageName != null) { + builder.put(PACKAGE_NAME, packageName); + } + + String packageVersion = jarDetails.version(); + if (packageVersion != null) { + builder.put(PACKAGE_VERSION, packageVersion); + } + + String packageDescription = jarDetails.packageDescription(); + if (packageDescription != null) { + builder.put(PACKAGE_DESCRIPTION, packageDescription); + } + + String packageChecksum = jarDetails.computeSha1(); + builder.put(PACKAGE_CHECKSUM, packageChecksum); + builder.put(PACKAGE_CHECKSUM_ALGORITHM, "SHA1"); + + eventEmitter.emit(EVENT_NAME_INFO, builder.build()); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstaller.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstaller.java new file mode 100644 index 000000000000..ec12fa81c2f1 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstaller.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.InstrumentationHolder; +import io.opentelemetry.javaagent.tooling.BeforeAgentListener; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.lang.instrument.Instrumentation; + +/** Installs the {@link JarAnalyzer}. */ +@AutoService(BeforeAgentListener.class) +public class JarAnalyzerInstaller implements BeforeAgentListener { + + @Override + public void beforeAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk); + boolean enabled = + config.getBoolean("otel.instrumentation.runtime-telemetry.package-emitter.enabled", false); + if (!enabled) { + return; + } + Instrumentation inst = InstrumentationHolder.getInstrumentation(); + if (inst == null) { + return; + } + int jarsPerSecond = + config.getInt("otel.instrumentation.runtime-telemetry.package-emitter.jars-per-second", 10); + JarAnalyzer jarAnalyzer = + JarAnalyzer.create(autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk(), jarsPerSecond); + inst.addTransformer(jarAnalyzer); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarDetails.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarDetails.java new file mode 100644 index 000000000000..88afca04c046 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarDetails.java @@ -0,0 +1,276 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toMap; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.net.URL; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +/** + * For a given URL representing a Jar directly on the file system or embedded within another + * archive, this class provides methods which expose useful information about it. + */ +class JarDetails { + static final String JAR_EXTENSION = "jar"; + static final String WAR_EXTENSION = "war"; + static final String EAR_EXTENSION = "ear"; + private static final Map EMBEDDED_FORMAT_TO_EXTENSION = + Stream.of(JAR_EXTENSION, WAR_EXTENSION, EAR_EXTENSION) + .collect( + collectingAndThen( + toMap(ext -> ('.' + ext + "!/"), identity()), + Collections::unmodifiableMap)); + private static final ThreadLocal SHA1 = + ThreadLocal.withInitial( + () -> { + try { + return MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + }); + + private final URL url; + protected final JarFile jarFile; + private final Properties pom; + private final Manifest manifest; + private final String sha1Checksum; + + private JarDetails(URL url, JarFile jarFile) throws IOException { + this.url = url; + this.jarFile = jarFile; + this.pom = getPom(); + this.manifest = getManifest(); + this.sha1Checksum = computeDigest(SHA1.get()); + } + + static JarDetails forUrl(URL url) throws IOException { + if (url.getProtocol().equals("jar")) { + String urlString = url.toExternalForm(); + String urlLower = urlString.toLowerCase(Locale.ROOT); + for (Map.Entry entry : EMBEDDED_FORMAT_TO_EXTENSION.entrySet()) { + int index = urlLower.indexOf(entry.getKey()); + if (index > 0) { + String targetEntry = urlString.substring(index + entry.getKey().length()); + JarFile jarFile = + new JarFile( + urlString.substring("jar:file:".length(), index + 1 + entry.getValue().length())); + JarEntry jarEntry = jarFile.getJarEntry(targetEntry); + return new EmbeddedJarDetails(url, jarFile, jarEntry); + } + } + } + return new JarDetails(url, new JarFile(url.getFile())); + } + + /** + * Returns the archive file name, e.g. {@code jackson-datatype-jsr310-2.15.2.jar}. Returns null if + * unable to identify the file name from {@link #url}. + */ + @Nullable + String packagePath() { + String path = url.getFile(); + int start = path.lastIndexOf(File.separator); + if (start > -1) { + return path.substring(start + 1); + } + return null; + } + + /** + * Returns the extension of the archive, e.g. {@code jar}. Returns null if unable to identify the + * extension from {@link #url} + */ + @Nullable + String packageType() { + String path = url.getFile(); + int extensionStart = path.lastIndexOf("."); + if (extensionStart > -1) { + return path.substring(extensionStart + 1); + } + return null; + } + + /** + * Returns the maven package name in the format {@code groupId:artifactId}, e.g. {@code + * com.fasterxml.jackson.datatype:jackson-datatype-jsr310}. Returns null if {@link #pom} is not + * found, or is missing groupId or artifactId properties. + */ + @Nullable + String packageName() { + if (pom == null) { + return null; + } + String groupId = pom.getProperty("groupId"); + String artifactId = pom.getProperty("artifactId"); + if (groupId != null && !groupId.isEmpty() && artifactId != null && !artifactId.isEmpty()) { + return groupId + ":" + artifactId; + } + return null; + } + + /** + * Returns the version from the pom file, e.g. {@code 2.15.2}. Returns null if {@link #pom} is not + * found or is missing version property. + */ + @Nullable + String version() { + if (pom == null) { + return null; + } + String version = pom.getProperty("version"); + if (version != null && !version.isEmpty()) { + return version; + } + return null; + } + + /** + * Returns the package description from the jar manifest "{Implementation-Title} by + * {Implementation-Vendor}", e.g. {@code Jackson datatype: JSR310 by FasterXML}. Returns null if + * {@link #manifest} is not found. + */ + @Nullable + String packageDescription() { + if (manifest == null) { + return null; + } + + java.util.jar.Attributes mainAttributes = manifest.getMainAttributes(); + String name = mainAttributes.getValue(java.util.jar.Attributes.Name.IMPLEMENTATION_TITLE); + String description = + mainAttributes.getValue(java.util.jar.Attributes.Name.IMPLEMENTATION_VENDOR); + + String packageDescription = name; + if (description != null && !description.isEmpty()) { + packageDescription += " by " + description; + } + return packageDescription; + } + + /** Returns the SHA1 hash of this file, e.g. {@code 30d16ec2aef6d8094c5e2dce1d95034ca8b6cb42}. */ + String computeSha1() { + return sha1Checksum; + } + + private String computeDigest(MessageDigest md) throws IOException { + try (InputStream inputStream = getInputStream()) { + DigestInputStream dis = new DigestInputStream(inputStream, md); + byte[] buffer = new byte[8192]; + while (dis.read(buffer) != -1) {} + byte[] digest = md.digest(); + return new BigInteger(1, digest).toString(16); + } + } + + /** + * Returns An open input stream for the associated url. It is the caller's responsibility to close + * the stream on completion. + */ + protected InputStream getInputStream() throws IOException { + return url.openStream(); + } + + @Nullable + protected Manifest getManifest() { + try { + return jarFile.getManifest(); + } catch (IOException e) { + return null; + } + } + + /** + * Returns the values from pom.properties if this file is found. If multiple pom.properties files + * are found or there is an error reading the file, return null. + */ + @Nullable + protected Properties getPom() throws IOException { + Properties pom = null; + for (Enumeration entries = jarFile.entries(); entries.hasMoreElements(); ) { + JarEntry jarEntry = entries.nextElement(); + if (jarEntry.getName().startsWith("META-INF/maven") + && jarEntry.getName().endsWith("pom.properties")) { + if (pom != null) { + // we've found multiple pom files. bail! + return null; + } + Properties props = new Properties(); + props.load(jarFile.getInputStream(jarEntry)); + pom = props; + } + } + return pom; + } + + private static class EmbeddedJarDetails extends JarDetails { + + private final JarEntry jarEntry; + + private EmbeddedJarDetails(URL url, JarFile jarFile, JarEntry jarEntry) throws IOException { + super(url, jarFile); + this.jarEntry = jarEntry; + } + + @Override + protected InputStream getInputStream() throws IOException { + return jarFile.getInputStream(jarEntry); + } + + @Override + protected Manifest getManifest() { + try (JarInputStream jarFile = new JarInputStream(getInputStream())) { + return jarFile.getManifest(); + } catch (IOException e) { + return null; + } + } + + @Override + @Nullable + protected Properties getPom() throws IOException { + Properties pom = null; + // Need to navigate inside the embedded jar which can't be done via random access. + try (JarInputStream jarFile = new JarInputStream(getInputStream())) { + for (JarEntry entry = jarFile.getNextJarEntry(); + entry != null; + entry = jarFile.getNextJarEntry()) { + if (entry.getName().startsWith("META-INF/maven") + && entry.getName().endsWith("pom.properties")) { + if (pom != null) { + // we've found multiple pom files. bail! + return null; + } + Properties props = new Properties(); + props.load(jarFile); + pom = props; + } + } + return pom; + } + } + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstallerTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstallerTest.java new file mode 100644 index 000000000000..1771c8174af4 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstallerTest.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.stream.Collectors.toList; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JarAnalyzerInstallerTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void jarAnalyzerEnabled() throws InterruptedException { + Thread.sleep(5000); + + List logRecordData = testing.logRecords(); + List events = + logRecordData.stream() + .filter( + record -> { + Attributes attributes = record.getAttributes(); + return "package".equals(attributes.get(AttributeKey.stringKey("event.domain"))) + && "info".equals(attributes.get(AttributeKey.stringKey("event.name"))); + }) + .collect(toList()); + assertThat(events) + .hasSizeGreaterThan(0) + .allSatisfy( + logRecord -> + assertThat(logRecord.getAttributes()) + .containsEntry("package.type", "jar") + .containsEntry("package.checksum_algorithm", "SHA1") + .hasEntrySatisfying( + AttributeKey.stringKey("package.checksum"), + value -> assertThat(value).isNotNull()) + .hasEntrySatisfying( + AttributeKey.stringKey("package.path"), + value -> assertThat(value).isNotNull()) + .satisfies( + attributes -> { + String packageName = + attributes.get(AttributeKey.stringKey("package.name")); + if (packageName != null) { + assertThat(packageName).matches(".*:.*"); + } + String packageVersion = + attributes.get(AttributeKey.stringKey("package.version")); + if (packageVersion != null) { + assertThat(packageVersion).matches(".*\\..*"); + } + })); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsUtil.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsUtil.java index 7dcb0245c1b0..334d405ad26f 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsUtil.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsUtil.java @@ -32,6 +32,15 @@ public static Meter getMeter(OpenTelemetry openTelemetry) { return meterBuilder.build(); } + public static String getInstrumentationName() { + return INSTRUMENTATION_NAME; + } + + @Nullable + public static String getInstrumentationVersion() { + return INSTRUMENTATION_VERSION; + } + public static void closeObservers(List observables) { observables.forEach( observable -> { diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/build.gradle.kts b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/build.gradle.kts new file mode 100644 index 000000000000..20f7e2cefbc8 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + id("otel.java-conventions") + + war +} + +dependencies { + testImplementation(project(":instrumentation:runtime-telemetry:runtime-telemetry-java8:javaagent")) + + testImplementation("io.opentelemetry:opentelemetry-api-events") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + + // Bring in various archives to test introspection logic + testImplementation("io.opentelemetry:opentelemetry-api") + testImplementation("org.springframework:spring-webmvc:3.1.0.RELEASE") + testImplementation("com.google.guava:guava") +} + +tasks.war { + archiveFileName.set("app.war") + manifest { + attributes( + "Implementation-Title" to "Dummy App", + "Implementation-Vendor" to "OpenTelemetry", + ) + } +} + +tasks.named("test") { + dependsOn(tasks.getByName("war")) +} + +tasks { + withType().configureEach { + environment( + mapOf( + // Expose dummy app war location to test + "DUMMY_APP_WAR" to "${layout.buildDirectory.asFile.get()}/libs/app.war" + ) + ) + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/main/java/io/opentelemetry/instrumentation/testapp/DummyApplication.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/main/java/io/opentelemetry/instrumentation/testapp/DummyApplication.java new file mode 100644 index 000000000000..c75d1f402c20 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/main/java/io/opentelemetry/instrumentation/testapp/DummyApplication.java @@ -0,0 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testapp; + +public class DummyApplication {} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerTest.java new file mode 100644 index 000000000000..04778527a4ab --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerTest.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; + +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_CHECKSUM; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_CHECKSUM_ALGORITHM; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_DESCRIPTION; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_NAME; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_PATH; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_TYPE; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_VERSION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.events.EventEmitter; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.assertj.AttributesAssert; +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.springframework.http.HttpRequest; + +class JarAnalyzerTest { + + @ParameterizedTest + @MethodSource("processUrlArguments") + void processUrl_EmitsEvents(URL archiveUrl, Consumer attributesConsumer) { + EventEmitter eventEmitter = mock(EventEmitter.class); + JarAnalyzer.processUrl(eventEmitter, archiveUrl); + + ArgumentCaptor attributesArgumentCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(eventEmitter).emit(eq("info"), attributesArgumentCaptor.capture()); + + attributesConsumer.accept( + OpenTelemetryAssertions.assertThat(attributesArgumentCaptor.getValue())); + } + + private static Stream processUrlArguments() { + return Stream.of( + // instrumentation code + Arguments.of( + archiveUrl(JarAnalyzer.class), + assertAttributes( + attributes -> + attributes + .containsEntry(PACKAGE_TYPE, "jar") + .hasEntrySatisfying( + PACKAGE_PATH, + path -> + assertThat( + path.matches( + "opentelemetry-javaagent-runtime-telemetry-java8-[0-9a-zA-Z-\\.]+\\.jar")) + .isTrue()) + .containsEntry(PACKAGE_DESCRIPTION, "javaagent by OpenTelemetry") + .containsEntry(PACKAGE_CHECKSUM_ALGORITHM, "SHA1") + .hasEntrySatisfying( + PACKAGE_CHECKSUM, checksum -> assertThat(checksum).isNotEmpty()))), + // dummy war + Arguments.of( + archiveUrl(new File(System.getenv("DUMMY_APP_WAR"))), + assertAttributes( + attributes -> + attributes + .containsEntry(PACKAGE_TYPE, "war") + .containsEntry(PACKAGE_PATH, "app.war") + .containsEntry(PACKAGE_DESCRIPTION, "Dummy App by OpenTelemetry") + .containsEntry(PACKAGE_CHECKSUM_ALGORITHM, "SHA1") + .hasEntrySatisfying( + PACKAGE_CHECKSUM, checksum -> assertThat(checksum).isNotEmpty()))), + // io.opentelemetry:opentelemetry-api + Arguments.of( + archiveUrl(Tracer.class), + assertAttributes( + attributes -> + attributes + .containsEntry(PACKAGE_TYPE, "jar") + .hasEntrySatisfying( + PACKAGE_PATH, + path -> + assertThat(path.matches("opentelemetry-api-[0-9a-zA-Z-\\.]+\\.jar")) + .isTrue()) + .containsEntry(PACKAGE_DESCRIPTION, "all") + .containsEntry(PACKAGE_CHECKSUM_ALGORITHM, "SHA1") + .hasEntrySatisfying( + PACKAGE_CHECKSUM, checksum -> assertThat(checksum).isNotEmpty()))), + // org.springframework:spring-webmvc + Arguments.of( + archiveUrl(HttpRequest.class), + assertAttributes( + attributes -> + attributes + .containsEntry(PACKAGE_TYPE, "jar") + // TODO(jack-berg): can we extract version out of path to populate + // package.version field? + .hasEntrySatisfying( + PACKAGE_PATH, + path -> + assertThat(path.matches("spring-web-[0-9a-zA-Z-\\.]+\\.jar")) + .isTrue()) + .containsEntry(PACKAGE_DESCRIPTION, "org.springframework.web") + .containsEntry(PACKAGE_CHECKSUM_ALGORITHM, "SHA1") + .hasEntrySatisfying( + PACKAGE_CHECKSUM, checksum -> assertThat(checksum).isNotEmpty()))), + // com.google.guava:guava + Arguments.of( + archiveUrl(ImmutableMap.class), + assertAttributes( + attributes -> + attributes + .containsEntry(PACKAGE_TYPE, "jar") + .hasEntrySatisfying( + PACKAGE_PATH, + path -> + assertThat(path.matches("guava-[0-9a-zA-Z-\\.]+\\.jar")).isTrue()) + .containsEntry(PACKAGE_NAME, "com.google.guava:guava") + .hasEntrySatisfying( + PACKAGE_VERSION, version -> assertThat(version).isNotEmpty()) + .containsEntry(PACKAGE_CHECKSUM_ALGORITHM, "SHA1") + .hasEntrySatisfying( + PACKAGE_CHECKSUM, checksum -> assertThat(checksum).isNotEmpty())))); + } + + private static URL archiveUrl(File file) { + try { + return new URL("file://" + file.getAbsolutePath()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Error creating URL for file", e); + } + } + + private static URL archiveUrl(Class clazz) { + return clazz.getProtectionDomain().getCodeSource().getLocation(); + } + + private static Consumer assertAttributes( + Consumer attributesAssert) { + return attributesAssert; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ffe25b100672..38392da44cb0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -458,6 +458,7 @@ include(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-5.0:javaagent include(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-5.0:testing") include(":instrumentation:runtime-telemetry:runtime-telemetry-java8:javaagent") include(":instrumentation:runtime-telemetry:runtime-telemetry-java8:library") +include(":instrumentation:runtime-telemetry:runtime-telemetry-java8:testing") include(":instrumentation:runtime-telemetry:runtime-telemetry-java17:javaagent") include(":instrumentation:runtime-telemetry:runtime-telemetry-java17:library") include(":instrumentation:rxjava:rxjava-1.0:library") diff --git a/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java b/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java index e6e07c57cf12..a78054b0804e 100644 --- a/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java +++ b/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java @@ -40,6 +40,7 @@ protected Map getAgentEnvironment( } environment.put("OTEL_JAVAAGENT_DEBUG", "true"); environment.put("OTEL_EXPERIMENTAL_JAVASCRIPT_SNIPPET", ""); + environment.put("OTEL_INSTRUMENTATION_RUNTIME_TELEMETRY_PACKAGE_EMITTER_ENABLED", "true"); return environment; }