Skip to content

Support for (auto) instrumentation for Spring Cloud Gateway MVC routes #16413

@holgerstolzenberg

Description

@holgerstolzenberg

Is your feature request related to a problem? Please describe.

Currently, it looks like "only" the reactive variant of Spring Cloud Gateway is natively supported by OpenTelemetry starter. In a recent project, we decided to go with the MVC variant, as in the advent of virtual threads, reactive approach was ruled out due to the complexities it introduces.

I was able to work around this by implementing a custom HandlerInterceptor:

package eu.eliagroup.gras.iam.gatekeeper.service.core.opentelemetry;

import static java.lang.String.format;
import static java.util.Optional.ofNullable;
import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import jakarta.annotation.Nonnull;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.val;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

/// The interceptor sets up the trace context for any given route.
///
/// It relies on the W3C Trace Context Propagator to extract the trace context from the request
/// headers.
///
/// After request completion, the trace- and span IDs get removed from the MDC.
@Component
@Order(HIGHEST_PRECEDENCE)
final class TraceContextInterceptor implements HandlerInterceptor {
  private static final String TRACE_ID = "trace_id";
  private static final String SPAN_ID = "span_id";

  @Override
  public boolean preHandle(
      @Nonnull final HttpServletRequest request,
      @Nonnull final HttpServletResponse response,
      @Nonnull final Object handler) {
    val extractedContext =
        W3CTraceContextPropagator.getInstance()
            .extract(Context.current(), request, new HeaderExtractor());

    try (final var ignored = extractedContext.makeCurrent()) {
      val currentSpan = Span.current();
      currentSpan.updateName(
          format("%s %s", request.getMethod().toUpperCase(), request.getRequestURI()));

      final var spanContext = currentSpan.getSpanContext();

      if (spanContext.isValid()) {
        MDC.put(TRACE_ID, spanContext.getTraceId());
        MDC.put(SPAN_ID, spanContext.getSpanId());
      }
    }

    return true;
  }

  @Override
  public void afterCompletion(
      @Nonnull final HttpServletRequest request,
      @Nonnull final HttpServletResponse response,
      @Nonnull final Object handler,
      final Exception ex) {
    MDC.remove(TRACE_ID);
    MDC.remove(SPAN_ID);
  }

  private static final class HeaderExtractor implements TextMapGetter<HttpServletRequest> {
    @Override
    public Iterable<String> keys(final HttpServletRequest carrier) {
      return () -> carrier.getHeaderNames().asIterator();
    }

    @Override
    public String get(final HttpServletRequest carrier, final String headerName) {
      return ofNullable(carrier).map(c -> c.getHeader(headerName)).orElse(null);
    }
  }
}

TBH - the feature is not of high priority, yet I think it would be a nice addition.

Describe the solution you'd like

It would be nice if the starter instruments Gateway MVC routes automagically.

Describe alternatives you've considered

None.

Additional context

None.

Tip

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it. Learn more here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds triageNew issue that requires triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions