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 77c65bfafe950e235c162c717e508e312fc32d43..6e4d0247f0d62fcfae72cf78511ff4af625c04e0 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 898c16327950d03a6d56458c2f343dbb73b21bff..a8789cda389c3eed568b3a87a3e11d896ebcabfe 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 @@ -103,5 +103,12 @@ 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 bf2f7989546e7803647cf2597a8ceb70cff43ff2..5e8b445b53a546809e6de4fc78204fb3e3a9f791 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 b33a84f8b92d001100860319e4d1a6e81b8c5410..4299ac757d87b0de2d41c49ecac43d23a13f242a 100644 --- a/apm-sniffer/apm-agent/pom.xml +++ b/apm-sniffer/apm-agent/pom.xml @@ -70,6 +70,11 @@ apm-resin-4.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 0000000000000000000000000000000000000000..bfe0848a7b87fd7f2c19c2b443b438c00346b478 --- /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 0000000000000000000000000000000000000000..f5c16f6691178aacb3dfc5922d7a04e9fb764f22 --- /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 0000000000000000000000000000000000000000..59a526143760423847a2980bc43e5f199e605d15 --- /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 0000000000000000000000000000000000000000..8ee8f54ce5c5db095ed6e8cb83e2ed6da9bcb171 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/http-plugin/okhttp-3.x-plugin/src/main/resources/skywalking-plugin.def @@ -0,0 +1 @@ +okhttp-3.x=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 0000000000000000000000000000000000000000..46a3b774c8871217a74ab579c5d72db557798309 --- /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 0000000000000000000000000000000000000000..38a019e78c50e7ae08b00697961fac7bd834b32c --- /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 75d35f783c861fe39b80703e36305ccef69cffbb..2795f85f0f3d8bc3f581d9ca4473b42746ea4024 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 resin-3.x-plugin resin-4.x-plugin