Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8ef3825
Attach request object to event for OTel
adinauer Jan 23, 2025
d3cab7c
fix test name
adinauer Jan 23, 2025
7c51a68
Merge branch 'main' into feat/request-for-otel
adinauer Jan 23, 2025
9a183ab
add http server request headers to sentry request in payload
adinauer Jan 23, 2025
ecd6040
rename test class
adinauer Jan 23, 2025
6e0b24d
changelog
adinauer Jan 23, 2025
3f10e3c
Merge branch 'main' into feat/request-for-otel
adinauer Jan 23, 2025
28cb76a
Merge branch 'main' into feat/request-for-otel
adinauer Jan 24, 2025
28a7e56
do not override existing url on request even with full url
adinauer Jan 24, 2025
c59f801
Merge branch 'feat/request-for-otel' into feat/otel-server-request-he…
adinauer Jan 24, 2025
0f77ec2
pass in options and use them
adinauer Jan 24, 2025
66d3b7b
remove span param; remove test exception
adinauer Jan 24, 2025
0d96359
changelog
adinauer Jan 24, 2025
ba2cbb0
changelog pii
adinauer Jan 24, 2025
1d89f04
Merge branch 'main' into feat/otel-server-request-headers
adinauer Jan 24, 2025
e1f7fec
Use `java.net.URL` for combining url attributes (#4105)
adinauer Jan 30, 2025
d698a14
changelog
adinauer Feb 3, 2025
12aca6b
do not send request headers in contexts/otel/attributes
adinauer Feb 3, 2025
90fb3d9
Merge branch 'main' into feat/otel-server-request-headers
adinauer Feb 20, 2025
3233b3c
also remove response headers from span attributes sent to Sentry
adinauer Feb 25, 2025
00d488d
Merge branch 'main' into feat/otel-server-request-headers
adinauer Feb 25, 2025
badf8ba
Apply suggestions from code review
adinauer Feb 25, 2025
8bc3f0b
Merge branch 'main' into feat/otel-server-request-headers
adinauer Feb 26, 2025
b2fef65
Merge branch 'main' into feat/otel-server-request-headers
adinauer Feb 26, 2025
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

### Features

- Add HTTP server request headers from OpenTelemetry span attributes to sentry `request` in payload ([#4102](https://github.com/getsentry/sentry-java/pull/4102))
- You have to explicitly enable each header by adding it to the [OpenTelemetry config](https://opentelemetry.io/docs/zero-code/java/agent/instrumentation/http/#capturing-http-request-and-response-headers)
- Please only enable headers you actually want to send to Sentry. Some may contain sensitive data like PII, cookies, tokens etc.
- We are no longer adding request headers to `contexts/otel/attributes` of the event.
- The `ignoredErrors` option is now configurable via the manifest property `io.sentry.traces.ignored-errors` ([#4178](https://github.com/getsentry/sentry-java/pull/4178))
- A list of active Spring profiles is attached to payloads sent to Sentry (errors, traces, etc.) and displayed in the UI when using our Spring or Spring Boot integrations ([#4147](https://github.com/getsentry/sentry-java/pull/4147))
- This consists of an empty list when only the default profile is active
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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 fun extract (Lio/opentelemetry/sdk/trace/data/SpanData;Lio/sentry/IScope;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor : io/sentry/EventProcessor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,39 @@
import io.opentelemetry.semconv.ServerAttributes;
import io.opentelemetry.semconv.UrlAttributes;
import io.sentry.IScope;
import io.sentry.ISpan;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.protocol.Request;
import io.sentry.util.HttpUtils;
import io.sentry.util.StringUtils;
import io.sentry.util.UrlUtils;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class OpenTelemetryAttributesExtractor {

private static final String HTTP_REQUEST_HEADER_PREFIX = "http.request.header.";

public void extract(
final @NotNull SpanData otelSpan,
final @NotNull ISpan sentrySpan,
final @NotNull IScope scope) {
final @NotNull IScope scope,
final @NotNull SentryOptions options) {
final @NotNull Attributes attributes = otelSpan.getAttributes();
addRequestAttributesToScope(attributes, scope);
if (attributes.get(HttpAttributes.HTTP_REQUEST_METHOD) != null) {
addRequestAttributesToScope(attributes, scope, options);
}
}

private void addRequestAttributesToScope(Attributes attributes, IScope scope) {
private void addRequestAttributesToScope(
final @NotNull Attributes attributes,
final @NotNull IScope scope,
final @NotNull SentryOptions options) {
if (scope.getRequest() == null) {
scope.setRequest(new Request());
}
Expand All @@ -44,7 +58,7 @@ private void addRequestAttributesToScope(Attributes attributes, IScope scope) {
}

if (request.getUrl() == null) {
final String urlString = buildUrlString(attributes);
final String urlString = buildUrlString(attributes, options);
if (!urlString.isEmpty()) {
request.setUrl(urlString);
}
Expand All @@ -56,10 +70,54 @@ private void addRequestAttributesToScope(Attributes attributes, IScope scope) {
request.setQueryString(query);
}
}

if (request.getHeaders() == null) {
Map<String, String> headers = collectHeaders(attributes, options);
if (!headers.isEmpty()) {
request.setHeaders(headers);
}
}
}
}

private @NotNull String buildUrlString(final @NotNull Attributes attributes) {
@SuppressWarnings("unchecked")
private static Map<String, String> collectHeaders(
final @NotNull Attributes attributes, final @NotNull SentryOptions options) {
Map<String, String> headers = new HashMap<>();

attributes.forEach(
(key, value) -> {
final @NotNull String attributeKeyAsString = key.getKey();
if (attributeKeyAsString.startsWith(HTTP_REQUEST_HEADER_PREFIX)) {
final @NotNull String headerName =
StringUtils.removePrefix(attributeKeyAsString, HTTP_REQUEST_HEADER_PREFIX);
if (options.isSendDefaultPii() || !HttpUtils.containsSensitiveHeader(headerName)) {
if (value instanceof List) {
try {
final @NotNull List<String> headerValues = (List<String>) value;
headers.put(
headerName,
toString(
HttpUtils.filterOutSecurityCookiesFromHeader(
headerValues, headerName, null)));
} catch (Throwable t) {
options
.getLogger()
.log(SentryLevel.WARNING, "Expected a List<String> as header", t);
}
}
}
}
});
return headers;
}

private static @Nullable String toString(final @Nullable List<String> list) {
return list != null ? String.join(",", list) : null;
}

private @NotNull String buildUrlString(
final @NotNull Attributes attributes, final @NotNull SentryOptions options) {
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);
Expand All @@ -69,22 +127,18 @@ private void addRequestAttributesToScope(Attributes attributes, IScope scope) {
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);
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 "";
}

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

return urlBuilder.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ public final class SentrySpanExporter implements SpanExporter {
InternalSemanticAttributes.PARENT_SAMPLED.getKey(),
ProcessIncubatingAttributes.PROCESS_COMMAND_ARGS.getKey() // can be very long
);

private final @NotNull List<String> attributeToRemoveByPrefix =
Arrays.asList("http.request.header.", "http.response.header.");
private static final @NotNull Long SPAN_TIMEOUT = DateUtils.secondsToNanos(5 * 60);

public static final String TRACE_ORIGIN = "auto.opentelemetry";
Expand Down Expand Up @@ -338,7 +341,8 @@ private void transferSpanDetails(
transferSpanDetails(sentrySpanMaybe, sentryTransaction);

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

return sentryTransaction;
}
Expand Down Expand Up @@ -488,7 +492,17 @@ private SpanStatus mapOtelStatus(
}

private boolean shouldRemoveAttribute(final @NotNull String key) {
return attributeKeysToRemove.contains(key);
if (attributeKeysToRemove.contains(key)) {
return true;
}

for (String prefix : attributeToRemoveByPrefix) {
if (key.startsWith(prefix)) {
return true;
}
}

return false;
}

private void setOtelInstrumentationInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ 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.HttpAttributes
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.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull

Expand All @@ -21,7 +22,6 @@ 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)

Expand All @@ -36,6 +36,7 @@ class OpenTelemetryAttributesExtractorTest {
fun `sets URL based on OTel attributes`() {
givenAttributes(
mapOf(
HttpAttributes.HTTP_REQUEST_METHOD to "GET",
UrlAttributes.URL_SCHEME to "https",
UrlAttributes.URL_PATH to "/path/to/123",
UrlAttributes.URL_QUERY to "q=123456&b=X",
Expand All @@ -56,6 +57,7 @@ class OpenTelemetryAttributesExtractorTest {
fixture.scope.request = Request().also { it.bodySize = 123L }
givenAttributes(
mapOf(
HttpAttributes.HTTP_REQUEST_METHOD to "GET",
UrlAttributes.URL_SCHEME to "https",
UrlAttributes.URL_PATH to "/path/to/123",
UrlAttributes.URL_QUERY to "q=123456&b=X",
Expand All @@ -80,6 +82,7 @@ class OpenTelemetryAttributesExtractorTest {
}
givenAttributes(
mapOf(
HttpAttributes.HTTP_REQUEST_METHOD to "GET",
UrlAttributes.URL_SCHEME to "https",
UrlAttributes.URL_PATH to "/path/to/123",
UrlAttributes.URL_QUERY to "q=123456&b=X",
Expand Down Expand Up @@ -118,6 +121,7 @@ class OpenTelemetryAttributesExtractorTest {
fun `sets URL based on OTel attributes without port`() {
givenAttributes(
mapOf(
HttpAttributes.HTTP_REQUEST_METHOD to "GET",
UrlAttributes.URL_SCHEME to "https",
UrlAttributes.URL_PATH to "/path/to/123",
ServerAttributes.SERVER_ADDRESS to "io.sentry"
Expand All @@ -134,6 +138,7 @@ class OpenTelemetryAttributesExtractorTest {
fun `sets URL based on OTel attributes without path`() {
givenAttributes(
mapOf(
HttpAttributes.HTTP_REQUEST_METHOD to "GET",
UrlAttributes.URL_SCHEME to "https",
ServerAttributes.SERVER_ADDRESS to "io.sentry"
)
Expand All @@ -149,6 +154,7 @@ class OpenTelemetryAttributesExtractorTest {
fun `does not set URL if server address is missing`() {
givenAttributes(
mapOf(
HttpAttributes.HTTP_REQUEST_METHOD to "GET",
UrlAttributes.URL_SCHEME to "https"
)
)
Expand All @@ -163,6 +169,7 @@ class OpenTelemetryAttributesExtractorTest {
fun `does not set URL if scheme is missing`() {
givenAttributes(
mapOf(
HttpAttributes.HTTP_REQUEST_METHOD to "GET",
ServerAttributes.SERVER_ADDRESS to "io.sentry"
)
)
Expand All @@ -173,20 +180,67 @@ class OpenTelemetryAttributesExtractorTest {
thenUrlIsNotSet()
}

@Test
fun `sets server request headers based on OTel attributes and merges list of values`() {
givenAttributes(
mapOf(
HttpAttributes.HTTP_REQUEST_METHOD to "GET",
AttributeKey.stringArrayKey("http.request.header.baggage") to listOf("sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d", "another-baggage=abc,more=def"),
AttributeKey.stringArrayKey("http.request.header.sentry-trace") to listOf("f9118105af4a2d42b4124532cd176588-4542d085bb0b4de5"),
AttributeKey.stringArrayKey("http.response.header.some-header") to listOf("some-value")
)
)

whenExtractingAttributes()

thenRequestIsSet()
thenHeaderIsPresentOnRequest("baggage", "sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d,another-baggage=abc,more=def")
thenHeaderIsPresentOnRequest("sentry-trace", "f9118105af4a2d42b4124532cd176588-4542d085bb0b4de5")
thenHeaderIsNotPresentOnRequest("some-header")
}

@Test
fun `if there are no header attributes does not set headers on request`() {
givenAttributes(mapOf(HttpAttributes.HTTP_REQUEST_METHOD to "GET"))

whenExtractingAttributes()

thenRequestIsSet()
assertNull(fixture.scope.request!!.headers)
}

@Test
fun `if there is no request method attribute does not set request on scope`() {
givenAttributes(
mapOf(
UrlAttributes.URL_SCHEME to "https",
ServerAttributes.SERVER_ADDRESS to "io.sentry"
)
)

whenExtractingAttributes()

thenRequestIsNotSet()
}

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)
OpenTelemetryAttributesExtractor().extract(fixture.spanData, fixture.scope, fixture.options)
}

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

private fun thenRequestIsNotSet() {
assertNull(fixture.scope.request)
}

private fun thenUrlIsSetTo(expected: String) {
assertEquals(expected, fixture.scope.request!!.url)
}
Expand All @@ -198,4 +252,12 @@ class OpenTelemetryAttributesExtractorTest {
private fun thenQueryIsSetTo(expected: String) {
assertEquals(expected, fixture.scope.request!!.queryString)
}

private fun thenHeaderIsPresentOnRequest(headerName: String, expectedValue: String) {
assertEquals(expectedValue, fixture.scope.request!!.headers!!.get(headerName))
}

private fun thenHeaderIsNotPresentOnRequest(headerName: String) {
assertFalse(fixture.scope.request!!.headers!!.containsKey(headerName))
}
}
Loading