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 index bba59e74c4e9..842d3767979c 100644 --- 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 @@ -5,13 +5,8 @@ package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; -import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzerUtil.addPackageChecksum; -import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzerUtil.addPackageDescription; -import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzerUtil.addPackageNameAndVersion; -import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzerUtil.addPackagePath; -import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzerUtil.addPackageType; - 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; @@ -46,6 +41,15 @@ final class JarAnalyzer implements ClassFileTransformer { private static final String WAR_EXTENSION = ".war"; 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<>(); @@ -172,39 +176,22 @@ public void run() { * content. */ static void processUrl(EventEmitter eventEmitter, URL archiveUrl) { - AttributesBuilder builder = Attributes.builder(); - - try { - addPackageType(builder, archiveUrl); - } catch (Exception e) { - logger.log(Level.WARNING, "Error adding package type for archive URL: {0}" + archiveUrl, e); - } - - try { - addPackageChecksum(builder, archiveUrl); - } catch (Exception e) { - logger.log(Level.WARNING, "Error adding package checksum for archive URL: " + archiveUrl, e); - } - - try { - addPackagePath(builder, archiveUrl); - } catch (Exception e) { - logger.log(Level.WARNING, "Error adding package path archive URL: " + archiveUrl, e); - } - + JarDetails jarDetails; try { - addPackageDescription(builder, archiveUrl); + jarDetails = JarDetails.forUrl(archiveUrl); } catch (Exception e) { - logger.log( - Level.WARNING, "Error adding package description for archive URL: " + archiveUrl, e); + logger.log(Level.WARNING, "Error reading package for archive URL: {0}" + archiveUrl, e); + return; } + AttributesBuilder builder = Attributes.builder(); - try { - addPackageNameAndVersion(builder, archiveUrl); - } catch (Exception e) { - logger.log( - Level.WARNING, "Error adding package name and version for archive URL: " + archiveUrl, e); - } + builder.put(PACKAGE_PATH, jarDetails.packagePath()); + builder.put(PACKAGE_TYPE, jarDetails.packageType()); + builder.put(PACKAGE_NAME, jarDetails.packageName()); + builder.put(PACKAGE_VERSION, jarDetails.version()); + builder.put(PACKAGE_DESCRIPTION, jarDetails.packageDescription()); + builder.put(PACKAGE_CHECKSUM, jarDetails.computeSha1()); + 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/JarAnalyzerUtil.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerUtil.java deleted file mode 100644 index 2e8bf814da48..000000000000 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerUtil.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.AttributesBuilder; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Properties; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarInputStream; -import java.util.jar.Manifest; - -final class JarAnalyzerUtil { - - 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 static final ThreadLocal MESSAGE_DIGEST_THREAD_LOCAL = - ThreadLocal.withInitial(JarAnalyzerUtil::createSha1MessageDigest); - - private static MessageDigest createSha1MessageDigest() { - try { - return MessageDigest.getInstance("SHA1"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException( - "Unexpected error. Checksum algorithm SHA1 does not exist.", e); - } - } - - /** - * Set the attributes {@link #PACKAGE_TYPE} from the {@code archiveUrl}. - * - *

The {@link #PACKAGE_TYPE} is set extension of the archive, e.g. {@code jar}. - */ - static void addPackageType(AttributesBuilder builder, URL archiveUrl) throws Exception { - String path = archiveUrl.getFile(); - int extensionStart = path.lastIndexOf("."); - if (extensionStart > -1) { - builder.put(PACKAGE_TYPE, path.substring(extensionStart + 1)); - return; - } - throw new Exception("Cannot extract archive type from URL: " + archiveUrl); - } - - /** - * Set the attributes {@link #PACKAGE_CHECKSUM} from the {@code archiveUrl}. - * - *

The {@link #PACKAGE_CHECKSUM} is set to the SHA-1 checksum of the archive, e.g. {@code - * 30d16ec2aef6d8094c5e2dce1d95034ca8b6cb42}. - */ - static void addPackageChecksum(AttributesBuilder builder, URL archiveUrl) throws IOException { - builder.put(PACKAGE_CHECKSUM, computeSha1(archiveUrl)); - builder.put(PACKAGE_CHECKSUM_ALGORITHM, "SHA1"); - } - - private static String computeSha1(URL archiveUrl) throws IOException { - MessageDigest md = MESSAGE_DIGEST_THREAD_LOCAL.get(); - md.reset(); // Reset reused thread local message digest instead - - try (InputStream is = new DigestInputStream(archiveUrl.openStream(), md)) { - byte[] buffer = new byte[1024 * 8]; - // read in the stream in chunks while updating the digest - while (is.read(buffer) != -1) {} - - byte[] mdbytes = md.digest(); - - // convert to hex format - StringBuilder sb = new StringBuilder(40); - for (byte mdbyte : mdbytes) { - sb.append(Integer.toString((mdbyte & 0xff) + 0x100, 16).substring(1)); - } - - return sb.toString(); - } - } - - /** - * Set the attributes {@link #PACKAGE_PATH} from the {@code archiveUrl}. - * - *

The {@link #PACKAGE_PATH} is set to the archive file name, e.g. {@code - * jackson-datatype-jsr310-2.15.2.jar}. - */ - static void addPackagePath(AttributesBuilder builder, URL archiveUrl) throws Exception { - builder.put(PACKAGE_PATH, archiveFilename(archiveUrl)); - } - - private static String archiveFilename(URL archiveUrl) throws Exception { - String path = archiveUrl.getFile(); - int start = path.lastIndexOf(File.separator); - if (start > -1) { - return path.substring(start + 1); - } - throw new Exception("Cannot extract archive file name from archive URL: " + archiveUrl); - } - - /** - * Set the attributes {@link #PACKAGE_DESCRIPTION} from the {@code archiveUrl}. - * - *

The {@link #PACKAGE_DESCRIPTION} is set to manifest "{Implementation-Title} by - * {Implementation-Vendor}", e.g. {@code Jackson datatype: JSR310 by FasterXML}. - */ - static void addPackageDescription(AttributesBuilder builder, URL archiveUrl) throws IOException { - try (JarFile jarFile = new JarFile(archiveUrl.getFile())) { - Manifest manifest = jarFile.getManifest(); - if (manifest == null) { - return; - } - - 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; - } - builder.put(PACKAGE_DESCRIPTION, packageDescription); - } - } - - /** - * Set the attributes {@link #PACKAGE_NAME} and {@link #PACKAGE_VERSION} from the {@code - * archiveUrl}. - * - *

The {@link #PACKAGE_NAME} is set to the POM "{groupId}:{artifactId}", e.g. {@code Jackson - * datatype: JSR310 by FasterXML}. - * - *

The {@link #PACKAGE_VERSION} is set to the POM "{version}", e.g. {@code 2.15.2}. - */ - static void addPackageNameAndVersion(AttributesBuilder builder, URL archiveUrl) - throws IOException { - Properties pom = null; - try (InputStream inputStream = archiveUrl.openStream(); - JarInputStream jarInputStream = new JarInputStream(inputStream)) { - // Advance the jarInputStream to the pom.properties entry - for (JarEntry entry = jarInputStream.getNextJarEntry(); - entry != null; - entry = jarInputStream.getNextJarEntry()) { - if (entry.getName().startsWith("META-INF/maven") - && entry.getName().endsWith("pom.properties")) { - if (pom != null) { - // we've found multiple pom files. bail! - return; - } - pom = new Properties(); - pom.load(jarInputStream); - } - } - } - if (pom == null) { - return; - } - String groupId = pom.getProperty("groupId"); - String artifactId = pom.getProperty("artifactId"); - if (groupId != null && !groupId.isEmpty() && artifactId != null && !artifactId.isEmpty()) { - builder.put(PACKAGE_NAME, groupId + ":" + artifactId); - } - String version = pom.getProperty("version"); - if (version != null && !version.isEmpty()) { - builder.put(PACKAGE_VERSION, version); - } - } - - private JarAnalyzerUtil() {} -} 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..5d3ec866e6f6 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarDetails.java @@ -0,0 +1,258 @@ +/* + * 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 { + private static final Map EMBEDDED_FORMAT_TO_EXTENSION = + Stream.of("ear", "war", "jar") + .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 JarDetails(URL url, JarFile jarFile) throws IOException { + this.url = url; + this.jarFile = jarFile; + this.pom = getPom(); + this.manifest = getManifest(); + } + + 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 Embedded(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}. */ + 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}. */ + 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}. */ + @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, or null if not found. */ + @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}. + */ + @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. */ + @Nullable + String computeSha1() { + return computeDigest(SHA1.get()); + } + + private String computeDigest(MessageDigest md) { + 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); + } catch (IOException e) { + return null; + } + } + + /** + * 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 Embedded extends JarDetails { + + private final JarEntry jarEntry; + + private Embedded(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; + } + } + } +}