Skip to content

Commit 3b6cfdf

Browse files
authored
Add HTTP server request headers from OpenTelemetry span attributes to sentry request in payload (#4102)
* Attach request object to event for OTel * fix test name * add http server request headers to sentry request in payload * rename test class * changelog * do not override existing url on request even with full url * pass in options and use them * remove span param; remove test exception * changelog * changelog pii * Use `java.net.URL` for combining url attributes (#4105) * changelog * do not send request headers in contexts/otel/attributes * also remove response headers from span attributes sent to Sentry * Apply suggestions from code review
1 parent cde02ad commit 3b6cfdf

File tree

6 files changed

+171
-35
lines changed

6 files changed

+171
-35
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
### Features
66

7+
- Add HTTP server request headers from OpenTelemetry span attributes to sentry `request` in payload ([#4102](https://github.com/getsentry/sentry-java/pull/4102))
8+
- 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)
9+
- Please only enable headers you actually want to send to Sentry. Some may contain sensitive data like PII, cookies, tokens etc.
10+
- We are no longer adding request/response headers to `contexts/otel/attributes` of the event.
711
- The `ignoredErrors` option is now configurable via the manifest property `io.sentry.traces.ignored-errors` ([#4178](https://github.com/getsentry/sentry-java/pull/4178))
812
- 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))
913
- This consists of an empty list when only the default profile is active

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
public final class io/sentry/opentelemetry/OpenTelemetryAttributesExtractor {
22
public fun <init> ()V
3-
public fun extract (Lio/opentelemetry/sdk/trace/data/SpanData;Lio/sentry/ISpan;Lio/sentry/IScope;)V
4-
public fun extractUrl (Lio/opentelemetry/api/common/Attributes;)Ljava/lang/String;
3+
public fun extract (Lio/opentelemetry/sdk/trace/data/SpanData;Lio/sentry/IScope;Lio/sentry/SentryOptions;)V
4+
public fun extractUrl (Lio/opentelemetry/api/common/Attributes;Lio/sentry/SentryOptions;)Ljava/lang/String;
55
}
66

77
public final class io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor : io/sentry/EventProcessor {

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

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,39 @@
66
import io.opentelemetry.semconv.ServerAttributes;
77
import io.opentelemetry.semconv.UrlAttributes;
88
import io.sentry.IScope;
9-
import io.sentry.ISpan;
9+
import io.sentry.SentryLevel;
10+
import io.sentry.SentryOptions;
1011
import io.sentry.protocol.Request;
12+
import io.sentry.util.HttpUtils;
13+
import io.sentry.util.StringUtils;
1114
import io.sentry.util.UrlUtils;
15+
import java.net.URL;
16+
import java.util.HashMap;
17+
import java.util.List;
18+
import java.util.Map;
1219
import org.jetbrains.annotations.ApiStatus;
1320
import org.jetbrains.annotations.NotNull;
1421
import org.jetbrains.annotations.Nullable;
1522

1623
@ApiStatus.Internal
1724
public final class OpenTelemetryAttributesExtractor {
1825

26+
private static final String HTTP_REQUEST_HEADER_PREFIX = "http.request.header.";
27+
1928
public void extract(
2029
final @NotNull SpanData otelSpan,
21-
final @NotNull ISpan sentrySpan,
22-
final @NotNull IScope scope) {
30+
final @NotNull IScope scope,
31+
final @NotNull SentryOptions options) {
2332
final @NotNull Attributes attributes = otelSpan.getAttributes();
24-
addRequestAttributesToScope(attributes, scope);
33+
if (attributes.get(HttpAttributes.HTTP_REQUEST_METHOD) != null) {
34+
addRequestAttributesToScope(attributes, scope, options);
35+
}
2536
}
2637

2738
private void addRequestAttributesToScope(
28-
final @NotNull Attributes attributes, final @NotNull IScope scope) {
39+
final @NotNull Attributes attributes,
40+
final @NotNull IScope scope,
41+
final @NotNull SentryOptions options) {
2942
if (scope.getRequest() == null) {
3043
scope.setRequest(new Request());
3144
}
@@ -37,7 +50,7 @@ private void addRequestAttributesToScope(
3750
}
3851

3952
if (request.getUrl() == null) {
40-
final @Nullable String url = extractUrl(attributes);
53+
final @Nullable String url = extractUrl(attributes, options);
4154
if (url != null) {
4255
final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(url);
4356
urlDetails.applyToRequest(request);
@@ -50,24 +63,69 @@ private void addRequestAttributesToScope(
5063
request.setQueryString(query);
5164
}
5265
}
66+
67+
if (request.getHeaders() == null) {
68+
Map<String, String> headers = collectHeaders(attributes, options);
69+
if (!headers.isEmpty()) {
70+
request.setHeaders(headers);
71+
}
72+
}
5373
}
5474
}
5575

56-
public @Nullable String extractUrl(final @NotNull Attributes attributes) {
76+
@SuppressWarnings("unchecked")
77+
private static Map<String, String> collectHeaders(
78+
final @NotNull Attributes attributes, final @NotNull SentryOptions options) {
79+
Map<String, String> headers = new HashMap<>();
80+
81+
attributes.forEach(
82+
(key, value) -> {
83+
final @NotNull String attributeKeyAsString = key.getKey();
84+
if (attributeKeyAsString.startsWith(HTTP_REQUEST_HEADER_PREFIX)) {
85+
final @NotNull String headerName =
86+
StringUtils.removePrefix(attributeKeyAsString, HTTP_REQUEST_HEADER_PREFIX);
87+
if (options.isSendDefaultPii() || !HttpUtils.containsSensitiveHeader(headerName)) {
88+
if (value instanceof List) {
89+
try {
90+
final @NotNull List<String> headerValues = (List<String>) value;
91+
headers.put(
92+
headerName,
93+
toString(
94+
HttpUtils.filterOutSecurityCookiesFromHeader(
95+
headerValues, headerName, null)));
96+
} catch (Throwable t) {
97+
options
98+
.getLogger()
99+
.log(SentryLevel.WARNING, "Expected a List<String> as header", t);
100+
}
101+
}
102+
}
103+
}
104+
});
105+
return headers;
106+
}
107+
108+
public @Nullable String extractUrl(
109+
final @NotNull Attributes attributes, final @NotNull SentryOptions options) {
57110
final @Nullable String urlFull = attributes.get(UrlAttributes.URL_FULL);
58111
if (urlFull != null) {
59112
return urlFull;
60113
}
61114

62-
final String urlString = buildUrlString(attributes);
115+
final String urlString = buildUrlString(attributes, options);
63116
if (!urlString.isEmpty()) {
64117
return urlString;
65118
}
66119

67120
return null;
68121
}
69122

70-
private @NotNull String buildUrlString(final @NotNull Attributes attributes) {
123+
private static @Nullable String toString(final @Nullable List<String> list) {
124+
return list != null ? String.join(",", list) : null;
125+
}
126+
127+
private @NotNull String buildUrlString(
128+
final @NotNull Attributes attributes, final @NotNull SentryOptions options) {
71129
final @Nullable String scheme = attributes.get(UrlAttributes.URL_SCHEME);
72130
final @Nullable String serverAddress = attributes.get(ServerAttributes.SERVER_ADDRESS);
73131
final @Nullable Long serverPort = attributes.get(ServerAttributes.SERVER_PORT);
@@ -77,22 +135,18 @@ private void addRequestAttributesToScope(
77135
return "";
78136
}
79137

80-
final @NotNull StringBuilder urlBuilder = new StringBuilder();
81-
urlBuilder.append(scheme);
82-
urlBuilder.append("://");
83-
84-
if (serverAddress != null) {
85-
urlBuilder.append(serverAddress);
86-
if (serverPort != null) {
87-
urlBuilder.append(":");
88-
urlBuilder.append(serverPort);
138+
try {
139+
final @NotNull String pathToUse = path == null ? "" : path;
140+
if (serverPort == null) {
141+
return new URL(scheme, serverAddress, pathToUse).toString();
142+
} else {
143+
return new URL(scheme, serverAddress, serverPort.intValue(), pathToUse).toString();
89144
}
145+
} catch (Throwable t) {
146+
options
147+
.getLogger()
148+
.log(SentryLevel.WARNING, "Unable to combine URL span attributes into one.", t);
149+
return "";
90150
}
91-
92-
if (path != null) {
93-
urlBuilder.append(path);
94-
}
95-
96-
return urlBuilder.toString();
97151
}
98152
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.sentry.ScopesAdapter;
1818
import io.sentry.Sentry;
1919
import io.sentry.SentryLevel;
20+
import io.sentry.SentryOptions;
2021
import io.sentry.SentryTraceHeader;
2122
import io.sentry.exception.InvalidSentryTraceHeaderException;
2223
import io.sentry.util.TracingUtils;
@@ -76,7 +77,7 @@ public <C> void inject(final Context context, final C carrier, final TextMapSett
7677
return;
7778
}
7879

79-
final @Nullable String url = getUrl(sentrySpan);
80+
final @Nullable String url = getUrl(sentrySpan, scopes.getOptions());
8081
final @Nullable TracingUtils.TracingHeaders tracingHeaders =
8182
url == null
8283
? TracingUtils.trace(scopes, Collections.emptyList(), sentrySpan)
@@ -92,12 +93,13 @@ public <C> void inject(final Context context, final C carrier, final TextMapSett
9293
}
9394
}
9495

95-
private @Nullable String getUrl(final @NotNull IOtelSpanWrapper sentrySpan) {
96+
private @Nullable String getUrl(
97+
final @NotNull IOtelSpanWrapper sentrySpan, final @NotNull SentryOptions options) {
9698
final @Nullable Attributes attributes = sentrySpan.getOpenTelemetrySpanAttributes();
9799
if (attributes == null) {
98100
return null;
99101
}
100-
return attributesExtractor.extractUrl(attributes);
102+
return attributesExtractor.extractUrl(attributes, options);
101103
}
102104

103105
@Override

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ public final class SentrySpanExporter implements SpanExporter {
6868
InternalSemanticAttributes.PARENT_SAMPLED.getKey(),
6969
ProcessIncubatingAttributes.PROCESS_COMMAND_ARGS.getKey() // can be very long
7070
);
71+
72+
private final @NotNull List<String> attributeToRemoveByPrefix =
73+
Arrays.asList("http.request.header.", "http.response.header.");
7174
private static final @NotNull Long SPAN_TIMEOUT = DateUtils.secondsToNanos(5 * 60);
7275

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

340343
scopesToUse.configureScope(
341-
ScopeType.CURRENT, scope -> attributesExtractor.extract(span, sentryTransaction, scope));
344+
ScopeType.CURRENT,
345+
scope -> attributesExtractor.extract(span, scope, scopesToUse.getOptions()));
342346

343347
return sentryTransaction;
344348
}
@@ -488,7 +492,17 @@ private SpanStatus mapOtelStatus(
488492
}
489493

490494
private boolean shouldRemoveAttribute(final @NotNull String key) {
491-
return attributeKeysToRemove.contains(key);
495+
if (attributeKeysToRemove.contains(key)) {
496+
return true;
497+
}
498+
499+
for (String prefix : attributeToRemoveByPrefix) {
500+
if (key.startsWith(prefix)) {
501+
return true;
502+
}
503+
}
504+
505+
return false;
492506
}
493507

494508
private void setOtelInstrumentationInfo(

0 commit comments

Comments
 (0)