diff --git a/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java b/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java index 6879723ebf9..4583dc9e6c1 100644 --- a/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java +++ b/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java @@ -131,7 +131,6 @@ public static DatabaseDetails parse(final @Nullable String databaseConnectionUrl String pathWithoutProperties = StringUtils.substringBefore(path, ";"); return new DatabaseDetails(dbSystem, pathWithoutProperties); } catch (Throwable t) { - System.out.println(t.getMessage()); // ignore } return new DatabaseDetails(dbSystem, null); diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-otlp/build.gradle.kts index d63c8a5c451..e6428020fea 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { api(libs.otel.extension.autoconfigure) api(libs.otel.exporter.otlp) compileOnly(libs.otel.extension.autoconfigure.spi) - // compileOnly(libs.otel.semconv) + implementation(libs.otel.semconv) // compileOnly(libs.otel.semconv.incubating) compileOnly(libs.jetbrains.annotations) diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java index a4249b27ec0..4cdf1b0ed09 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java @@ -2,6 +2,7 @@ import static io.sentry.SentryTraceHeader.SENTRY_TRACE_HEADER; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; @@ -11,14 +12,20 @@ import io.opentelemetry.context.propagation.TextMapGetter; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; import io.sentry.Baggage; import io.sentry.BaggageHeader; import io.sentry.IScopes; import io.sentry.ScopesAdapter; import io.sentry.SentryLevel; +import io.sentry.SentryOptions; import io.sentry.SentryTraceHeader; import io.sentry.exception.InvalidSentryTraceHeaderException; +import io.sentry.util.PropagationTargetsUtils; import io.sentry.util.TracingUtils; +import java.net.URL; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -61,6 +68,10 @@ public void inject(final Context context, final C carrier, final TextMapSett return; } + if (!shouldInjectTracingHeaders(otelSpan)) { + return; + } + setter.set( carrier, SENTRY_TRACE_HEADER, @@ -76,6 +87,50 @@ public void inject(final Context context, final C carrier, final TextMapSett } } + private boolean shouldInjectTracingHeaders(final @NotNull Span otelSpan) { + final @NotNull SentryOptions options = scopes.getOptions(); + final @Nullable String url = extractUrl(otelSpan, options); + + return url == null + || PropagationTargetsUtils.contain(options.getTracePropagationTargets(), url); + } + + private @Nullable String extractUrl( + final @NotNull Span otelSpan, final @NotNull SentryOptions options) { + if (!(otelSpan instanceof ReadableSpan)) { + return null; + } + + final @NotNull Attributes attributes = ((ReadableSpan) otelSpan).getAttributes(); + final @Nullable String urlFull = attributes.get(UrlAttributes.URL_FULL); + if (urlFull != null) { + return urlFull; + } + + final @Nullable String scheme = attributes.get(UrlAttributes.URL_SCHEME); + final @Nullable String serverAddress = attributes.get(ServerAttributes.SERVER_ADDRESS); + final @Nullable Long serverPort = attributes.get(ServerAttributes.SERVER_PORT); + final @Nullable String path = attributes.get(UrlAttributes.URL_PATH); + + if (scheme == null || serverAddress == null) { + return null; + } + + try { + final @NotNull String pathToUse = path == null ? "" : path; + if (serverPort == null) { + return new URL(scheme, serverAddress, pathToUse).toString(); + } else { + return new URL(scheme, serverAddress, serverPort.intValue(), pathToUse).toString(); + } + } catch (Throwable t) { + options + .getLogger() + .log(SentryLevel.WARNING, "Unable to combine URL span attributes into one.", t); + return null; + } + } + @Override public Context extract( final Context context, final C carrier, final TextMapGetter getter) { diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt index 1d5d56c5bff..afa728c7ff0 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt @@ -7,6 +7,7 @@ import io.opentelemetry.api.trace.TraceState import io.opentelemetry.context.Context import io.opentelemetry.context.propagation.TextMapGetter import io.opentelemetry.context.propagation.TextMapSetter +import io.opentelemetry.sdk.trace.SdkTracerProvider import io.sentry.Baggage import io.sentry.Sentry import kotlin.test.AfterTest @@ -171,6 +172,97 @@ class OpenTelemetryOtlpPropagatorTest { ) } + @Test + fun `injects headers if URL in span attributes matches tracePropagationTargets`() { + Sentry.init { options -> + options.dsn = "https://key@sentry.io/proj" + options.setTracePropagationTargets(listOf("sentry.io")) + } + val propagator = OpenTelemetryOtlpPropagator() + val carrier = mutableMapOf() + val tracerProvider = SdkTracerProvider.builder().build() + val otelSpan = + tracerProvider + .get("test") + .spanBuilder("test") + .setAttribute("url.full", "https://sentry.io/api/0/") + .startSpan() + val baggage = + Baggage.fromHeader( + "sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d" + ) + + try { + val context = + Context.root().with(otelSpan).with(OpenTelemetryOtlpPropagator.SENTRY_BAGGAGE_KEY, baggage) + + propagator.inject(context, carrier, MapSetter()) + } finally { + otelSpan.end() + tracerProvider.shutdown() + } + + assertEquals( + "${otelSpan.spanContext.traceId}-${otelSpan.spanContext.spanId}-1", + carrier["sentry-trace"], + ) + assertEquals( + "sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d", + carrier["baggage"], + ) + } + + @Test + fun `does not inject headers if URL in span attributes does not match tracePropagationTargets`() { + Sentry.init { options -> + options.dsn = "https://key@sentry.io/proj" + options.setTracePropagationTargets(listOf("github.com")) + } + val propagator = OpenTelemetryOtlpPropagator() + val carrier = mutableMapOf() + val tracerProvider = SdkTracerProvider.builder().build() + val otelSpan = + tracerProvider + .get("test") + .spanBuilder("test") + .setAttribute("url.full", "https://sentry.io/api/0/") + .startSpan() + + try { + propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) + } finally { + otelSpan.end() + tracerProvider.shutdown() + } + + assertNull(carrier["sentry-trace"]) + assertNull(carrier["baggage"]) + } + + @Test + fun `injects headers if tracePropagationTargets is restricted and URL is unavailable`() { + Sentry.init { options -> + options.dsn = "https://key@sentry.io/proj" + options.setTracePropagationTargets(listOf("sentry.io")) + } + val propagator = OpenTelemetryOtlpPropagator() + val carrier = mutableMapOf() + + val otelSpanContext = + SpanContext.create( + "f9118105af4a2d42b4124532cd1065ff", + "424cffc8f94feeee", + TraceFlags.getSampled(), + TraceState.getDefault(), + ) + val otelSpan = Span.wrap(otelSpanContext) + + propagator.inject(Context.root().with(otelSpan), carrier, MapSetter()) + + assertEquals("f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1", carrier["sentry-trace"]) + assertNull(carrier["baggage"]) + } + @Test fun `does not inject headers when no span in context`() { val propagator = OpenTelemetryOtlpPropagator() diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java index 6726302a83e..942cda241b7 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java @@ -15,6 +15,7 @@ import io.sentry.util.Objects; import io.sentry.util.SpanUtils; import io.sentry.util.TracingUtils; +import io.sentry.util.UrlUtils; import java.util.Locale; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,8 +46,9 @@ public SentrySpanClientWebRequestFilter(final @NotNull IScopes scopes) { final @NotNull SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final ISpan span = activeSpan.startChild("http.client", null, spanOptions); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.url().toString()); final @NotNull String method = request.method().name(); - span.setDescription(method + " " + request.url()); + span.setDescription(method + " " + urlDetails.getUrlOrFallback()); span.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)); final @NotNull ClientRequest modifiedRequest = maybeAddTracingHeaders(request, span); diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/mvc/SentrySpringIntegrationTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/mvc/SentrySpringIntegrationTest.kt index 4b7e806730f..36e6af289fd 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/mvc/SentrySpringIntegrationTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/mvc/SentrySpringIntegrationTest.kt @@ -376,7 +376,8 @@ class SentrySpringIntegrationTest { assertThat(transaction.spans).hasSize(1) val span = transaction.spans.first() assertThat(span.op).isEqualTo("http.client") - assertThat(span.description).isEqualTo("GET http://localhost:$port/hello") + assertThat(span.description) + .isEqualTo("GET http://[Filtered]:[Filtered]@localhost:$port/hello") assertThat(span.data?.get(SpanDataConvention.HTTP_STATUS_CODE_KEY)).isEqualTo(200) assertThat(span.status).isEqualTo(SpanStatus.OK) }, @@ -519,7 +520,7 @@ class HelloController(private val webClient: WebClient, private val env: Environ fun webClient(): String? { return webClient .get() - .uri("http://localhost:${env.getProperty("local.server.port")}/hello") + .uri("http://user:password@localhost:${env.getProperty("local.server.port")}/hello") .retrieve() .bodyToMono(String::class.java) .block() diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java index 1189532c0c4..ec29b9c68a3 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java @@ -15,6 +15,7 @@ import io.sentry.util.Objects; import io.sentry.util.SpanUtils; import io.sentry.util.TracingUtils; +import io.sentry.util.UrlUtils; import java.util.Locale; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,8 +46,9 @@ public SentrySpanClientWebRequestFilter(final @NotNull IScopes scopes) { final @NotNull SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final ISpan span = activeSpan.startChild("http.client", null, spanOptions); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.url().toString()); final @NotNull String method = request.method().name(); - span.setDescription(method + " " + request.url()); + span.setDescription(method + " " + urlDetails.getUrlOrFallback()); span.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)); final @NotNull ClientRequest modifiedRequest = maybeAddTracingHeaders(request, span); diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt index 6d399323f5f..f1837a08c1f 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt @@ -374,7 +374,8 @@ class SentrySpringIntegrationTest { assertThat(transaction.spans).hasSize(1) val span = transaction.spans.first() assertThat(span.op).isEqualTo("http.client") - assertThat(span.description).isEqualTo("GET http://localhost:$port/hello") + assertThat(span.description) + .isEqualTo("GET http://[Filtered]:[Filtered]@localhost:$port/hello") assertThat(span.data?.get(SpanDataConvention.HTTP_STATUS_CODE_KEY)).isEqualTo(200) assertThat(span.status).isEqualTo(SpanStatus.OK) }, @@ -517,7 +518,7 @@ class HelloController(private val webClient: WebClient, private val env: Environ fun webClient(): String? { return webClient .get() - .uri("http://localhost:${env.getProperty("local.server.port")}/hello") + .uri("http://user:password@localhost:${env.getProperty("local.server.port")}/hello") .retrieve() .bodyToMono(String::class.java) .block() diff --git a/sentry/src/main/java/io/sentry/transport/HttpConnection.java b/sentry/src/main/java/io/sentry/transport/HttpConnection.java index 71c3ebb15b2..e7935e76cf3 100644 --- a/sentry/src/main/java/io/sentry/transport/HttpConnection.java +++ b/sentry/src/main/java/io/sentry/transport/HttpConnection.java @@ -64,9 +64,10 @@ public HttpConnection( if (proxy != null && options.getProxy() != null) { final String proxyUser = options.getProxy().getUser(); final String proxyPassword = options.getProxy().getPass(); - - if (proxyUser != null && proxyPassword != null) { - authenticatorWrapper.setDefault(new ProxyAuthenticator(proxyUser, proxyPassword)); + final String proxyHost = options.getProxy().getHost(); + if (proxyUser != null && proxyPassword != null && proxyHost != null) { + authenticatorWrapper.setDefault( + new ProxyAuthenticator(proxyUser, proxyPassword, proxyHost)); } } } diff --git a/sentry/src/main/java/io/sentry/transport/ProxyAuthenticator.java b/sentry/src/main/java/io/sentry/transport/ProxyAuthenticator.java index 7aad9967f18..74b63566b37 100644 --- a/sentry/src/main/java/io/sentry/transport/ProxyAuthenticator.java +++ b/sentry/src/main/java/io/sentry/transport/ProxyAuthenticator.java @@ -9,6 +9,7 @@ final class ProxyAuthenticator extends Authenticator { private final @NotNull String user; private final @NotNull String password; + private final @NotNull String proxyHost; /** * Proxy authenticator. @@ -16,14 +17,16 @@ final class ProxyAuthenticator extends Authenticator { * @param user proxy username * @param password proxy password */ - ProxyAuthenticator(final @NotNull String user, final @NotNull String password) { + ProxyAuthenticator( + final @NotNull String user, final @NotNull String password, final @NotNull String proxyHost) { this.user = Objects.requireNonNull(user, "user is required"); this.password = Objects.requireNonNull(password, "password is required"); + this.proxyHost = Objects.requireNonNull(proxyHost, "proxyHost is required"); } @Override protected @Nullable PasswordAuthentication getPasswordAuthentication() { - if (getRequestorType() == RequestorType.PROXY) { + if (getRequestorType() == RequestorType.PROXY && proxyHost.equals(getRequestingHost())) { return new PasswordAuthentication(user, password.toCharArray()); } return null; diff --git a/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt b/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt index 2a856ab5800..4c17c71fecc 100644 --- a/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt +++ b/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt @@ -259,6 +259,17 @@ class HttpConnectionTest { assertEquals(Type.SOCKS, transport.proxy!!.type()) } + @Test + fun `When Proxy username and password are given but host is missing, authenticator is not set`() { + fixture.proxy = Proxy(null, "8090", "some-user", "some-password") + val transport = fixture.getSUT() + + transport.send(createEnvelope()) + + assertNull(transport.proxy) + verify(fixture.authenticatorWrapper, never()).setDefault(any()) + } + @Test fun `sets common headers and on http connection`() { val transport = fixture.getSUT() diff --git a/sentry/src/test/java/io/sentry/transport/ProxyAuthenticatorTest.kt b/sentry/src/test/java/io/sentry/transport/ProxyAuthenticatorTest.kt new file mode 100644 index 00000000000..c3af7da226d --- /dev/null +++ b/sentry/src/test/java/io/sentry/transport/ProxyAuthenticatorTest.kt @@ -0,0 +1,62 @@ +package io.sentry.transport + +import java.net.Authenticator +import java.net.Authenticator.RequestorType +import java.net.PasswordAuthentication +import java.net.URL +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProxyAuthenticatorTest { + @BeforeTest + @AfterTest + fun reset() { + Authenticator.setDefault(null) + } + + @Test + fun `returns authentication when proxy request host matches proxy host`() { + Authenticator.setDefault(ProxyAuthenticator("some-user", "some-password", "proxy.example.com")) + + val authentication = requestPasswordAuthentication("proxy.example.com", RequestorType.PROXY) + + assertEquals("some-user", authentication!!.userName) + assertEquals("some-password", String(authentication.password)) + } + + @Test + fun `returns null when requestor type is not proxy`() { + Authenticator.setDefault(ProxyAuthenticator("some-user", "some-password", "proxy.example.com")) + + val authentication = requestPasswordAuthentication("proxy.example.com", RequestorType.SERVER) + + assertNull(authentication) + } + + @Test + fun `returns null when proxy request host does not match proxy host`() { + Authenticator.setDefault(ProxyAuthenticator("some-user", "some-password", "proxy.example.com")) + + val authentication = requestPasswordAuthentication("other.example.com", RequestorType.PROXY) + + assertNull(authentication) + } + + private fun requestPasswordAuthentication( + host: String, + requestorType: RequestorType, + ): PasswordAuthentication? = + Authenticator.requestPasswordAuthentication( + host, + null, + 8080, + "https", + "prompt", + "basic", + URL("https://sentry.io"), + requestorType, + ) +}