From a16fbab0a1b738b64b4050c0902c31d88a1faec1 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Tue, 4 Aug 2015 14:37:46 -0600 Subject: [PATCH] Initial implementation of http annotations Part of gh-11 --- .../sleuth/instrument/web/TraceFilter.java | 57 +++++++- .../sleuth/instrument/web/TraceWebAspect.java | 2 +- .../instrument/web/TraceFilterTests.java | 126 ++++++++++++++++++ 3 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterTests.java diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceFilter.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceFilter.java index c9549821d..920ff58a6 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceFilter.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceFilter.java @@ -23,6 +23,7 @@ import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME; import static org.springframework.util.StringUtils.hasText; import java.io.IOException; +import java.util.Enumeration; import java.util.regex.Pattern; import javax.servlet.FilterChain; @@ -50,11 +51,12 @@ import org.springframework.web.util.UrlPathHelper; * @author Tomasz Nurkiewicz, 4financeIT * @author Marcin Grzejszczak, 4financeIT * @author Spencer Gibb + * @author Dave Syer */ @Order(Ordered.HIGHEST_PRECEDENCE + 5) public class TraceFilter extends OncePerRequestFilter { - private static final String TRACE_REQUEST_ATTR = TraceFilter.class.getName() + protected static final String TRACE_REQUEST_ATTR = TraceFilter.class.getName() + ".TRACE"; public static final Pattern DEFAULT_SKIP_PATTERN = Pattern @@ -92,26 +94,28 @@ public class TraceFilter extends OncePerRequestFilter { String name = "http" + uri; if (hasText(spanId) && hasText(traceId)) { - MilliSpanBuilder traceInfo = MilliSpan.builder().traceId(traceId) + MilliSpanBuilder span = MilliSpan.builder().traceId(traceId) .spanId(spanId); String parentId = getHeader(request, response, PARENT_ID_NAME); String processId = getHeader(request, response, PROCESS_ID_NAME); String parentName = getHeader(request, response, SPAN_NAME_NAME); if (parentName != null) { - traceInfo.name(parentName); + span.name(parentName); } if (processId != null) { - traceInfo.processId(processId); + span.processId(processId); } if (parentId != null) { - traceInfo.parent(parentId); + span.parent(parentId); } - traceInfo.remote(true); + span.remote(true); // TODO: trace description? - traceScope = this.trace.startSpan(name, traceInfo.build()); + traceScope = this.trace.startSpan(name, span.build()); request.setAttribute(TRACE_REQUEST_ATTR, traceScope); // Send new span id back + addToResponseIfNotPresent(response, TRACE_ID_NAME, traceScope.getSpan() + .getTraceId()); addToResponseIfNotPresent(response, SPAN_ID_NAME, traceScope.getSpan() .getSpanId()); } @@ -121,18 +125,57 @@ public class TraceFilter extends OncePerRequestFilter { } try { + + addRequestAnnotations(request); + filterChain.doFilter(request, response); } finally { if (request.isAsyncSupported() && request.isAsyncStarted()) { + //TODO: howto deal with response annotations and async? return; } if (traceScope != null) { + addResponseAnnotations(response); + traceScope.close(); } } } + protected void addRequestAnnotations(HttpServletRequest request) { + String uri = this.urlPathHelper.getPathWithinApplication(request); + this.trace.addKVAnnotation("/http/request/uri", + request.getRequestURL().toString()); + this.trace.addKVAnnotation("/http/request/endpoint", uri); + this.trace.addKVAnnotation("/http/request/method", + request.getMethod()); + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String name = headerNames.nextElement(); + Enumeration values = request.getHeaders(name); + while (values.hasMoreElements()) { + String value = values.nextElement(); + String key = "/http/request/headers/"+name.toLowerCase(); + this.trace.addKVAnnotation(key, value); + + } + } + } + + private void addResponseAnnotations(HttpServletResponse response) { + this.trace.addKVAnnotation("/http/response/status_code", + String.valueOf(response.getStatus())); + + for (String name : response.getHeaderNames()) { + for (String value : response.getHeaders(name)) { + String key = "/http/response/headers/"+name.toLowerCase(); + this.trace.addKVAnnotation(key, value); + } + } + } + private String getHeader(HttpServletRequest request, HttpServletResponse response, String name) { String value = request.getHeader(name); diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebAspect.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebAspect.java index 7a06bc4e3..f1c339380 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebAspect.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebAspect.java @@ -72,7 +72,7 @@ public class TraceWebAspect { log.debug("Wrapping callable with span [" + TraceContextHolder.getCurrentSpan() + "]"); - return new TraceCallable(this.trace, callable); + return new TraceCallable<>(this.trace, callable); } else { return callable; diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterTests.java new file mode 100644 index 000000000..f32fcb233 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterTests.java @@ -0,0 +1,126 @@ +package org.springframework.cloud.sleuth.instrument.web; + +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.springframework.cloud.sleuth.Trace.SPAN_ID_NAME; +import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +import lombok.SneakyThrows; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Trace; +import org.springframework.cloud.sleuth.TraceScope; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +/** + * @author Spencer Gibb + */ +public class TraceFilterTests { + + @Mock + private Trace trace; + + @Mock + private TraceScope traceScope; + + @Mock + private Span span; + + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private MockFilterChain filterChain; + + @Before + @SneakyThrows + public void init() { + initMocks(this); + request = builder() + .buildRequest(new MockServletContext()); + response = new MockHttpServletResponse(); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + filterChain = new MockFilterChain(); + } + + public MockHttpServletRequestBuilder builder() { + return get("/") + .accept(MediaType.APPLICATION_JSON) + .header("User-Agent", "MockMvc"); + } + + @Test + public void startsNewTrace() throws Exception { + TraceFilter filter = new TraceFilter(trace); + + when(this.trace.startSpan(anyString())).thenReturn(traceScope); + + filter.doFilter(request, response, filterChain); + + verify(this.trace).startSpan(anyString()); + + verifyHttpAnnotations(); + + verify(this.traceScope).close(); + } + + @Test + public void continuesSpanInRequestAttr() throws Exception { + request.setAttribute(TraceFilter.TRACE_REQUEST_ATTR, this.traceScope); + + TraceFilter filter = new TraceFilter(trace); + filter.doFilter(request, response, filterChain); + + verify(this.trace).continueSpan((Span) anyObject()); + + verifyHttpAnnotations(); + + verify(this.traceScope).close(); + } + + + @Test + public void continuesSpanFromHeaders() throws Exception { + request = builder() + .header(SPAN_ID_NAME, "myspan") + .header(TRACE_ID_NAME, "mytrace") + .buildRequest(new MockServletContext()); + + when(this.trace.startSpan(anyString(), (Span) anyObject())).thenReturn(traceScope); + when(this.traceScope.getSpan()).thenReturn(this.span); + when(this.span.getSpanId()).thenReturn("myspan"); + when(this.span.getTraceId()).thenReturn("mytrace"); + + TraceFilter filter = new TraceFilter(trace); + filter.doFilter(request, response, filterChain); + + verify(this.trace).startSpan(anyString(), (Span) anyObject()); + + verifyHttpAnnotations(); + + verify(this.traceScope).close(); + } + + + public void verifyHttpAnnotations() { + verify(this.trace).addKVAnnotation("/http/request/uri", "http://localhost/"); + verify(this.trace).addKVAnnotation("/http/request/endpoint", "/"); + verify(this.trace).addKVAnnotation("/http/request/method", "GET"); + verify(this.trace).addKVAnnotation("/http/request/headers/accept", MediaType.APPLICATION_JSON_VALUE); + verify(this.trace).addKVAnnotation("/http/request/headers/user-agent", "MockMvc"); + + verify(this.trace).addKVAnnotation("/http/response/status_code", HttpStatus.OK.toString()); + verify(this.trace).addKVAnnotation("/http/response/headers/content-type", MediaType.APPLICATION_JSON_VALUE); + } +}