Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add request details to transactions created through OpenTelemetry #4098

Merged
merged 10 commits into from
Jan 24, 2025
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
### Fixes

- Avoid logging an error when a float is passed in the manifest ([#4031](https://github.com/getsentry/sentry-java/pull/4031))
- Add `request` details to transactions created through OpenTelemetry ([#4098](https://github.com/getsentry/sentry-java/pull/4098))
- We now add HTTP request method and URL where Sentry expects it to display it in Sentry UI
- Remove `java.lang.ClassNotFoundException` debug logs when searching for OpenTelemetry marker classes ([#4091](https://github.com/getsentry/sentry-java/pull/4091))
- There was up to three of these, one for `io.sentry.opentelemetry.agent.AgentMarker`, `io.sentry.opentelemetry.agent.AgentlessMarker` and `io.sentry.opentelemetry.agent.AgentlessSpringMarker`.
- These were not indicators of something being wrong but rather the SDK looking at what is available at runtime to configure itself accordingly.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
public final class io/sentry/opentelemetry/OpenTelemetryAttributesExtractor {
public fun <init> ()V
public fun extract (Lio/opentelemetry/sdk/trace/data/SpanData;Lio/sentry/ISpan;Lio/sentry/IScope;)V
}

public final class io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor : io/sentry/EventProcessor {
public fun <init> ()V
public fun getOrder ()Ljava/lang/Long;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.sentry.opentelemetry;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.semconv.HttpAttributes;
import io.opentelemetry.semconv.ServerAttributes;
import io.opentelemetry.semconv.UrlAttributes;
import io.sentry.IScope;
import io.sentry.ISpan;
import io.sentry.protocol.Request;
import io.sentry.util.UrlUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class OpenTelemetryAttributesExtractor {

public void extract(
final @NotNull SpanData otelSpan,
final @NotNull ISpan sentrySpan,
final @NotNull IScope scope) {
final @NotNull Attributes attributes = otelSpan.getAttributes();
addRequestAttributesToScope(attributes, scope);
}

private void addRequestAttributesToScope(Attributes attributes, IScope scope) {
if (scope.getRequest() == null) {
scope.setRequest(new Request());
}
final @Nullable Request request = scope.getRequest();
if (request != null) {
final @Nullable String requestMethod = attributes.get(HttpAttributes.HTTP_REQUEST_METHOD);
if (requestMethod != null) {
request.setMethod(requestMethod);
}

if (request.getUrl() == null) {
final @Nullable String urlFull = attributes.get(UrlAttributes.URL_FULL);
if (urlFull != null) {
final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(urlFull);
urlDetails.applyToRequest(request);
}
}

if (request.getUrl() == null) {
final String urlString = buildUrlString(attributes);
if (!urlString.isEmpty()) {
request.setUrl(urlString);
}
}

if (request.getQueryString() == null) {
final @Nullable String query = attributes.get(UrlAttributes.URL_QUERY);
if (query != null) {
request.setQueryString(query);
}
}
}
}

private @NotNull String buildUrlString(final @NotNull Attributes attributes) {
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 "";
}

final @NotNull StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(scheme);
urlBuilder.append("://");

if (serverAddress != null) {
urlBuilder.append(serverAddress);
if (serverPort != null) {
urlBuilder.append(":");
urlBuilder.append(serverPort);
}
}

if (path != null) {
urlBuilder.append(path);
}

return urlBuilder.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.Instrumenter;
import io.sentry.ScopeType;
import io.sentry.ScopesAdapter;
import io.sentry.SentryDate;
import io.sentry.SentryInstantDate;
Expand Down Expand Up @@ -50,6 +51,8 @@ public final class SentrySpanExporter implements SpanExporter {
private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance();
private final @NotNull SpanDescriptionExtractor spanDescriptionExtractor =
new SpanDescriptionExtractor();
private final @NotNull OpenTelemetryAttributesExtractor attributesExtractor =
new OpenTelemetryAttributesExtractor();
private final @NotNull IScopes scopes;

private final @NotNull List<String> attributeKeysToRemove =
Expand Down Expand Up @@ -267,8 +270,10 @@ private void transferSpanDetails(
spanStorage.getSentrySpan(span.getSpanContext());
final @Nullable IScopes scopesMaybe =
sentrySpanMaybe != null ? sentrySpanMaybe.getScopes() : null;
final @NotNull IScopes scopesToUse =
final @NotNull IScopes scopesToUseBeforeForking =
scopesMaybe == null ? ScopesAdapter.getInstance() : scopesMaybe;
final @NotNull IScopes scopesToUse =
scopesToUseBeforeForking.forkedCurrentScope("SentrySpanExporter.createTransaction");
final @NotNull OtelSpanInfo spanInfo =
spanDescriptionExtractor.extractSpanInfo(span, sentrySpanMaybe);

Expand Down Expand Up @@ -331,6 +336,9 @@ private void transferSpanDetails(
setOtelSpanKind(span, sentryTransaction);
transferSpanDetails(sentrySpanMaybe, sentryTransaction);

scopesToUse.configureScope(
ScopeType.CURRENT, scope -> attributesExtractor.extract(span, sentryTransaction, scope));

return sentryTransaction;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package io.sentry.opentelemetry

import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.sdk.internal.AttributesMap
import io.opentelemetry.sdk.trace.data.SpanData
import io.opentelemetry.semconv.ServerAttributes
import io.opentelemetry.semconv.UrlAttributes
import io.sentry.ISpan
import io.sentry.Scope
import io.sentry.SentryOptions
import io.sentry.protocol.Request
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull

class OpenTelemetryAttributesExtractorTest {

private class Fixture {
val spanData = mock<SpanData>()
val attributes = AttributesMap.create(100, 100)
val sentrySpan = mock<ISpan>()
val options = SentryOptions.empty()
val scope = Scope(options)

init {
whenever(spanData.attributes).thenReturn(attributes)
}
}

private val fixture = Fixture()

@Test
fun `sets URL based on OTel attributes`() {
givenAttributes(
mapOf(
UrlAttributes.URL_SCHEME to "https",
UrlAttributes.URL_PATH to "/path/to/123",
UrlAttributes.URL_QUERY to "q=123456&b=X",
ServerAttributes.SERVER_ADDRESS to "io.sentry",
ServerAttributes.SERVER_PORT to 8081L
)
)

whenExtractingAttributes()

thenRequestIsSet()
thenUrlIsSetTo("https://io.sentry:8081/path/to/123")
thenQueryIsSetTo("q=123456&b=X")
}

@Test
fun `when there is an existing request on scope it is filled with more details`() {
fixture.scope.request = Request().also { it.bodySize = 123L }
givenAttributes(
mapOf(
UrlAttributes.URL_SCHEME to "https",
UrlAttributes.URL_PATH to "/path/to/123",
UrlAttributes.URL_QUERY to "q=123456&b=X",
ServerAttributes.SERVER_ADDRESS to "io.sentry",
ServerAttributes.SERVER_PORT to 8081L
)
)

whenExtractingAttributes()

thenRequestIsSet()
thenUrlIsSetTo("https://io.sentry:8081/path/to/123")
thenQueryIsSetTo("q=123456&b=X")
assertEquals(123L, fixture.scope.request!!.bodySize)
}

@Test
fun `when there is an existing request with url on scope it is kept`() {
fixture.scope.request = Request().also {
it.url = "http://docs.sentry.io:3000/platform"
it.queryString = "s=abc"
}
givenAttributes(
mapOf(
UrlAttributes.URL_SCHEME to "https",
UrlAttributes.URL_PATH to "/path/to/123",
UrlAttributes.URL_QUERY to "q=123456&b=X",
ServerAttributes.SERVER_ADDRESS to "io.sentry",
ServerAttributes.SERVER_PORT to 8081L
)
)

whenExtractingAttributes()

thenRequestIsSet()
thenUrlIsSetTo("http://docs.sentry.io:3000/platform")
thenQueryIsSetTo("s=abc")
}

@Test
fun `when there is an existing request with url on scope it is kept with URL_FULL`() {
fixture.scope.request = Request().also {
it.url = "http://docs.sentry.io:3000/platform"
it.queryString = "s=abc"
}
givenAttributes(
mapOf(
UrlAttributes.URL_FULL to "https://io.sentry:8081/path/to/123?q=123456&b=X"
)
)

whenExtractingAttributes()

thenRequestIsSet()
thenUrlIsSetTo("http://docs.sentry.io:3000/platform")
thenQueryIsSetTo("s=abc")
}

@Test
fun `sets URL based on OTel attributes without port`() {
givenAttributes(
mapOf(
UrlAttributes.URL_SCHEME to "https",
UrlAttributes.URL_PATH to "/path/to/123",
ServerAttributes.SERVER_ADDRESS to "io.sentry"
)
)

whenExtractingAttributes()

thenRequestIsSet()
thenUrlIsSetTo("https://io.sentry/path/to/123")
}

@Test
fun `sets URL based on OTel attributes without path`() {
givenAttributes(
mapOf(
UrlAttributes.URL_SCHEME to "https",
ServerAttributes.SERVER_ADDRESS to "io.sentry"
)
)

whenExtractingAttributes()

thenRequestIsSet()
thenUrlIsSetTo("https://io.sentry")
}

@Test
fun `does not set URL if server address is missing`() {
givenAttributes(
mapOf(
UrlAttributes.URL_SCHEME to "https"
)
)

whenExtractingAttributes()

thenRequestIsSet()
thenUrlIsNotSet()
}

@Test
fun `does not set URL if scheme is missing`() {
givenAttributes(
mapOf(
ServerAttributes.SERVER_ADDRESS to "io.sentry"
)
)

whenExtractingAttributes()

thenRequestIsSet()
thenUrlIsNotSet()
}

private fun givenAttributes(map: Map<AttributeKey<out Any>, Any>) {
map.forEach { k, v ->
fixture.attributes.put(k, v)
}
}

private fun whenExtractingAttributes() {
OpenTelemetryAttributesExtractor().extract(fixture.spanData, fixture.sentrySpan, fixture.scope)
}

private fun thenRequestIsSet() {
assertNotNull(fixture.scope.request)
}

private fun thenUrlIsSetTo(expected: String) {
assertEquals(expected, fixture.scope.request!!.url)
}

private fun thenUrlIsNotSet() {
assertNull(fixture.scope.request!!.url)
}

private fun thenQueryIsSetTo(expected: String) {
assertEquals(expected, fixture.scope.request!!.queryString)
}
}
Loading