/** * Wrapper around {@link #doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain)} to make sure this * filter's logic is only executed once per request. */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { throw new ServletException(this.getClass().getName() + " only supports HTTP requests"); } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; boolean filterHasAlreadyExecuted = request.getAttribute(FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE) != null; if (filterHasAlreadyExecuted || skipDispatch(httpRequest)) { // Already executed or we're supposed to skip, so continue the filter chain without doing the // distributed tracing work. filterChain.doFilter(request, response); } else { // Time to execute the distributed tracing logic. request.setAttribute(FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE, Boolean.TRUE); doFilterInternal(httpRequest, httpResponse, filterChain); } }
@Override public void doFilter( ServletRequest request, ServletResponse response ) throws IOException, ServletException { // Verify that when the filter chain is called we're in doFilterInternal, and that the request has ALREADY_FILTERED_ATTRIBUTE_KEY set verify(spyFilter).doFilterInternal(requestMock, responseMock, this); verify(requestMock).setAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE, Boolean.TRUE ); verify(requestMock, times(0)).removeAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE ); ifObjectAddedThenSmartFilterChainCalled.add(true); } };
@Override public void doFilter( ServletRequest request, ServletResponse response ) throws IOException, ServletException { // Verify that when the filter chain is called we're in doFilterInternal, and that the request has ALREADY_FILTERED_ATTRIBUTE_KEY set verify(spyFilter).doFilterInternal(requestMock, responseMock, this); verify(requestMock).setAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE, Boolean.TRUE ); verify(requestMock, times(0)).removeAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE ); ifObjectAddedThenSmartFilterChainCalled.add(true); throw new IllegalStateException("boom"); } };
@Test public void doFilterInternal_should_set_trace_id_in_response_header() throws ServletException, IOException { // given: filter RequestTracingFilter filter = getBasicFilter(); // when: doFilterInternal is called filter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: response header should be set with the span's trace ID assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); verify(responseMock).setHeader(TraceHeaders.TRACE_ID, spanCapturingFilterChain.capturedSpan.getTraceId()); }
@Test public void doFilterInternal_should_not_call_setupTracingCompletionWhenAsyncRequestCompletes_when_isAsyncRequest_returns_false( ) throws ServletException, IOException { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); doReturn(false).when(filterSpy).isAsyncRequest(any(HttpServletRequest.class)); // when filterSpy.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); assertThat(spanCapturingFilterChain.capturedSpan.isCompleted()).isTrue(); verify(filterSpy, never()).setupTracingCompletionWhenAsyncRequestCompletes( any(HttpServletRequest.class), any(HttpServletResponse.class), any(TracingState.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); }
@Test public void doFilterInternal_should_set_request_attributes_to_new_span_info_with_alt_user_id( ) throws ServletException, IOException { // given: filter RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getHeader(ALT_USER_ID_HEADER_KEY)).willReturn("testUserId"); // when: doFilterInternal is called spyFilter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: request attributes should be set with the new span's info assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); Span newSpan = spanCapturingFilterChain.capturedSpan; assertThat(newSpan.getUserId()).isEqualTo("testUserId"); }
@Test public void doFilterInternal_should_set_request_attributes_to_new_span_info() throws ServletException, IOException { // given: filter RequestTracingFilter filter = getBasicFilter(); // when: doFilterInternal is called filter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: request attributes should be set with the new span's info assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); Span newSpan = spanCapturingFilterChain.capturedSpan; verify(requestMock).setAttribute(TraceHeaders.TRACE_SAMPLED, newSpan.isSampleable()); verify(requestMock).setAttribute(TraceHeaders.TRACE_ID, newSpan.getTraceId()); verify(requestMock).setAttribute(TraceHeaders.SPAN_ID, newSpan.getSpanId()); verify(requestMock).setAttribute(TraceHeaders.PARENT_SPAN_ID, newSpan.getParentSpanId()); verify(requestMock).setAttribute(TraceHeaders.SPAN_NAME, newSpan.getSpanName()); verify(requestMock).setAttribute(Span.class.getName(), newSpan); }
@Test public void doFilterInternal_should_not_add_async_listener_when_isAsyncRequest_returns_false( ) throws ServletException, IOException { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); doReturn(false).when(filterSpy).isAsyncRequest(any(HttpServletRequest.class)); setupAsyncContextWorkflow(); // when filterSpy.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); assertThat(spanCapturingFilterChain.capturedSpan.isCompleted()).isTrue(); assertThat(capturedAsyncListeners).hasSize(0); verify(filterSpy, never()).setupTracingCompletionWhenAsyncRequestCompletes( any(HttpServletRequest.class), any(HttpServletResponse.class), any(TracingState.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); }
@Test public void doFilterInternal_should_set_request_attributes_to_new_span_info_with_user_id( ) throws ServletException, IOException { // given: filter RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getHeader(USER_ID_HEADER_KEY)).willReturn("testUserId"); // when: doFilterInternal is called spyFilter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: request attributes should be set with the new span's info assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); Span newSpan = spanCapturingFilterChain.capturedSpan; assertThat(newSpan.getUserId()).isEqualTo("testUserId"); }
@Test public void doFilter_should_call_doFilterInternal_and_set_ALREADY_FILTERED_ATTRIBUTE_KEY_if_not_already_filtered_and_skipDispatch_returns_false() throws IOException, ServletException { // given: filter that returns false for skipDispatch and request that returns null for already-filtered attribute RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE)).willReturn(null); // when: doFilter() is called spyFilter.doFilter(requestMock, responseMock, filterChainMock); // then: doFilterInternal should be called and ALREADY_FILTERED_ATTRIBUTE_KEY should be set on the request verify(spyFilter).doFilterInternal(requestMock, responseMock, filterChainMock); verify(requestMock).setAttribute(RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE, Boolean.TRUE); }
@Test public void doFilter_should_not_call_doFilterInternal_if_already_filtered() throws IOException, ServletException { // given: filter that returns false for skipDispatch but request that returns non-null for already-filtered attribute RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getAttribute( RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE)).willReturn(Boolean.TRUE); // when: doFilter() is called spyFilter.doFilter(requestMock, responseMock, filterChainMock); // then: doFilterInternal should not be called verify(spyFilter, times(0)).doFilterInternal(requestMock, responseMock, filterChainMock); }
@Test public void doFilter_should_not_call_doFilterInternal_if_not_already_filtered_but_skipDispatch_returns_true( ) throws IOException, ServletException { // given: request that returns null for already-filtered attribute but filter that returns true for skipDispatch RequestTracingFilter spyFilter = spy(getBasicFilter()); doReturn(true).when(spyFilter).skipDispatch(any(HttpServletRequest.class)); given(requestMock.getAttribute(RequestTracingFilter.FILTER_HAS_ALREADY_EXECUTED_ATTRIBUTE)).willReturn(null); // when: doFilter() is called spyFilter.doFilter(requestMock, responseMock, filterChainMock); // then: doFilterInternal should not be called verify(spyFilter, times(0)).doFilterInternal(requestMock, responseMock, filterChainMock); verify(spyFilter).skipDispatch(requestMock); }
@Test public void doFilterInternal_should_not_complete_span_or_response_tags_until_after_filter_chain_runs( ) throws ServletException, IOException { // given: filter and filter chain that can tell us whether or not the span is complete at the time it is called RequestTracingFilter filter = getBasicFilter(); AtomicBoolean spanCompletedHolder = new AtomicBoolean(false); AtomicReference<Span> spanHolder = new AtomicReference<>(); AtomicReference<Boolean> requestTagsExecutedAtTimeOfFilterChain = new AtomicReference<>(); AtomicReference<Boolean> responseTagsExecutedAtTimeOfFilterChain = new AtomicReference<>(); FilterChain smartFilterChain = (request, response) -> { Span span = Tracer.getInstance().getCurrentSpan(); spanHolder.set(span); if (span != null) { spanCompletedHolder.set(span.isCompleted()); } requestTagsExecutedAtTimeOfFilterChain.set(strategyRequestTaggingMethodCalled.get()); responseTagsExecutedAtTimeOfFilterChain.set(strategyResponseTaggingAndFinalSpanNameMethodCalled.get()); }; // when: doFilterInternal is called filter.doFilterInternal(requestMock, responseMock, smartFilterChain); // then: we should be able to validate that the smartFilterChain was called, and when it was called the span // had not yet been completed, and after doFilterInternal finished it was completed. Similarly, when // the chain is being run, request tags should be done but response tags should not. assertThat(spanHolder.get()).isNotNull(); assertThat(spanCompletedHolder.get()).isFalse(); assertThat(spanHolder.get().isCompleted()).isTrue(); assertThat(requestTagsExecutedAtTimeOfFilterChain.get()).isTrue(); assertThat(responseTagsExecutedAtTimeOfFilterChain.get()).isFalse(); }
@Test public void doFilterInternal_should_call_setupTracingCompletionWhenAsyncRequestCompletes_when_isAsyncRequest_returns_true( ) throws ServletException, IOException { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); setupAsyncContextWorkflow(); doReturn(true).when(filterSpy).isAsyncRequest(any(HttpServletRequest.class)); // when filterSpy.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); assertThat(spanCapturingFilterChain.capturedSpan.isCompleted()).isFalse(); verify(filterSpy).setupTracingCompletionWhenAsyncRequestCompletes( eq(requestMock), eq(responseMock), any(TracingState.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); }
@Test public void doFilterInternal_should_add_async_listener_but_not_complete_span_when_async_request_is_detected( ) throws ServletException, IOException { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); setupAsyncContextWorkflow(); // when filterSpy.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); assertThat(spanCapturingFilterChain.capturedSpan.isCompleted()).isFalse(); assertThat(capturedAsyncListeners).hasSize(1); assertThat(capturedAsyncListeners.get(0)).isInstanceOf(WingtipsRequestSpanCompletionAsyncListener.class); verify(filterSpy).setupTracingCompletionWhenAsyncRequestCompletes( eq(requestMock), eq(responseMock), any(TracingState.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); }
@DataProvider(value = { "true", "false" }) @Test public void doFilterInternal_should_use_getInitialSpanName_for_span_name( boolean parentSpanExists ) throws ServletException, IOException { // given RequestTracingFilter filterSpy = spy(getBasicFilter()); filterSpy.tagAndNamingStrategy = tagAndNamingStrategy; filterSpy.tagAndNamingAdapter = tagAndNamingAdapterMock; String expectedSpanName = UUID.randomUUID().toString(); doReturn(expectedSpanName).when(filterSpy).getInitialSpanName( any(HttpServletRequest.class), any(HttpTagAndSpanNamingStrategy.class), any(HttpTagAndSpanNamingAdapter.class) ); if (parentSpanExists) { given(requestMock.getHeader(TraceHeaders.TRACE_ID)).willReturn(TraceAndSpanIdGenerator.generateId()); given(requestMock.getHeader(TraceHeaders.SPAN_ID)).willReturn(TraceAndSpanIdGenerator.generateId()); } // when filterSpy.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then assertThat(spanCapturingFilterChain.captureSpanCopyAtTimeOfDoFilter).isNotNull(); assertThat(spanCapturingFilterChain.captureSpanCopyAtTimeOfDoFilter.getSpanName()).isEqualTo(expectedSpanName); verify(filterSpy).getInitialSpanName(requestMock, tagAndNamingStrategy, tagAndNamingAdapterMock); }
@Test public void doFilterInternal_should_create_new_sampleable_span_if_no_parent_in_request_and_it_should_be_completed_and_tags_should_be_handled( ) throws ServletException, IOException { // given: filter RequestTracingFilter filter = getBasicFilter(); // when: doFilterInternal is called with a request that does not have a parent span filter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: a new valid sampleable span should be created and completed, // and tagging should have been done as expected Span span = spanCapturingFilterChain.capturedSpan; assertThat(span).isNotNull(); assertThat(span.getTraceId()).isNotNull(); assertThat(span.getSpanId()).isNotNull(); assertThat(span.getSpanName()).isNotNull(); assertThat(span.getParentSpanId()).isNull(); assertThat(span.isSampleable()).isTrue(); assertThat(span.isCompleted()).isTrue(); assertThat(strategyRequestTaggingMethodCalled.get()).isTrue(); strategyRequestTaggingArgs.get().verifyArgs(span, requestMock, filter.tagAndNamingAdapter); assertThat(strategyResponseTaggingAndFinalSpanNameMethodCalled.get()).isTrue(); strategyResponseTaggingArgs.get().verifyArgs( span, requestMock, responseMock, null, filter.tagAndNamingAdapter ); }
() -> filter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain) );
filter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain);
@Test public void doFilterInternal_should_use_user_id_from_parent_span_info_if_present_in_request_headers( ) throws ServletException, IOException { // given: filter and request that has parent span info RequestTracingFilter spyFilter = spy(getBasicFilter()); given(requestMock.getHeader(ALT_USER_ID_HEADER_KEY)).willReturn("testUserId"); Span parentSpan = Span.newBuilder("someParentSpan", null) .withParentSpanId(TraceAndSpanIdGenerator.generateId()) .withSampleable(false) .withUserId("someUser") .build(); given(requestMock.getHeader(TraceHeaders.TRACE_ID)).willReturn(parentSpan.getTraceId()); given(requestMock.getHeader(TraceHeaders.SPAN_ID)).willReturn(parentSpan.getSpanId()); given(requestMock.getHeader(TraceHeaders.PARENT_SPAN_ID)).willReturn(parentSpan.getParentSpanId()); given(requestMock.getHeader(TraceHeaders.SPAN_NAME)).willReturn(parentSpan.getSpanName()); given(requestMock.getHeader(TraceHeaders.TRACE_SAMPLED)).willReturn(String.valueOf(parentSpan.isSampleable())); given(requestMock.getServletPath()).willReturn("/some/path"); given(requestMock.getMethod()).willReturn("GET"); // when: doFilterInternal is called spyFilter.doFilterInternal(requestMock, responseMock, spanCapturingFilterChain); // then: the span that is created should use the parent span info as its parent assertThat(spanCapturingFilterChain.capturedSpan).isNotNull(); Span newSpan = spanCapturingFilterChain.capturedSpan; assertThat(newSpan.getUserId()).isEqualTo("testUserId"); }