Search code examples
javaspring-bootaws-xray

How to access incoming HTTP requests in X-Ray SegmentListener?


Issue

I use AWS X-Ray SDK for Java to enable X-Ray tracing for my Spring Boot micro services. With following snippet I am able to attach a custom SegmentListener:

final AWSXRayRecorder recorder = AWSXRayRecorderBuilder
                .standard()
                .withPlugin(new EcsPlugin())
                .withSegmentListener(new SLF4JSegmentListener())
                .withSegmentListener(new MyHttpHeaderSegementListener())
                .build();
AWSXRay.setGlobalRecorder(recorder);

In MyHttpHeaderSegementListener I try to inject a X-Ray annotation based on an incoming HTTP request header (from the frontend):


public class MyHttpHeaderSegementListener implements SegmentListener {

  // snippet source: https://stackoverflow.com/a/54349178/6489012
  public static Optional<HttpServletRequest> getCurrentHttpRequest() {
    return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
                   .filter(ServletRequestAttributes.class::isInstance)
                   .map(ServletRequestAttributes.class::cast)
                   .map(ServletRequestAttributes::getRequest);
  }

  public MyHttpHeaderSegementListener() {}

  @Override
  public void onBeginSegment(final Segment segment) {
    final var httpContext = MyHttpHeaderSegementListener.getCurrentHttpRequest();
    httpContext.ifPresent(context -> segment.putAnnotation("Origin", context.getHeader("Origin")));
  }

}

The segment listener is triggered as expected onBeginSegment segment but MyHttpHeaderSegementListener.getCurrentHttpRequest() always returns an Optional.empty.

Questions

  1. Is there a possibility to inspect incoming HTTP requests (as they were received by a Controller) within a SegmentListener?
  2. Does aws-xray-sdk-java maybe even support a native way to do so?
  3. Why is the request retrieved from RequestContextHolder always empty?

(A bit off-topic but: 4. Is it even a good practice to set an annotation based on a HTTP header)


Solution

  • I have no answer for the 2. and 3. question but I found an answer for 1. question.

    For incoming requests you need to add a Spring Filter to configure AWS X-Ray. As filters have access to the HTTP request I just wrapped my own filter around the com.amazonaws.xray.javax.servlet.AWSXRayServletFilter of AWS:

    public class XRayServletFilter extends AWSXRayServletFilter {
      public XRayServletFilter(String fixedSegmentName) {
        super(fixedSegmentName);
      }
    
      @Override
      public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
        this.addHttpRequestToContext(request);
    
        super.doFilter(request, response, chain);
      }
    
      private void addHttpRequestToContext(final ServletRequest request){
        final Optional<HttpServletRequest> httpServletRequest = HttpRequestUtils.castToHttpRequest(request);
    
        if (httpServletRequest.isPresent()) {
          final ServletRequestAttributes requestAttributes = new ServletRequestAttributes(httpServletRequest.get());
          RequestContextHolder.setRequestAttributes(requestAttributes);
        }
      }
    }
    

    Which uses a static class that I wrote:

    public final class HttpRequestUtils {
      public static Optional<HttpServletRequest> getCurrentHttpRequest() {
        return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
                       .filter(ServletRequestAttributes.class::isInstance)
                       .map(ServletRequestAttributes.class::cast)
                       .map(ServletRequestAttributes::getRequest);
      }
    
      public static Optional<HttpServletRequest> castToHttpRequest(ServletRequest request) {
        try {
          return Optional.of((HttpServletRequest) request);
        } catch (ClassCastException classCastException) {
          return Optional.empty();
        }
      }
    }
    

    This custom filter basically sets the HTTP requests in the RequestContextHolder. After that you can use it in your segment listeners:

    public class MyHttpHeaderSegementListener implements SegmentListener {
      public MyHttpHeaderSegementListener() {}
    
      @Override
      public void onBeginSegment(final Segment segment) {
        final Optional<HttpServletRequest> request = HttpRequestUtils.getCurrentHttpRequest();
    
        request.map(req -> req.getHeader("Origin")).ifPresent(origin -> segment.putAnnotation("client_origin", origin));;
      }
    }