Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

### Fixes

- Close manifest input streams after version detection to avoid retaining JAR resources ([#6125](https://github.com/getsentry/sentry-java/pull/6125))
- Keep resolving the server name after `Sentry.close()` or a re-init. Closing the SDK shut down the shared hostname cache for the life of the process, so `server_name` silently froze at the value it had last resolved ([#6119](https://github.com/getsentry/sentry-java/pull/6119))

### Internal
Expand Down
102 changes: 60 additions & 42 deletions sentry/src/main/java/io/sentry/internal/ManifestVersionReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@
import io.sentry.SentryIntegrationPackageStorage;
import io.sentry.util.AutoClosableReentrantLock;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

@ApiStatus.Internal
public final class ManifestVersionReader {
private static volatile @Nullable ManifestVersionReader INSTANCE;
private static final @NotNull AutoClosableReentrantLock staticLock =
new AutoClosableReentrantLock();
private volatile boolean hasManifestBeenRead = false;
private final @NotNull ClassLoader classLoader;
private final @NotNull VersionInfoHolder versionInfo = new VersionInfoHolder();
private @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();

Expand All @@ -33,7 +37,14 @@ public final class ManifestVersionReader {
return INSTANCE;
}

private ManifestVersionReader() {}
private ManifestVersionReader() {
this(ClassLoader.getSystemClassLoader());
}

@TestOnly
ManifestVersionReader(final @NotNull ClassLoader classLoader) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this constructor allows the creation of mutliple instances of what was previously a lazily instantiated singleton.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for tests, I'll mark it accordingly.

this.classLoader = classLoader;
}

public @Nullable VersionInfoHolder readOpenTelemetryVersion() {
readManifestFiles();
Expand All @@ -52,52 +63,59 @@ public void readManifestFiles() {
if (hasManifestBeenRead) {
return;
}
final @NotNull Enumeration<URL> resources =
ClassLoader.getSystemClassLoader().getResources("META-INF/MANIFEST.MF");
final @NotNull Enumeration<URL> resources = classLoader.getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
try {
final @NotNull Manifest manifest = new Manifest(resources.nextElement().openStream());
final @Nullable Attributes mainAttributes = manifest.getMainAttributes();
if (mainAttributes != null) {
final @Nullable String name = mainAttributes.getValue("Sentry-Opentelemetry-SDK-Name");
final @Nullable String version = mainAttributes.getValue("Implementation-Version");
final @Nullable String sdkName = mainAttributes.getValue("Sentry-SDK-Name");
final @Nullable String packageName = mainAttributes.getValue("Sentry-SDK-Package-Name");
final @NotNull URLConnection connection = resources.nextElement().openConnection();
// Avoid retaining JarFile and inflater resources in the default cache for jar: URLs.
connection.setUseCaches(false);
try (final @NotNull InputStream inputStream = connection.getInputStream()) {
final @NotNull Manifest manifest = new Manifest(inputStream);
final @Nullable Attributes mainAttributes = manifest.getMainAttributes();
if (mainAttributes != null) {
final @Nullable String name =
mainAttributes.getValue("Sentry-Opentelemetry-SDK-Name");
final @Nullable String version = mainAttributes.getValue("Implementation-Version");
final @Nullable String sdkName = mainAttributes.getValue("Sentry-SDK-Name");
final @Nullable String packageName =
mainAttributes.getValue("Sentry-SDK-Package-Name");

if (name != null && version != null) {
versionInfo.sdkName = name;
versionInfo.sdkVersion = version;
final @Nullable String otelVersion =
mainAttributes.getValue("Sentry-Opentelemetry-Version-Name");
if (otelVersion != null) {
SentryIntegrationPackageStorage.getInstance()
.addPackage("maven:io.opentelemetry:opentelemetry-sdk", otelVersion);
SentryIntegrationPackageStorage.getInstance().addIntegration("OpenTelemetry");
}
final @Nullable String otelJavaagentVersion =
mainAttributes.getValue("Sentry-Opentelemetry-Javaagent-Version-Name");
if (otelJavaagentVersion != null) {
SentryIntegrationPackageStorage.getInstance()
.addPackage(
"maven:io.opentelemetry.javaagent:opentelemetry-javaagent",
otelJavaagentVersion);
SentryIntegrationPackageStorage.getInstance().addIntegration("OpenTelemetry-Agent");
if (name != null && version != null) {
versionInfo.sdkName = name;
versionInfo.sdkVersion = version;
final @Nullable String otelVersion =
mainAttributes.getValue("Sentry-Opentelemetry-Version-Name");
if (otelVersion != null) {
SentryIntegrationPackageStorage.getInstance()
.addPackage("maven:io.opentelemetry:opentelemetry-sdk", otelVersion);
SentryIntegrationPackageStorage.getInstance().addIntegration("OpenTelemetry");
}
final @Nullable String otelJavaagentVersion =
mainAttributes.getValue("Sentry-Opentelemetry-Javaagent-Version-Name");
if (otelJavaagentVersion != null) {
SentryIntegrationPackageStorage.getInstance()
.addPackage(
"maven:io.opentelemetry.javaagent:opentelemetry-javaagent",
otelJavaagentVersion);
SentryIntegrationPackageStorage.getInstance()
.addIntegration("OpenTelemetry-Agent");
}
if (name.equals("sentry.java.opentelemetry.agentless")) {
SentryIntegrationPackageStorage.getInstance()
.addIntegration("OpenTelemetry-Agentless");
}
if (name.equals("sentry.java.opentelemetry.agentless-spring")) {
SentryIntegrationPackageStorage.getInstance()
.addIntegration("OpenTelemetry-Agentless-Spring");
}
}
if (name.equals("sentry.java.opentelemetry.agentless")) {
SentryIntegrationPackageStorage.getInstance()
.addIntegration("OpenTelemetry-Agentless");
}
if (name.equals("sentry.java.opentelemetry.agentless-spring")) {
SentryIntegrationPackageStorage.getInstance()
.addIntegration("OpenTelemetry-Agentless-Spring");
}
}

if (sdkName != null
&& version != null
&& packageName != null
&& sdkName.startsWith("sentry.java")) {
SentryIntegrationPackageStorage.getInstance().addPackage(packageName, version);
if (sdkName != null
&& version != null
&& packageName != null
&& sdkName.startsWith("sentry.java")) {
SentryIntegrationPackageStorage.getInstance().addPackage(packageName, version);
}
}
}
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.sentry.internal

import com.google.common.truth.Truth.assertThat
import java.io.ByteArrayInputStream
import java.net.URL
import java.net.URLConnection
import java.nio.charset.StandardCharsets
import java.util.Collections
import kotlin.test.Test
import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

class ManifestVersionReaderTest {
private class CloseTrackingInputStream(content: String) :
ByteArrayInputStream(content.toByteArray(StandardCharsets.UTF_8)) {
var isClosed = false

override fun close() {
isClosed = true
super.close()
}
}

private class Fixture(contents: List<String>) {
val classLoader = mock<ClassLoader>()
val inputStreams = contents.map(::CloseTrackingInputStream)
val connections = contents.map { mock<URLConnection>() }
val urls = contents.map { mock<URL>() }

init {
whenever(classLoader.getResources("META-INF/MANIFEST.MF"))
.thenReturn(Collections.enumeration(urls))
urls.indices.forEach { index ->
whenever(urls[index].openConnection()).thenReturn(connections[index])
whenever(connections[index].inputStream).thenReturn(inputStreams[index])
}
}

val sut = ManifestVersionReader(classLoader)
}

@Test
fun `closes manifest stream and disables connection caching before opening it`() {
val fixture = Fixture(listOf(validManifest()))

fixture.sut.readManifestFiles()

assertThat(fixture.inputStreams.single().isClosed).isTrue()
inOrder(fixture.connections.single()) {
verify(fixture.connections.single()).useCaches = false
verify(fixture.connections.single()).inputStream
}
}

@Test
fun `closes malformed manifest stream and continues reading manifests`() {
val fixture = Fixture(listOf("not a manifest\n", validManifest()))

val versionInfo = fixture.sut.readOpenTelemetryVersion()

assertThat(fixture.inputStreams.map { it.isClosed }).containsExactly(true, true).inOrder()
assertThat(versionInfo).isNotNull()
assertThat(versionInfo!!.sdkName).isEqualTo("sentry.java.opentelemetry.test")
assertThat(versionInfo.sdkVersion).isEqualTo("1.2.3")
}

companion object {
private fun validManifest() =
"""
Manifest-Version: 1.0
Sentry-Opentelemetry-SDK-Name: sentry.java.opentelemetry.test
Implementation-Version: 1.2.3

"""
.trimIndent()
}
}
Loading