Skip to content

Commit 29a4185

Browse files
authored
Add request details to transactions created through OpenTelemetry (#4098)
* Attach request object to event for OTel * fix test name * rename test class * changelog * do not override existing url on request even with full url
1 parent 5c403ff commit 29a4185

File tree

5 files changed

+307
-1
lines changed

5 files changed

+307
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
### Fixes
1414

1515
- Avoid logging an error when a float is passed in the manifest ([#4031](https://github.com/getsentry/sentry-java/pull/4031))
16+
- Add `request` details to transactions created through OpenTelemetry ([#4098](https://github.com/getsentry/sentry-java/pull/4098))
17+
- We now add HTTP request method and URL where Sentry expects it to display it in Sentry UI
1618
- Remove `java.lang.ClassNotFoundException` debug logs when searching for OpenTelemetry marker classes ([#4091](https://github.com/getsentry/sentry-java/pull/4091))
1719
- 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`.
1820
- These were not indicators of something being wrong but rather the SDK looking at what is available at runtime to configure itself accordingly.

sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
public final class io/sentry/opentelemetry/OpenTelemetryAttributesExtractor {
2+
public fun <init> ()V
3+
public fun extract (Lio/opentelemetry/sdk/trace/data/SpanData;Lio/sentry/ISpan;Lio/sentry/IScope;)V
4+
}
5+
16
public final class io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor : io/sentry/EventProcessor {
27
public fun <init> ()V
38
public fun getOrder ()Ljava/lang/Long;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.sentry.opentelemetry;
2+
3+
import io.opentelemetry.api.common.Attributes;
4+
import io.opentelemetry.sdk.trace.data.SpanData;
5+
import io.opentelemetry.semconv.HttpAttributes;
6+
import io.opentelemetry.semconv.ServerAttributes;
7+
import io.opentelemetry.semconv.UrlAttributes;
8+
import io.sentry.IScope;
9+
import io.sentry.ISpan;
10+
import io.sentry.protocol.Request;
11+
import io.sentry.util.UrlUtils;
12+
import org.jetbrains.annotations.ApiStatus;
13+
import org.jetbrains.annotations.NotNull;
14+
import org.jetbrains.annotations.Nullable;
15+
16+
@ApiStatus.Internal
17+
public final class OpenTelemetryAttributesExtractor {
18+
19+
public void extract(
20+
final @NotNull SpanData otelSpan,
21+
final @NotNull ISpan sentrySpan,
22+
final @NotNull IScope scope) {
23+
final @NotNull Attributes attributes = otelSpan.getAttributes();
24+
addRequestAttributesToScope(attributes, scope);
25+
}
26+
27+
private void addRequestAttributesToScope(Attributes attributes, IScope scope) {
28+
if (scope.getRequest() == null) {
29+
scope.setRequest(new Request());
30+
}
31+
final @Nullable Request request = scope.getRequest();
32+
if (request != null) {
33+
final @Nullable String requestMethod = attributes.get(HttpAttributes.HTTP_REQUEST_METHOD);
34+
if (requestMethod != null) {
35+
request.setMethod(requestMethod);
36+
}
37+
38+
if (request.getUrl() == null) {
39+
final @Nullable String urlFull = attributes.get(UrlAttributes.URL_FULL);
40+
if (urlFull != null) {
41+
final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(urlFull);
42+
urlDetails.applyToRequest(request);
43+
}
44+
}
45+
46+
if (request.getUrl() == null) {
47+
final String urlString = buildUrlString(attributes);
48+
if (!urlString.isEmpty()) {
49+
request.setUrl(urlString);
50+
}
51+
}
52+
53+
if (request.getQueryString() == null) {
54+
final @Nullable String query = attributes.get(UrlAttributes.URL_QUERY);
55+
if (query != null) {
56+
request.setQueryString(query);
57+
}
58+
}
59+
}
60+
}
61+
62+
private @NotNull String buildUrlString(final @NotNull Attributes attributes) {
63+
final @Nullable String scheme = attributes.get(UrlAttributes.URL_SCHEME);
64+
final @Nullable String serverAddress = attributes.get(ServerAttributes.SERVER_ADDRESS);
65+
final @Nullable Long serverPort = attributes.get(ServerAttributes.SERVER_PORT);
66+
final @Nullable String path = attributes.get(UrlAttributes.URL_PATH);
67+
68+
if (scheme == null || serverAddress == null) {
69+
return "";
70+
}
71+
72+
final @NotNull StringBuilder urlBuilder = new StringBuilder();
73+
urlBuilder.append(scheme);
74+
urlBuilder.append("://");
75+
76+
if (serverAddress != null) {
77+
urlBuilder.append(serverAddress);
78+
if (serverPort != null) {
79+
urlBuilder.append(":");
80+
urlBuilder.append(serverPort);
81+
}
82+
}
83+
84+
if (path != null) {
85+
urlBuilder.append(path);
86+
}
87+
88+
return urlBuilder.toString();
89+
}
90+
}

sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.sentry.ISpan;
2020
import io.sentry.ITransaction;
2121
import io.sentry.Instrumenter;
22+
import io.sentry.ScopeType;
2223
import io.sentry.ScopesAdapter;
2324
import io.sentry.SentryDate;
2425
import io.sentry.SentryInstantDate;
@@ -50,6 +51,8 @@ public final class SentrySpanExporter implements SpanExporter {
5051
private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance();
5152
private final @NotNull SpanDescriptionExtractor spanDescriptionExtractor =
5253
new SpanDescriptionExtractor();
54+
private final @NotNull OpenTelemetryAttributesExtractor attributesExtractor =
55+
new OpenTelemetryAttributesExtractor();
5356
private final @NotNull IScopes scopes;
5457

5558
private final @NotNull List<String> attributeKeysToRemove =
@@ -267,8 +270,10 @@ private void transferSpanDetails(
267270
spanStorage.getSentrySpan(span.getSpanContext());
268271
final @Nullable IScopes scopesMaybe =
269272
sentrySpanMaybe != null ? sentrySpanMaybe.getScopes() : null;
270-
final @NotNull IScopes scopesToUse =
273+
final @NotNull IScopes scopesToUseBeforeForking =
271274
scopesMaybe == null ? ScopesAdapter.getInstance() : scopesMaybe;
275+
final @NotNull IScopes scopesToUse =
276+
scopesToUseBeforeForking.forkedCurrentScope("SentrySpanExporter.createTransaction");
272277
final @NotNull OtelSpanInfo spanInfo =
273278
spanDescriptionExtractor.extractSpanInfo(span, sentrySpanMaybe);
274279

@@ -331,6 +336,9 @@ private void transferSpanDetails(
331336
setOtelSpanKind(span, sentryTransaction);
332337
transferSpanDetails(sentrySpanMaybe, sentryTransaction);
333338

339+
scopesToUse.configureScope(
340+
ScopeType.CURRENT, scope -> attributesExtractor.extract(span, sentryTransaction, scope));
341+
334342
return sentryTransaction;
335343
}
336344

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package io.sentry.opentelemetry
2+
3+
import io.opentelemetry.api.common.AttributeKey
4+
import io.opentelemetry.sdk.internal.AttributesMap
5+
import io.opentelemetry.sdk.trace.data.SpanData
6+
import io.opentelemetry.semconv.ServerAttributes
7+
import io.opentelemetry.semconv.UrlAttributes
8+
import io.sentry.ISpan
9+
import io.sentry.Scope
10+
import io.sentry.SentryOptions
11+
import io.sentry.protocol.Request
12+
import org.mockito.kotlin.mock
13+
import org.mockito.kotlin.whenever
14+
import kotlin.test.Test
15+
import kotlin.test.assertEquals
16+
import kotlin.test.assertNotNull
17+
import kotlin.test.assertNull
18+
19+
class OpenTelemetryAttributesExtractorTest {
20+
21+
private class Fixture {
22+
val spanData = mock<SpanData>()
23+
val attributes = AttributesMap.create(100, 100)
24+
val sentrySpan = mock<ISpan>()
25+
val options = SentryOptions.empty()
26+
val scope = Scope(options)
27+
28+
init {
29+
whenever(spanData.attributes).thenReturn(attributes)
30+
}
31+
}
32+
33+
private val fixture = Fixture()
34+
35+
@Test
36+
fun `sets URL based on OTel attributes`() {
37+
givenAttributes(
38+
mapOf(
39+
UrlAttributes.URL_SCHEME to "https",
40+
UrlAttributes.URL_PATH to "/path/to/123",
41+
UrlAttributes.URL_QUERY to "q=123456&b=X",
42+
ServerAttributes.SERVER_ADDRESS to "io.sentry",
43+
ServerAttributes.SERVER_PORT to 8081L
44+
)
45+
)
46+
47+
whenExtractingAttributes()
48+
49+
thenRequestIsSet()
50+
thenUrlIsSetTo("https://io.sentry:8081/path/to/123")
51+
thenQueryIsSetTo("q=123456&b=X")
52+
}
53+
54+
@Test
55+
fun `when there is an existing request on scope it is filled with more details`() {
56+
fixture.scope.request = Request().also { it.bodySize = 123L }
57+
givenAttributes(
58+
mapOf(
59+
UrlAttributes.URL_SCHEME to "https",
60+
UrlAttributes.URL_PATH to "/path/to/123",
61+
UrlAttributes.URL_QUERY to "q=123456&b=X",
62+
ServerAttributes.SERVER_ADDRESS to "io.sentry",
63+
ServerAttributes.SERVER_PORT to 8081L
64+
)
65+
)
66+
67+
whenExtractingAttributes()
68+
69+
thenRequestIsSet()
70+
thenUrlIsSetTo("https://io.sentry:8081/path/to/123")
71+
thenQueryIsSetTo("q=123456&b=X")
72+
assertEquals(123L, fixture.scope.request!!.bodySize)
73+
}
74+
75+
@Test
76+
fun `when there is an existing request with url on scope it is kept`() {
77+
fixture.scope.request = Request().also {
78+
it.url = "http://docs.sentry.io:3000/platform"
79+
it.queryString = "s=abc"
80+
}
81+
givenAttributes(
82+
mapOf(
83+
UrlAttributes.URL_SCHEME to "https",
84+
UrlAttributes.URL_PATH to "/path/to/123",
85+
UrlAttributes.URL_QUERY to "q=123456&b=X",
86+
ServerAttributes.SERVER_ADDRESS to "io.sentry",
87+
ServerAttributes.SERVER_PORT to 8081L
88+
)
89+
)
90+
91+
whenExtractingAttributes()
92+
93+
thenRequestIsSet()
94+
thenUrlIsSetTo("http://docs.sentry.io:3000/platform")
95+
thenQueryIsSetTo("s=abc")
96+
}
97+
98+
@Test
99+
fun `when there is an existing request with url on scope it is kept with URL_FULL`() {
100+
fixture.scope.request = Request().also {
101+
it.url = "http://docs.sentry.io:3000/platform"
102+
it.queryString = "s=abc"
103+
}
104+
givenAttributes(
105+
mapOf(
106+
UrlAttributes.URL_FULL to "https://io.sentry:8081/path/to/123?q=123456&b=X"
107+
)
108+
)
109+
110+
whenExtractingAttributes()
111+
112+
thenRequestIsSet()
113+
thenUrlIsSetTo("http://docs.sentry.io:3000/platform")
114+
thenQueryIsSetTo("s=abc")
115+
}
116+
117+
@Test
118+
fun `sets URL based on OTel attributes without port`() {
119+
givenAttributes(
120+
mapOf(
121+
UrlAttributes.URL_SCHEME to "https",
122+
UrlAttributes.URL_PATH to "/path/to/123",
123+
ServerAttributes.SERVER_ADDRESS to "io.sentry"
124+
)
125+
)
126+
127+
whenExtractingAttributes()
128+
129+
thenRequestIsSet()
130+
thenUrlIsSetTo("https://io.sentry/path/to/123")
131+
}
132+
133+
@Test
134+
fun `sets URL based on OTel attributes without path`() {
135+
givenAttributes(
136+
mapOf(
137+
UrlAttributes.URL_SCHEME to "https",
138+
ServerAttributes.SERVER_ADDRESS to "io.sentry"
139+
)
140+
)
141+
142+
whenExtractingAttributes()
143+
144+
thenRequestIsSet()
145+
thenUrlIsSetTo("https://io.sentry")
146+
}
147+
148+
@Test
149+
fun `does not set URL if server address is missing`() {
150+
givenAttributes(
151+
mapOf(
152+
UrlAttributes.URL_SCHEME to "https"
153+
)
154+
)
155+
156+
whenExtractingAttributes()
157+
158+
thenRequestIsSet()
159+
thenUrlIsNotSet()
160+
}
161+
162+
@Test
163+
fun `does not set URL if scheme is missing`() {
164+
givenAttributes(
165+
mapOf(
166+
ServerAttributes.SERVER_ADDRESS to "io.sentry"
167+
)
168+
)
169+
170+
whenExtractingAttributes()
171+
172+
thenRequestIsSet()
173+
thenUrlIsNotSet()
174+
}
175+
176+
private fun givenAttributes(map: Map<AttributeKey<out Any>, Any>) {
177+
map.forEach { k, v ->
178+
fixture.attributes.put(k, v)
179+
}
180+
}
181+
182+
private fun whenExtractingAttributes() {
183+
OpenTelemetryAttributesExtractor().extract(fixture.spanData, fixture.sentrySpan, fixture.scope)
184+
}
185+
186+
private fun thenRequestIsSet() {
187+
assertNotNull(fixture.scope.request)
188+
}
189+
190+
private fun thenUrlIsSetTo(expected: String) {
191+
assertEquals(expected, fixture.scope.request!!.url)
192+
}
193+
194+
private fun thenUrlIsNotSet() {
195+
assertNull(fixture.scope.request!!.url)
196+
}
197+
198+
private fun thenQueryIsSetTo(expected: String) {
199+
assertEquals(expected, fixture.scope.request!!.queryString)
200+
}
201+
}

0 commit comments

Comments
 (0)