Skip to content

Commit

Permalink
Add HTTP server request headers from OpenTelemetry span attributes to…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
adinauer authored Feb 26, 2025
1 parent cde02ad commit 3b6cfdf
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 35 deletions.
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/response 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,7 +1,7 @@
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 extractUrl (Lio/opentelemetry/api/common/Attributes;)Ljava/lang/String;
public fun extract (Lio/opentelemetry/sdk/trace/data/SpanData;Lio/sentry/IScope;Lio/sentry/SentryOptions;)V
public fun extractUrl (Lio/opentelemetry/api/common/Attributes;Lio/sentry/SentryOptions;)Ljava/lang/String;
}

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,26 +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(
final @NotNull Attributes attributes, final @NotNull IScope scope) {
final @NotNull Attributes attributes,
final @NotNull IScope scope,
final @NotNull SentryOptions options) {
if (scope.getRequest() == null) {
scope.setRequest(new Request());
}
Expand All @@ -37,7 +50,7 @@ private void addRequestAttributesToScope(
}

if (request.getUrl() == null) {
final @Nullable String url = extractUrl(attributes);
final @Nullable String url = extractUrl(attributes, options);
if (url != null) {
final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(url);
urlDetails.applyToRequest(request);
Expand All @@ -50,24 +63,69 @@ private void addRequestAttributesToScope(
request.setQueryString(query);
}
}

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

public @Nullable String extractUrl(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;
}

public @Nullable String extractUrl(
final @NotNull Attributes attributes, final @NotNull SentryOptions options) {
final @Nullable String urlFull = attributes.get(UrlAttributes.URL_FULL);
if (urlFull != null) {
return urlFull;
}

final String urlString = buildUrlString(attributes);
final String urlString = buildUrlString(attributes, options);
if (!urlString.isEmpty()) {
return urlString;
}

return null;
}

private @NotNull String buildUrlString(final @NotNull Attributes attributes) {
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 @@ -77,22 +135,18 @@ private void addRequestAttributesToScope(
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 @@ -17,6 +17,7 @@
import io.sentry.ScopesAdapter;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.SentryTraceHeader;
import io.sentry.exception.InvalidSentryTraceHeaderException;
import io.sentry.util.TracingUtils;
Expand Down Expand Up @@ -76,7 +77,7 @@ public <C> void inject(final Context context, final C carrier, final TextMapSett
return;
}

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

private @Nullable String getUrl(final @NotNull IOtelSpanWrapper sentrySpan) {
private @Nullable String getUrl(
final @NotNull IOtelSpanWrapper sentrySpan, final @NotNull SentryOptions options) {
final @Nullable Attributes attributes = sentrySpan.getOpenTelemetrySpanAttributes();
if (attributes == null) {
return null;
}
return attributesExtractor.extractUrl(attributes);
return attributesExtractor.extractUrl(attributes, options);
}

@Override
Expand Down
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
Loading

0 comments on commit 3b6cfdf

Please sign in to comment.