diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/tag/Tags.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/tag/Tags.java index caa05bf25..11cc947c1 100644 --- a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/tag/Tags.java +++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/tag/Tags.java @@ -83,6 +83,8 @@ public final class Tags { public static final StringTag PARAMS = new StringTag(11, "http.params", true); public static final StringTag BODY = new StringTag(13, "http.body"); + + public static final StringTag HEADERS = new StringTag(14, "http.headers"); } public static final StringTag LOGIC_ENDPOINT = new StringTag(12, "x-le"); diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/mvc/v4/RestMappingMethodInterceptorTest.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/mvc/v4/RestMappingMethodInterceptorTest.java index e5eebb200..86a1858d5 100644 --- a/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/mvc/v4/RestMappingMethodInterceptorTest.java +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/mvc/v4/RestMappingMethodInterceptorTest.java @@ -19,7 +19,9 @@ package org.apache.skywalking.apm.plugin.spring.mvc.v4; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.List; +import java.util.Vector; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.skywalking.apm.agent.core.context.trace.AbstractTracingSpan; @@ -40,8 +42,10 @@ import org.apache.skywalking.apm.agent.test.tools.TracingSegmentRunner; import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; import org.apache.skywalking.apm.plugin.spring.mvc.commons.EnhanceRequireObjectCache; import org.apache.skywalking.apm.plugin.spring.mvc.commons.PathMappingCache; +import org.apache.skywalking.apm.plugin.spring.mvc.commons.SpringMVCPluginConfig; import org.apache.skywalking.apm.plugin.spring.mvc.commons.interceptor.RestMappingMethodInterceptor; import org.hamcrest.MatcherAssert; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -113,6 +117,12 @@ public class RestMappingMethodInterceptorTest { response.getClass() }; + SpringMVCPluginConfig.Plugin.Http.INCLUDE_HTTP_HEADERS = Arrays.asList("connection"); + } + + @After + public void cleanup() { + SpringMVCPluginConfig.Plugin.Http.INCLUDE_HTTP_HEADERS = null; } @Test @@ -300,6 +310,35 @@ public class RestMappingMethodInterceptorTest { SpanAssert.assertException(logDataEntities.get(0), RuntimeException.class); } + @Test + public void testGetWithRequestHeaderCollected() throws Throwable { + SpringTestCaseHelper.createCaseHandler(request, response, new SpringTestCaseHelper.CaseHandler() { + @Override + public void handleCase() throws Throwable { + controllerConstructorInterceptor.onConstruct(enhancedInstance, null); + RestMappingClass1 mappingClass1 = new RestMappingClass1(); + Method m = mappingClass1.getClass().getMethod("getRequestURL"); + when(request.getRequestURI()).thenReturn("/test/testRequestURL"); + when(request.getRequestURL()).thenReturn(new StringBuffer("http://localhost:8080/test/getRequestURL")); + when(request.getHeaderNames()).thenReturn(new Vector(Arrays.asList("Connection", "Cookie")).elements()); + when(request.getHeaders("connection")).thenReturn(new Vector(Arrays.asList("keep-alive")).elements()); + when(request.getHeaders("cookie")).thenReturn(new Vector(Arrays.asList("dummy cookies")).elements()); + ServletRequestAttributes servletRequestAttributes = new ServletRequestAttributes(request, response); + RequestContextHolder.setRequestAttributes(servletRequestAttributes); + + interceptor.beforeMethod(enhancedInstance, m, arguments, argumentType, methodInterceptResult); + interceptor.afterMethod(enhancedInstance, m, arguments, argumentType, null); + } + }); + + assertThat(segmentStorage.getTraceSegments().size(), is(1)); + TraceSegment traceSegment = segmentStorage.getTraceSegments().get(0); + List spans = SegmentHelper.getSpans(traceSegment); + + assertHttpSpan(spans.get(0), "/getRequestURL"); + SpanAssert.assertTag(spans.get(0), 2, "connection=[keep-alive]"); + } + private void assertTraceSegmentRef(TraceSegmentRef ref) { MatcherAssert.assertThat(SegmentRefHelper.getParentServiceInstance(ref), is("instance")); assertThat(SegmentRefHelper.getSpanId(ref), is(3)); diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/SpringMVCPluginConfig.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/SpringMVCPluginConfig.java index 4a3386dbc..5699aac51 100644 --- a/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/SpringMVCPluginConfig.java +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/SpringMVCPluginConfig.java @@ -20,6 +20,8 @@ package org.apache.skywalking.apm.plugin.spring.mvc.commons; import org.apache.skywalking.apm.agent.core.boot.PluginConfig; +import java.util.List; + public class SpringMVCPluginConfig { public static class Plugin { @PluginConfig(root = SpringMVCPluginConfig.class) @@ -45,6 +47,18 @@ public class SpringMVCPluginConfig { * added for the sake of performance */ public static int HTTP_PARAMS_LENGTH_THRESHOLD = 1024; + + /** + * When {@link Plugin.Http#INCLUDE_HTTP_HEADERS} declares header names, this threshold controls the length + * limitation of all header values. use negative values to keep and send the complete headers. + * Note. this config item is added for the sake of performance. + */ + public static int HTTP_HEADERS_LENGTH_THRESHOLD = 2048; + + /** + * It controls what header data should be collected, this is for security purpose, values must be in lower case + */ + public static List INCLUDE_HTTP_HEADERS ; } } } diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/interceptor/AbstractMethodInterceptor.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/interceptor/AbstractMethodInterceptor.java index 911ec74c3..d7817d67d 100644 --- a/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/interceptor/AbstractMethodInterceptor.java +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/mvc-annotation-commons/src/main/java/org/apache/skywalking/apm/plugin/spring/mvc/commons/interceptor/AbstractMethodInterceptor.java @@ -19,9 +19,15 @@ package org.apache.skywalking.apm.plugin.spring.mvc.commons.interceptor; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedList; import java.util.Map; +import java.util.List; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + import org.apache.skywalking.apm.agent.core.context.CarrierItem; import org.apache.skywalking.apm.agent.core.context.ContextCarrier; import org.apache.skywalking.apm.agent.core.context.ContextManager; @@ -113,6 +119,10 @@ public abstract class AbstractMethodInterceptor implements InstanceMethodsAround collectHttpParam(request, span); } + if (!CollectionUtil.isEmpty(SpringMVCPluginConfig.Plugin.Http.INCLUDE_HTTP_HEADERS)) { + collectHttpHeaders(request, span); + } + stackDepth = new StackDepth(); ContextManager.getRuntimeContext().put(CONTROLLER_METHOD_STACK_DEPTH, stackDepth); } else { @@ -208,4 +218,23 @@ public abstract class AbstractMethodInterceptor implements InstanceMethodsAround Tags.HTTP.PARAMS.set(span, tagValue); } } + + private void collectHttpHeaders(HttpServletRequest request, AbstractSpan span) { + final List headersList = new LinkedList<>(); + SpringMVCPluginConfig.Plugin.Http.INCLUDE_HTTP_HEADERS.stream().filter(headerName -> request.getHeaders(headerName) != null).forEach(headerName -> { + Enumeration headerValues = request.getHeaders(headerName); + List valueList = Collections.list(headerValues); + if (!CollectionUtil.isEmpty(valueList)) { + String headerValue = valueList.toString(); + headersList.add(headerName + "=" + headerValue); + } + }); + + if (!headersList.isEmpty()) { + String tagValue = headersList.stream().collect(Collectors.joining("\n")); + tagValue = SpringMVCPluginConfig.Plugin.Http.HTTP_HEADERS_LENGTH_THRESHOLD > 0 ? + StringUtil.cut(tagValue, SpringMVCPluginConfig.Plugin.Http.HTTP_HEADERS_LENGTH_THRESHOLD) : tagValue; + Tags.HTTP.HEADERS.set(span, tagValue); + } + } } diff --git a/docs/en/setup/service-agent/java-agent/README.md b/docs/en/setup/service-agent/java-agent/README.md index f12bd2806..cbdf94e90 100755 --- a/docs/en/setup/service-agent/java-agent/README.md +++ b/docs/en/setup/service-agent/java-agent/README.md @@ -132,6 +132,8 @@ property key | Description | Default | `plugin.tomcat.collect_http_params`| This config item controls that whether the Tomcat plugin should collect the parameters of the request. Also, activate implicitly in the profiled trace. | `false` | `plugin.springmvc.collect_http_params`| This config item controls that whether the SpringMVC plugin should collect the parameters of the request, when your Spring application is based on Tomcat, consider only setting either `plugin.tomcat.collect_http_params` or `plugin.springmvc.collect_http_params`. Also, activate implicitly in the profiled trace. | `false` | `plugin.http.http_params_length_threshold`| When `COLLECT_HTTP_PARAMS` is enabled, how many characters to keep and send to the OAP backend, use negative values to keep and send the complete parameters, NB. this config item is added for the sake of performance. | `1024` | +`plugin.http.http_headers_length_threshold`| When `include_http_headers` declares header names, this threshold controls the length limitation of all header values. use negative values to keep and send the complete headers. Note. this config item is added for the sake of performance. | `2048` | +`plugin.http.include_http_headers`| Set the header names, which should be collected by the plugin. Header name must follow `javax.servlet.http` definition. Multiple names should be split by comma. | ``(No header would be collected) | `plugin.feign.collect_request_body`| This config item controls that whether the Feign plugin should collect the http body of the request. | `false` | `plugin.feign.filter_length_limit`| When `COLLECT_REQUEST_BODY` is enabled, how many characters to keep and send to the OAP backend, use negative values to keep and send the complete body. | `1024` | `plugin.feign.supported_content_types_prefix`| When `COLLECT_REQUEST_BODY` is enabled and content-type start with SUPPORTED_CONTENT_TYPES_PREFIX, collect the body of the request , multiple paths should be separated by `,` | `application/json,text/` | diff --git a/test/plugin/scenarios/spring-4.3.x-scenario/config/expectedData.yaml b/test/plugin/scenarios/spring-4.3.x-scenario/config/expectedData.yaml index 7961509d1..4bc9bfc60 100644 --- a/test/plugin/scenarios/spring-4.3.x-scenario/config/expectedData.yaml +++ b/test/plugin/scenarios/spring-4.3.x-scenario/config/expectedData.yaml @@ -62,6 +62,7 @@ segmentItems: tags: - {key: url, value: 'http://localhost:8080/spring-4.3.x-scenario/case/spring3/'} - {key: http.method, value: GET} + - {key: http.headers, value: 'mock_header=[mock_value]'} refs: - {parentEndpoint: /case/resttemplate, networkAddress: 'localhost:8080', refType: CrossProcess, parentSpanId: 1, parentTraceSegmentId: not null, parentServiceInstance: not diff --git a/test/plugin/scenarios/spring-4.3.x-scenario/configuration.yml b/test/plugin/scenarios/spring-4.3.x-scenario/configuration.yml index 2c9e72038..458fe8c45 100644 --- a/test/plugin/scenarios/spring-4.3.x-scenario/configuration.yml +++ b/test/plugin/scenarios/spring-4.3.x-scenario/configuration.yml @@ -19,3 +19,5 @@ entryService: http://localhost:8080/spring-4.3.x-scenario/case/resttemplate healthCheck: http://localhost:8080/spring-4.3.x-scenario/healthCheck runningMode: with_optional withPlugins: apm-spring-annotation-plugin-*.jar +environment: + - CATALINA_OPTS="-Dskywalking.plugin.http.include_http_headers=mock_header" diff --git a/test/plugin/scenarios/spring-4.3.x-scenario/src/main/java/test/org/apache/skywalking/apm/testcase/resttemplate/RestTemplateController.java b/test/plugin/scenarios/spring-4.3.x-scenario/src/main/java/test/org/apache/skywalking/apm/testcase/resttemplate/RestTemplateController.java index 3f5ca0038..0d4a851df 100644 --- a/test/plugin/scenarios/spring-4.3.x-scenario/src/main/java/test/org/apache/skywalking/apm/testcase/resttemplate/RestTemplateController.java +++ b/test/plugin/scenarios/spring-4.3.x-scenario/src/main/java/test/org/apache/skywalking/apm/testcase/resttemplate/RestTemplateController.java @@ -43,7 +43,7 @@ public class RestTemplateController { @RequestMapping("/case/resttemplate") @ResponseBody public String restTemplate() throws IOException { - Request request = new Request.Builder().url(url + "/case/spring3/").build(); + Request request = new Request.Builder().header("mock_header", "mock_value").url(url + "/case/spring3/").build(); Response response = new OkHttpClient().newCall(request).execute(); logger.info(response.toString());