From 39708a9ab10e8379af207666183bb82d757b6222 Mon Sep 17 00:00:00 2001 From: pengys5 <8082209@qq.com> Date: Mon, 15 May 2017 10:53:36 +0800 Subject: [PATCH] add okhttp plugin(provide synchronous http calls ) add test case for okhttp interceptor --- .../org/skywalking/apm/trace/tag/Tags.java | 4 + .../apm/agent/core/conf/Config.java | 8 + .../InstanceMethodsAroundInterceptor.java | 20 +-- apm-sniffer/apm-agent/pom.xml | 5 + .../http-plugin/okhttp-3.x-plugin/pom.xml | 34 +++++ .../plugin/okhttp/v3/RealCallInterceptor.java | 115 ++++++++++++++ .../v3/define/RealCallInstrumentation.java | 64 ++++++++ .../src/main/resources/skywalking-plugin.def | 1 + .../okhttp/v3/RealCallInterceptorTest.java | 143 ++++++++++++++++++ .../apm-sdk-plugin/http-plugin/pom.xml | 18 +++ apm-sniffer/apm-sdk-plugin/pom.xml | 1 + 11 files changed, 404 insertions(+), 9 deletions(-) create mode 100644 apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/pom.xml create mode 100644 apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/java/org/skywalking/apm/plugin/okhttp/v3/RealCallInterceptor.java create mode 100644 apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/java/org/skywalking/apm/plugin/okhttp/v3/define/RealCallInstrumentation.java create mode 100644 apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/resources/skywalking-plugin.def create mode 100644 apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/test/java/org/skywalking/apm/plugin/okhttp/v3/RealCallInterceptorTest.java create mode 100644 apm-sniffer/apm-sdk-plugin/http-plugin/pom.xml diff --git a/apm-commons/apm-trace/src/main/java/org/skywalking/apm/trace/tag/Tags.java b/apm-commons/apm-trace/src/main/java/org/skywalking/apm/trace/tag/Tags.java index 77c65bfaf..6e4d0247f 100644 --- a/apm-commons/apm-trace/src/main/java/org/skywalking/apm/trace/tag/Tags.java +++ b/apm-commons/apm-trace/src/main/java/org/skywalking/apm/trace/tag/Tags.java @@ -121,4 +121,8 @@ public final class Tags { * DB_STATEMENT records the sql statement of the database access. */ public static final StringTag DB_STATEMENT = new StringTag("db.statement"); + + public static final class HTTP { + public static final StringTag METHOD = new StringTag("http.method"); + } } diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/skywalking/apm/agent/core/conf/Config.java b/apm-sniffer/apm-agent-core/src/main/java/org/skywalking/apm/agent/core/conf/Config.java index 79f641370..c4ac43f4d 100644 --- a/apm-sniffer/apm-agent-core/src/main/java/org/skywalking/apm/agent/core/conf/Config.java +++ b/apm-sniffer/apm-agent-core/src/main/java/org/skywalking/apm/agent/core/conf/Config.java @@ -90,5 +90,13 @@ public class Config { */ public static boolean TRACE_PARAM = false; } + + public static class Http { + + /** + * The header name of context data. + */ + public static String HEADER_NAME_OF_CONTEXT_DATA = "SWTraceContext"; + } } } diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/skywalking/apm/agent/core/plugin/interceptor/enhance/InstanceMethodsAroundInterceptor.java b/apm-sniffer/apm-agent-core/src/main/java/org/skywalking/apm/agent/core/plugin/interceptor/enhance/InstanceMethodsAroundInterceptor.java index bf2f79895..5e8b445b5 100644 --- a/apm-sniffer/apm-agent-core/src/main/java/org/skywalking/apm/agent/core/plugin/interceptor/enhance/InstanceMethodsAroundInterceptor.java +++ b/apm-sniffer/apm-agent-core/src/main/java/org/skywalking/apm/agent/core/plugin/interceptor/enhance/InstanceMethodsAroundInterceptor.java @@ -12,31 +12,33 @@ public interface InstanceMethodsAroundInterceptor { /** * called before target method invocation. * - * @param context instance context, a class instance only has one {@link EnhancedClassInstanceContext} instance. + * @param context instance context, a class instance only has one {@link EnhancedClassInstanceContext} instance. * @param interceptorContext method context, includes class name, method name, etc. - * @param result change this result, if you want to truncate the method. + * @param result change this result, if you want to truncate the method. + * @throws Throwable */ void beforeMethod(EnhancedClassInstanceContext context, InstanceMethodInvokeContext interceptorContext, - MethodInterceptResult result); + MethodInterceptResult result) throws Throwable; /** * called after target method invocation. Even method's invocation triggers an exception. * - * @param context instance context, a class instance only has one {@link EnhancedClassInstanceContext} instance. + * @param context instance context, a class instance only has one {@link EnhancedClassInstanceContext} instance. * @param interceptorContext method context, includes class name, method name, etc. - * @param ret the method's original return value. + * @param ret the method's original return value. * @return the method's actual return value. + * @throws Throwable */ Object afterMethod(EnhancedClassInstanceContext context, InstanceMethodInvokeContext interceptorContext, - Object ret); + Object ret) throws Throwable; /** * called when occur exception. * - * @param t the exception occur. - * @param context instance context, a class instance only has one {@link EnhancedClassInstanceContext} instance. + * @param t the exception occur. + * @param context instance context, a class instance only has one {@link EnhancedClassInstanceContext} instance. * @param interceptorContext method context, includes class name, method name, etc. */ void handleMethodException(Throwable t, EnhancedClassInstanceContext context, - InstanceMethodInvokeContext interceptorContext); + InstanceMethodInvokeContext interceptorContext); } diff --git a/apm-sniffer/apm-agent/pom.xml b/apm-sniffer/apm-agent/pom.xml index bfc577a9e..351656ed7 100644 --- a/apm-sniffer/apm-agent/pom.xml +++ b/apm-sniffer/apm-agent/pom.xml @@ -60,6 +60,11 @@ apm-mongodb-3.x-plugin ${project.version} + + org.skywalking + apm-okhttp-3.x-plugin + ${project.version} + diff --git a/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/pom.xml new file mode 100644 index 000000000..bfe0848a7 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/pom.xml @@ -0,0 +1,34 @@ + + + + http-plugin + org.skywalking + 3.1-2017 + + 4.0.0 + + apm-okhttp-3.x-plugin + okhttp-3.x-plugin + jar + + + 3.7.0 + + + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + provided + + + com.squareup.okhttp3 + mockwebserver + 3.7.0 + test + + + \ No newline at end of file diff --git a/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/java/org/skywalking/apm/plugin/okhttp/v3/RealCallInterceptor.java b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/java/org/skywalking/apm/plugin/okhttp/v3/RealCallInterceptor.java new file mode 100644 index 000000000..f5c16f669 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/java/org/skywalking/apm/plugin/okhttp/v3/RealCallInterceptor.java @@ -0,0 +1,115 @@ +package org.skywalking.apm.plugin.okhttp.v3; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.skywalking.apm.agent.core.conf.Config; +import org.skywalking.apm.agent.core.context.ContextCarrier; +import org.skywalking.apm.agent.core.context.ContextManager; +import org.skywalking.apm.agent.core.plugin.interceptor.EnhancedClassInstanceContext; +import org.skywalking.apm.agent.core.plugin.interceptor.enhance.ConstructorInvokeContext; +import org.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor; +import org.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodInvokeContext; +import org.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor; +import org.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult; +import org.skywalking.apm.trace.Span; +import org.skywalking.apm.trace.tag.Tags; + +/** + * {@link RealCallInterceptor} intercept the synchronous http calls by the client of okhttp. + * + * @author pengys5 + */ +public class RealCallInterceptor implements InstanceMethodsAroundInterceptor, InstanceConstructorInterceptor { + + private static final String COMPONENT_NAME = "OKHttp"; + + private static final String REQUEST_CONTEXT_KEY = "SWRequestContextKey"; + + /** + * Intercept the {@link okhttp3.RealCall#RealCall(OkHttpClient, Request, boolean)}, then put the second argument of + * {@link okhttp3.Request} into {@link EnhancedClassInstanceContext} with the key of {@link + * RealCallInterceptor#REQUEST_CONTEXT_KEY}. + * + * @param context a new added instance field + * @param interceptorContext constructor invocation context. + */ + @Override + public void onConstruct(EnhancedClassInstanceContext context, ConstructorInvokeContext interceptorContext) { + context.set(REQUEST_CONTEXT_KEY, interceptorContext.allArguments()[1]); + } + + /** + * Get the {@link okhttp3.Request} from {@link EnhancedClassInstanceContext}, then create {@link Span} and set host, + * port, kind, component, url from {@link okhttp3.Request}. + * Through the reflection of the way, set the http header of context data into {@link okhttp3.Request#headers}. + * + * @param context instance context, a class instance only has one {@link EnhancedClassInstanceContext} instance. + * @param interceptorContext method context, includes class name, method name, etc. + * @param result change this result, if you want to truncate the method. + * @throws Throwable + */ + @Override + public void beforeMethod(EnhancedClassInstanceContext context, InstanceMethodInvokeContext interceptorContext, + MethodInterceptResult result) throws Throwable { + Request request = (Request)context.get(REQUEST_CONTEXT_KEY); + + Span span = ContextManager.createSpan(request.url().uri().toString()); + Tags.PEER_PORT.set(span, request.url().port()); + Tags.PEER_HOST.set(span, request.url().host()); + Tags.SPAN_KIND.set(span, Tags.SPAN_KIND_CLIENT); + Tags.COMPONENT.set(span, COMPONENT_NAME); + Tags.HTTP.METHOD.set(span, request.method()); + Tags.URL.set(span, request.url().url().getPath()); + Tags.SPAN_LAYER.asHttp(span); + + ContextCarrier contextCarrier = new ContextCarrier(); + ContextManager.inject(contextCarrier); + + Field headersField = Request.class.getDeclaredField("headers"); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(headersField, headersField.getModifiers() & ~Modifier.FINAL); + + headersField.setAccessible(true); + Headers headers = request.headers().newBuilder().add(Config.Plugin.Http.HEADER_NAME_OF_CONTEXT_DATA, contextCarrier.serialize()).build(); + headersField.set(request, headers); + } + + /** + * Get the status code from {@link Response}, when status code greater than 400, it means there was some errors in + * the server. + * Finish the {@link Span}. + * + * @param context instance context, a class instance only has one {@link EnhancedClassInstanceContext} instance. + * @param interceptorContext method context, includes class name, method name, etc. + * @param ret the method's original return value. + * @return + * @throws Throwable + */ + @Override + public Object afterMethod(EnhancedClassInstanceContext context, InstanceMethodInvokeContext interceptorContext, + Object ret) throws Throwable { + Response response = (Response)ret; + int statusCode = response.code(); + + Span span = ContextManager.activeSpan(); + if (statusCode >= 400) { + Tags.ERROR.set(span, true); + } + + Tags.STATUS_CODE.set(span, statusCode); + ContextManager.stopSpan(); + + return ret; + } + + @Override public void handleMethodException(Throwable t, EnhancedClassInstanceContext context, + InstanceMethodInvokeContext interceptorContext) { + Tags.ERROR.set(ContextManager.activeSpan(), true); + ContextManager.activeSpan().log(t); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/java/org/skywalking/apm/plugin/okhttp/v3/define/RealCallInstrumentation.java b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/java/org/skywalking/apm/plugin/okhttp/v3/define/RealCallInstrumentation.java new file mode 100644 index 000000000..59a526143 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/java/org/skywalking/apm/plugin/okhttp/v3/define/RealCallInstrumentation.java @@ -0,0 +1,64 @@ +package org.skywalking.apm.plugin.okhttp.v3.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint; +import org.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine; +import org.skywalking.apm.plugin.okhttp.v3.RealCallInterceptor; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +/** + * {@link RealCallInstrumentation} presents that skywalking intercepts {@link okhttp3.RealCall#RealCall(OkHttpClient, + * Request, boolean)}, {@link okhttp3.RealCall#execute()} by using {@link RealCallInterceptor}. + * + * @author pengys5 + */ +public class RealCallInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { + + /** + * Enhance class. + */ + private static final String ENHANCE_CLASS = "okhttp3.RealCall"; + + /** + * Intercept class. + */ + private static final String INTERCEPT_CLASS = "org.skywalking.apm.plugin.okhttp.v3.RealCallInterceptor"; + + @Override protected String enhanceClassName() { + return ENHANCE_CLASS; + } + + @Override protected ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[] { + new ConstructorInterceptPoint() { + @Override public ElementMatcher getConstructorMatcher() { + return takesArguments(OkHttpClient.class, Request.class, boolean.class); + } + + @Override public String getConstructorInterceptor() { + return INTERCEPT_CLASS; + } + } + }; + } + + @Override protected InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { + return new InstanceMethodsInterceptPoint[] { + new InstanceMethodsInterceptPoint() { + @Override public ElementMatcher getMethodsMatcher() { + return named("execute"); + } + + @Override public String getMethodsInterceptor() { + return INTERCEPT_CLASS; + } + } + }; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/resources/skywalking-plugin.def b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/resources/skywalking-plugin.def new file mode 100644 index 000000000..4f89b3cb3 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/resources/skywalking-plugin.def @@ -0,0 +1 @@ +org.skywalking.apm.plugin.okhttp.v3.define.RealCallInstrumentation \ No newline at end of file diff --git a/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/test/java/org/skywalking/apm/plugin/okhttp/v3/RealCallInterceptorTest.java b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/test/java/org/skywalking/apm/plugin/okhttp/v3/RealCallInterceptorTest.java new file mode 100644 index 000000000..46a3b774c --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/test/java/org/skywalking/apm/plugin/okhttp/v3/RealCallInterceptorTest.java @@ -0,0 +1,143 @@ +package org.skywalking.apm.plugin.okhttp.v3; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.skywalking.apm.agent.core.boot.ServiceManager; +import org.skywalking.apm.agent.core.context.TracerContext; +import org.skywalking.apm.agent.core.plugin.interceptor.EnhancedClassInstanceContext; +import org.skywalking.apm.agent.core.plugin.interceptor.enhance.ConstructorInvokeContext; +import org.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodInvokeContext; +import org.skywalking.apm.sniffer.mock.context.MockTracerContextListener; +import org.skywalking.apm.sniffer.mock.context.SegmentAssert; +import org.skywalking.apm.trace.Span; +import org.skywalking.apm.trace.TraceSegment; +import org.skywalking.apm.trace.tag.Tags; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author pengys5 + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({Response.class}) +public class RealCallInterceptorTest { + + private RealCallInterceptor realCallInterceptor; + private MockTracerContextListener mockTracerContextListener; + + private EnhancedClassInstanceContext classInstanceContext; + + @Mock + private ConstructorInvokeContext constructorInvokeContext; + + @Mock + private InstanceMethodInvokeContext instanceMethodInvokeContext; + + @Mock + private OkHttpClient client; + + private Request request; + + @Before + public void setUp() throws Exception { + mockTracerContextListener = new MockTracerContextListener(); + + classInstanceContext = new EnhancedClassInstanceContext(); + + request = new Request.Builder().url("http://skywalking.org").build(); + Object[] allArguments = {client, request, false}; + when(constructorInvokeContext.allArguments()).thenReturn(allArguments); + + ServiceManager.INSTANCE.boot(); + realCallInterceptor = new RealCallInterceptor(); + + TracerContext.ListenerManager.add(mockTracerContextListener); + } + + @Test + public void testOnConstruct() { + realCallInterceptor.onConstruct(classInstanceContext, constructorInvokeContext); + Assert.assertEquals(request, classInstanceContext.get("SWRequestContextKey")); + } + + @Test + public void testMethodsAround() throws Throwable { + realCallInterceptor.onConstruct(classInstanceContext, constructorInvokeContext); + realCallInterceptor.beforeMethod(classInstanceContext, instanceMethodInvokeContext, null); + + Response response = mock(Response.class); + when(response.code()).thenReturn(200); + realCallInterceptor.afterMethod(classInstanceContext, instanceMethodInvokeContext, response); + + mockTracerContextListener.assertSize(1); + mockTracerContextListener.assertTraceSegment(0, new SegmentAssert() { + @Override public void call(TraceSegment finishedSegment) { + Assert.assertEquals(1, finishedSegment.getSpans().size()); + assertSpan(finishedSegment.getSpans().get(0)); + Assert.assertEquals(false, Tags.ERROR.get(finishedSegment.getSpans().get(0))); + } + }); + } + + @Test + public void testMethodsAroundError() throws Throwable { + realCallInterceptor.onConstruct(classInstanceContext, constructorInvokeContext); + realCallInterceptor.beforeMethod(classInstanceContext, instanceMethodInvokeContext, null); + + Response response = mock(Response.class); + when(response.code()).thenReturn(404); + realCallInterceptor.afterMethod(classInstanceContext, instanceMethodInvokeContext, response); + + mockTracerContextListener.assertSize(1); + mockTracerContextListener.assertTraceSegment(0, new SegmentAssert() { + @Override public void call(TraceSegment finishedSegment) { + Assert.assertEquals(1, finishedSegment.getSpans().size()); + assertSpan(finishedSegment.getSpans().get(0)); + Assert.assertEquals(true, Tags.ERROR.get(finishedSegment.getSpans().get(0))); + } + }); + } + + private void assertSpan(Span span) { + Assert.assertEquals("http", Tags.SPAN_LAYER.get(span)); + Assert.assertEquals("GET", Tags.HTTP.METHOD.get(span)); + Assert.assertEquals("skywalking.org", Tags.PEER_HOST.get(span)); + Assert.assertEquals(80, Tags.PEER_PORT.get(span).intValue()); + Assert.assertEquals("OKHttp", Tags.COMPONENT.get(span)); + Assert.assertEquals("client", Tags.SPAN_KIND.get(span)); + Assert.assertEquals("/", Tags.URL.get(span)); + } + + @Test + public void testException() throws Throwable { + realCallInterceptor.onConstruct(classInstanceContext, constructorInvokeContext); + realCallInterceptor.beforeMethod(classInstanceContext, instanceMethodInvokeContext, null); + + realCallInterceptor.handleMethodException(new NullPointerException("testException"), classInstanceContext, null); + + Response response = mock(Response.class); + when(response.code()).thenReturn(200); + realCallInterceptor.afterMethod(classInstanceContext, instanceMethodInvokeContext, response); + + mockTracerContextListener.assertSize(1); + mockTracerContextListener.assertTraceSegment(0, new SegmentAssert() { + @Override public void call(TraceSegment finishedSegment) { + Assert.assertEquals(1, finishedSegment.getSpans().size()); + assertSpan(finishedSegment.getSpans().get(0)); + Assert.assertEquals(true, Tags.ERROR.get(finishedSegment.getSpans().get(0))); + + Assert.assertEquals(1, finishedSegment.getSpans().get(0).getLogs().size()); + Assert.assertEquals(true, finishedSegment.getSpans().get(0).getLogs().get(0).getFields().containsKey("stack")); + } + }); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/http-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/http-plugin/pom.xml new file mode 100644 index 000000000..38a019e78 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/http-plugin/pom.xml @@ -0,0 +1,18 @@ + + + + apm-sdk-plugin + org.skywalking + 3.1-2017 + + 4.0.0 + + http-plugin + pom + + okhttp-3.x-plugin + + + \ No newline at end of file diff --git a/apm-sniffer/apm-sdk-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/pom.xml index accc48abb..3b4d1f610 100644 --- a/apm-sniffer/apm-sdk-plugin/pom.xml +++ b/apm-sniffer/apm-sdk-plugin/pom.xml @@ -18,6 +18,7 @@ tomcat-7.x-8.x-plugin motan-plugin mongodb-3.x-plugin + http-plugin pom -- GitLab