diff --git a/CHANGES.md b/CHANGES.md index 8c7c7d5ed..042d97069 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -150,6 +150,7 @@ Callable { * Fix witness class in springmvc-annotation-5.x-plugin to avoid falling into v3 use cases. * Fix Jedis-2.x plugin bug and add test for Redis cluster scene * Merge two instrumentation classes to avoid duplicate enhancements in MySQL plugins. +* Support asynchronous invocation in jetty client 9.0 and 9.x plugin #### Documentation diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/AsyncHttpRequestSendInterceptor.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/AsyncHttpRequestSendInterceptor.java new file mode 100644 index 000000000..669b43ffd --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/AsyncHttpRequestSendInterceptor.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v90.client; + +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; +import org.apache.skywalking.apm.agent.core.context.tag.Tags; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; + +import java.lang.reflect.Method; + +public class AsyncHttpRequestSendInterceptor implements InstanceMethodsAroundInterceptor { + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInterceptResult result) throws Throwable { + HttpRequest request = (HttpRequest) objInst; + ContextCarrier contextCarrier = new ContextCarrier(); + AbstractSpan span = ContextManager.createExitSpan(request.getURI().getPath(), contextCarrier, + request.getHost() + ":" + request.getPort()); + span.setComponent(ComponentsDefine.JETTY_CLIENT); + + Tags.HTTP.METHOD.set(span, getHttpMethod(request)); + Tags.URL.set(span, request.getURI().toString()); + SpanLayer.asHttp(span); + + CarrierItem next = contextCarrier.items(); + HttpFields field = request.getHeaders(); + while (next.hasNext()) { + next = next.next(); + field.add(next.getHeadKey(), next.getHeadValue()); + } + + span.prepareForAsync(); + request.attribute(Constants.SW_JETTY_EXIT_SPAN_KEY, span); + Response.CompleteListener callback = (Response.CompleteListener) allArguments[0]; + allArguments[0] = new CompleteListenerWrapper(callback, ContextManager.capture()); + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret) throws Throwable { + ContextManager.stopSpan(); + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t) { + ContextManager.activeSpan().log(t); + } + + private String getHttpMethod(HttpRequest request) { + HttpMethod httpMethod = HttpMethod.GET; + + /** + * The method is null if the client using GET method. + * + * @see org.eclipse.jetty.client.HttpRequest#GET(String uri) + * @see org.eclipse.jetty.client.HttpRequest( org.eclipse.jetty.client.HttpClient client, long conversation, java.net.URI uri) + */ + if (request.getMethod() != null) { + httpMethod = request.getMethod(); + } + + return httpMethod.name(); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/CompleteListenerWrapper.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/CompleteListenerWrapper.java new file mode 100644 index 000000000..0318bd4c0 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/CompleteListenerWrapper.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v90.client; + +import org.apache.skywalking.apm.agent.core.context.ContextManager; +import org.apache.skywalking.apm.agent.core.context.ContextSnapshot; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; + +public class CompleteListenerWrapper implements Response.CompleteListener { + private Response.CompleteListener callback; + private ContextSnapshot context; + + public CompleteListenerWrapper(Response.CompleteListener callback, ContextSnapshot context) { + this.callback = callback; + this.context = context; + } + + @Override + public void onComplete(Result result) { + AbstractSpan span = ContextManager.createLocalSpan(Constants.PLUGIN_NAME + "/CompleteListener/onComplete"); + span.setComponent(ComponentsDefine.JETTY_CLIENT); + SpanLayer.asHttp(span); + if (context != null) { + ContextManager.continued(context); + } + if (callback != null) { + callback.onComplete(result); + } + ContextManager.stopSpan(); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/Constants.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/Constants.java new file mode 100644 index 000000000..eec22341d --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/Constants.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v90.client; + +public class Constants { + public final static String SW_JETTY_EXIT_SPAN_KEY = "SW_JETTY_EXIT_SPAN"; + + public final static String PLUGIN_NAME = "JettyClient9.0"; +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/ResponseNotifierInterceptor.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/ResponseNotifierInterceptor.java new file mode 100644 index 000000000..b2d930bf1 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/ResponseNotifierInterceptor.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v90.client; + +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Result; + +import java.lang.reflect.Method; +import java.util.Optional; + +public class ResponseNotifierInterceptor implements InstanceMethodsAroundInterceptor { + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInterceptResult result) throws Throwable { + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret) throws Throwable { + // get async span and stop it. + Optional.ofNullable(getAsyncSpan(allArguments)).ifPresent(v -> v.asyncFinish()); + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t) { + Optional.ofNullable(getAsyncSpan(allArguments)).ifPresent(v -> v.log(t)); + } + + private AbstractSpan getAsyncSpan(Object[] allArguments) { + Result results = (Result) allArguments[1]; + if (results == null) { + return null; + } + Request request = results.getRequest(); + if (request == null) { + return null; + } + + return (AbstractSpan) request.getAttributes().get(Constants.SW_JETTY_EXIT_SPAN_KEY); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/define/HttpRequestInstrumentation.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/define/HttpRequestInstrumentation.java index cadaf0b07..ef8daf105 100644 --- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/define/HttpRequestInstrumentation.java +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/define/HttpRequestInstrumentation.java @@ -31,15 +31,20 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; /** * {@link HttpRequestInstrumentation} enhance the send method without argument in - * org.eclipse.jetty.client.HttpRequest by org.apache.skywalking.apm.plugin.jetty.client.SyncHttpRequestSendInterceptor - * and enhance the send with org.eclipse.jetty.client.api.Response$CompleteListener parameter - * by org.apache.skywalking.apm.plugin.jetty.client.AsyncHttpRequestSendInterceptor + * org.eclipse.jetty.client.HttpRequest by + * org.apache.skywalking.apm.plugin.jetty.client.SyncHttpRequestSendInterceptor and enhance the + * send with org.eclipse.jetty.client.api.Response$CompleteListener parameter by + * org.apache.skywalking.apm.plugin.jetty.client.AsyncHttpRequestSendInterceptor */ public class HttpRequestInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { private static final String ENHANCE_CLASS = "org.eclipse.jetty.client.HttpRequest"; private static final String ENHANCE_CLASS_NAME = "send"; - public static final String SYNC_SEND_INTERCEPTOR = "org.apache.skywalking.apm.plugin.jetty.v90.client.SyncHttpRequestSendV90Interceptor"; + public static final String SYNC_SEND_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.jetty.v90.client.SyncHttpRequestSendV90Interceptor"; + + public static final String ASYNC_SEND_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.jetty.v90.client.AsyncHttpRequestSendInterceptor"; @Override public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { @@ -61,6 +66,23 @@ public class HttpRequestInstrumentation extends ClassInstanceMethodsEnhancePlugi return SYNC_SEND_INTERCEPTOR; } + @Override + public boolean isOverrideArgs() { + return false; + } + }, + new InstanceMethodsInterceptPoint() { + // async call interceptor point + @Override + public ElementMatcher getMethodsMatcher() { + return named(ENHANCE_CLASS_NAME).and(takesArguments(1)); + } + + @Override + public String getMethodsInterceptor() { + return ASYNC_SEND_INTERCEPTOR; + } + @Override public boolean isOverrideArgs() { return false; diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/define/ResponseNotifierInstrumentation.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/define/ResponseNotifierInstrumentation.java new file mode 100644 index 000000000..c4f39c414 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v90/client/define/ResponseNotifierInstrumentation.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v90.client.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; +import org.apache.skywalking.apm.agent.core.plugin.match.NameMatch; + +import java.util.List; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +/** + * {@link ResponseNotifierInstrumentation} enhance the send method in + * org.eclipse.jetty.client.ResponseNotifier by + * org.apache.skywalking.apm.plugin.jetty.v9.client.ResponseNotifierInterceptor + *

+ * It is implemented to be able to stop the asynchronous span in the attributes of the request. + */ +public class ResponseNotifierInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { + + private static final String ENHANCE_CLASS = "org.eclipse.jetty.client.tar"; + private static final String ENHANCE_CLASS_NAME = "notifyComplete"; + public static final String SYNC_SEND_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.jetty.v9.client.ResponseNotifierInterceptor"; + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[0]; + } + + @Override + public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { + return new InstanceMethodsInterceptPoint[]{ + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher getMethodsMatcher() { + return named(ENHANCE_CLASS_NAME).and(takesArguments(2)).and(takesArgument(0, List.class)); + } + + @Override + public String getMethodsInterceptor() { + return SYNC_SEND_INTERCEPTOR; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } + + @Override + protected ClassMatch enhanceClass() { + return NameMatch.byName(ENHANCE_CLASS); + } + + @Override + protected String[] witnessClasses() { + return new String[]{"org.eclipse.jetty.client.api.ProxyConfiguration"}; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/resources/skywalking-plugin.def b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/resources/skywalking-plugin.def index b8d07b97d..c78967226 100644 --- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/resources/skywalking-plugin.def +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/main/resources/skywalking-plugin.def @@ -15,3 +15,4 @@ # limitations under the License. jetty-client-9.0=org.apache.skywalking.apm.plugin.jetty.v90.client.define.HttpRequestInstrumentation +jetty-client-9.0=org.apache.skywalking.apm.plugin.jetty.v90.client.define.ResponseNotifierInstrumentation diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/test/java/org/apache/skywalking/apm/plugin/jetty/v90/client/AsyncHttpRequestSendInterceptorTest.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/test/java/org/apache/skywalking/apm/plugin/jetty/v90/client/AsyncHttpRequestSendInterceptorTest.java new file mode 100644 index 000000000..10487bb44 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.0-plugin/src/test/java/org/apache/skywalking/apm/plugin/jetty/v90/client/AsyncHttpRequestSendInterceptorTest.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v90.client; + +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractTracingSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.context.trace.TraceSegment; +import org.apache.skywalking.apm.agent.core.context.util.TagValuePair; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.test.helper.SegmentHelper; +import org.apache.skywalking.apm.agent.test.helper.SpanHelper; +import org.apache.skywalking.apm.agent.test.tools.AgentServiceRule; +import org.apache.skywalking.apm.agent.test.tools.SegmentStorage; +import org.apache.skywalking.apm.agent.test.tools.SegmentStoragePoint; +import org.apache.skywalking.apm.agent.test.tools.SpanAssert; +import org.apache.skywalking.apm.agent.test.tools.TracingSegmentRunner; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.ResponseNotifier; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +import static org.apache.skywalking.apm.agent.test.tools.SpanAssert.assertComponent; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +@RunWith(TracingSegmentRunner.class) +public class AsyncHttpRequestSendInterceptorTest { + @SegmentStoragePoint + private SegmentStorage segmentStorage; + + @Rule + public AgentServiceRule serviceRule = new AgentServiceRule(); + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + private MockHttpRequest httpRequestEnhancedInstance; + private AsyncHttpRequestSendInterceptor asyncHttpRequestSendInterceptor; + + private MockResponseNotifier responseNotifierEnhancedInstance; + private ResponseNotifierInterceptor responseNotifierInterceptor; + + @Mock + private HttpClient httpClient; + @Mock + private Response response; + private Object[] allArguments; + private Class[] argumentTypes; + private URI uri = URI.create("http://localhost:8080/test"); + + @Before + public void setUp() throws Exception { + httpRequestEnhancedInstance = new MockHttpRequest(httpClient, uri); + responseNotifierEnhancedInstance = new MockResponseNotifier(httpClient); + + Result results = new Result(httpRequestEnhancedInstance, response); + allArguments = new Object[]{(Response.CompleteListener) result -> { }, results}; + argumentTypes = new Class[]{List.class, Result.class}; + + asyncHttpRequestSendInterceptor = new AsyncHttpRequestSendInterceptor(); + responseNotifierInterceptor = new ResponseNotifierInterceptor(); + } + + @Test + public void testHttpMethodsAround() throws Throwable { + asyncHttpRequestSendInterceptor.beforeMethod(httpRequestEnhancedInstance, null, allArguments, argumentTypes, null); + asyncHttpRequestSendInterceptor.afterMethod(httpRequestEnhancedInstance, null, allArguments, argumentTypes, null); + responseNotifierInterceptor.beforeMethod(responseNotifierEnhancedInstance, null, allArguments, argumentTypes, null); + responseNotifierInterceptor.afterMethod(responseNotifierEnhancedInstance, null, allArguments, argumentTypes, null); + + Map attributes = httpRequestEnhancedInstance.getAttributes(); + AbstractSpan asyncSpan = (AbstractSpan) attributes.get(Constants.SW_JETTY_EXIT_SPAN_KEY); + assertNotNull(asyncSpan); + + assertJettySpan(); + + Assert.assertEquals(false, SpanHelper.getErrorOccurred(asyncSpan)); + } + + @Test + public void testMethodsAroundError() throws Throwable { + asyncHttpRequestSendInterceptor.beforeMethod(httpRequestEnhancedInstance, null, allArguments, argumentTypes, null); + asyncHttpRequestSendInterceptor.handleMethodException(httpRequestEnhancedInstance, null, allArguments, argumentTypes, new RuntimeException()); + asyncHttpRequestSendInterceptor.afterMethod(httpRequestEnhancedInstance, null, allArguments, argumentTypes, null); + responseNotifierInterceptor.beforeMethod(responseNotifierEnhancedInstance, null, allArguments, argumentTypes, null); + responseNotifierInterceptor.handleMethodException(responseNotifierEnhancedInstance, null, allArguments, argumentTypes, new RuntimeException()); + responseNotifierInterceptor.afterMethod(responseNotifierEnhancedInstance, null, allArguments, argumentTypes, null); + + Map attributes = httpRequestEnhancedInstance.getAttributes(); + AbstractSpan asyncSpan = (AbstractSpan) attributes.get(Constants.SW_JETTY_EXIT_SPAN_KEY); + assertNotNull(asyncSpan); + + assertJettySpan(); + + Assert.assertEquals(true, SpanHelper.getErrorOccurred(asyncSpan)); + SpanAssert.assertException(SpanHelper.getLogs(asyncSpan).get(0), RuntimeException.class); + } + + private void assertJettySpan() { + assertThat(segmentStorage.getTraceSegments().size(), is(1)); + TraceSegment traceSegment = segmentStorage.getTraceSegments().get(0); + + Assert.assertEquals(1, SegmentHelper.getSpans(traceSegment).size()); + AbstractTracingSpan finishedSpan = SegmentHelper.getSpans(traceSegment).get(0); + assertNotNull(finishedSpan); + assertComponent(finishedSpan, ComponentsDefine.JETTY_CLIENT); + MatcherAssert.assertThat(finishedSpan.isExit(), is(true)); + SpanAssert.assertLayer(finishedSpan, SpanLayer.HTTP); + + List tags = SpanHelper.getTags(finishedSpan); + assertThat(tags.size(), is(2)); + assertThat(tags.get(0).getValue(), is("GET")); + assertThat(tags.get(1).getValue(), is(uri.toString())); + } + + private class MockHttpRequest extends HttpRequest implements EnhancedInstance { + public MockHttpRequest(HttpClient httpClient, URI uri) { + super(httpClient, uri); + } + + @Override + public URI getURI() { + return uri; + } + + @Override + public Object getSkyWalkingDynamicField() { + return null; + } + + @Override + public void setSkyWalkingDynamicField(Object value) { + + } + } + + private class MockResponseNotifier extends ResponseNotifier implements EnhancedInstance { + public MockResponseNotifier(HttpClient client) { + super(client); + } + + @Override + public Object getSkyWalkingDynamicField() { + return null; + } + + @Override + public void setSkyWalkingDynamicField(Object value) { + + } + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/AsyncHttpRequestSendInterceptor.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/AsyncHttpRequestSendInterceptor.java new file mode 100644 index 000000000..6f8870af4 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/AsyncHttpRequestSendInterceptor.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v9.client; + +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; +import org.apache.skywalking.apm.agent.core.context.tag.Tags; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpFields; + +import java.lang.reflect.Method; + +public class AsyncHttpRequestSendInterceptor implements InstanceMethodsAroundInterceptor { + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInterceptResult result) throws Throwable { + HttpRequest request = (HttpRequest) objInst; + ContextCarrier contextCarrier = new ContextCarrier(); + AbstractSpan span = ContextManager.createExitSpan(request.getURI().getPath(), contextCarrier, + request.getHost() + ":" + request.getPort()); + span.setComponent(ComponentsDefine.JETTY_CLIENT); + + Tags.HTTP.METHOD.set(span, getHttpMethod(request)); + Tags.URL.set(span, request.getURI().toString()); + SpanLayer.asHttp(span); + + CarrierItem next = contextCarrier.items(); + HttpFields field = request.getHeaders(); + while (next.hasNext()) { + next = next.next(); + field.add(next.getHeadKey(), next.getHeadValue()); + } + + span.prepareForAsync(); + request.attribute(Constants.SW_JETTY_EXIT_SPAN_KEY, span); + Response.CompleteListener callback = (Response.CompleteListener) allArguments[0]; + allArguments[0] = new CompleteListenerWrapper(callback, ContextManager.capture()); + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret) throws Throwable { + ContextManager.stopSpan(); + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t) { + ContextManager.activeSpan().log(t); + } + + public String getHttpMethod(HttpRequest request) { + String method = request.getMethod(); + + if (method == null || method.length() == 0) { + method = "GET"; + } + + return method; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/CompleteListenerWrapper.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/CompleteListenerWrapper.java new file mode 100644 index 000000000..50697bf3e --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/CompleteListenerWrapper.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v9.client; + +import org.apache.skywalking.apm.agent.core.context.ContextManager; +import org.apache.skywalking.apm.agent.core.context.ContextSnapshot; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; + +public class CompleteListenerWrapper implements Response.CompleteListener { + private Response.CompleteListener callback; + private ContextSnapshot context; + + public CompleteListenerWrapper(Response.CompleteListener callback, ContextSnapshot context) { + this.callback = callback; + this.context = context; + } + + @Override + public void onComplete(Result result) { + AbstractSpan span = ContextManager.createLocalSpan(Constants.PLUGIN_NAME + "/CompleteListener/onComplete"); + span.setComponent(ComponentsDefine.JETTY_CLIENT); + SpanLayer.asHttp(span); + if (context != null) { + ContextManager.continued(context); + } + if (callback != null) { + callback.onComplete(result); + } + ContextManager.stopSpan(); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/Constants.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/Constants.java new file mode 100644 index 000000000..614e3ff45 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/Constants.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v9.client; + +public class Constants { + public final static String SW_JETTY_EXIT_SPAN_KEY = "SW_JETTY_EXIT_SPAN"; + + public final static String PLUGIN_NAME = "JettyClient9.x"; +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/ResponseNotifierInterceptor.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/ResponseNotifierInterceptor.java new file mode 100644 index 000000000..e1ba9c9ef --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/ResponseNotifierInterceptor.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v9.client; + +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Result; + +import java.lang.reflect.Method; +import java.util.Optional; + +public class ResponseNotifierInterceptor implements InstanceMethodsAroundInterceptor { + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInterceptResult result) throws Throwable { + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret) throws Throwable { + // get async span and stop it. + Optional.ofNullable(getAsyncSpan(allArguments)).ifPresent(v -> v.asyncFinish()); + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t) { + Optional.ofNullable(getAsyncSpan(allArguments)).ifPresent(v -> v.log(t)); + } + + private AbstractSpan getAsyncSpan(Object[] allArguments) { + Result results = (Result) allArguments[1]; + if (results == null) { + return null; + } + Request request = results.getRequest(); + if (request == null) { + return null; + } + + return (AbstractSpan) request.getAttributes().get(Constants.SW_JETTY_EXIT_SPAN_KEY); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/define/HttpRequestInstrumentation.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/define/HttpRequestInstrumentation.java index d0456c11d..5d8cb05ee 100644 --- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/define/HttpRequestInstrumentation.java +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/define/HttpRequestInstrumentation.java @@ -35,15 +35,20 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; /** * {@link HttpRequestInstrumentation} enhance the send method without argument in - * org.eclipse.jetty.client.HttpRequest by org.apache.skywalking.apm.plugin.jetty.client.SyncHttpRequestSendInterceptor - * and enhance the send with org.eclipse.jetty.client.api.Response$CompleteListener parameter - * by org.apache.skywalking.apm.plugin.jetty.client.AsyncHttpRequestSendInterceptor + * org.eclipse.jetty.client.HttpRequest by + * org.apache.skywalking.apm.plugin.jetty.client.SyncHttpRequestSendInterceptor and enhance the + * send with org.eclipse.jetty.client.api.Response$CompleteListener parameter by + * org.apache.skywalking.apm.plugin.jetty.client.AsyncHttpRequestSendInterceptor */ public class HttpRequestInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { private static final String ENHANCE_CLASS = "org.eclipse.jetty.client.HttpRequest"; private static final String ENHANCE_CLASS_NAME = "send"; - public static final String SYNC_SEND_INTERCEPTOR = "org.apache.skywalking.apm.plugin.jetty.v9.client.SyncHttpRequestSendInterceptor"; + public static final String SYNC_SEND_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.jetty.v9.client.SyncHttpRequestSendInterceptor"; + + public static final String ASYNC_SEND_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.jetty.v9.client.AsyncHttpRequestSendInterceptor"; @Override public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { @@ -54,7 +59,7 @@ public class HttpRequestInstrumentation extends ClassInstanceMethodsEnhancePlugi public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { return new InstanceMethodsInterceptPoint[] { new InstanceMethodsInterceptPoint() { - //sync call interceptor point + // sync call interceptor point @Override public ElementMatcher getMethodsMatcher() { return named(ENHANCE_CLASS_NAME).and(takesArguments(0)); @@ -69,6 +74,23 @@ public class HttpRequestInstrumentation extends ClassInstanceMethodsEnhancePlugi public boolean isOverrideArgs() { return false; } + }, + new InstanceMethodsInterceptPoint() { + // async call interceptor point + @Override + public ElementMatcher getMethodsMatcher() { + return named(ENHANCE_CLASS_NAME).and(takesArguments(1)); + } + + @Override + public String getMethodsInterceptor() { + return ASYNC_SEND_INTERCEPTOR; + } + + @Override + public boolean isOverrideArgs() { + return true; + } } }; } diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/define/ResponseNotifierInstrumentation.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/define/ResponseNotifierInstrumentation.java new file mode 100644 index 000000000..d61a61ca5 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v9/client/define/ResponseNotifierInstrumentation.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v9.client.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.WitnessMethod; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; +import org.apache.skywalking.apm.agent.core.plugin.match.NameMatch; + +import java.util.Collections; +import java.util.List; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +/** + * {@link ResponseNotifierInstrumentation} enhance the send method in + * org.eclipse.jetty.client.ResponseNotifier by + * org.apache.skywalking.apm.plugin.jetty.v9.client.ResponseNotifierInterceptor + *

+ * It is implemented to be able to stop the asynchronous span in the attributes of the request. + */ +public class ResponseNotifierInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { + + private static final String ENHANCE_CLASS = "org.eclipse.jetty.client.ResponseNotifier"; + private static final String ENHANCE_CLASS_NAME = "notifyComplete"; + public static final String SYNC_SEND_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.jetty.v9.client.ResponseNotifierInterceptor"; + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[0]; + } + + @Override + public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { + return new InstanceMethodsInterceptPoint[]{ + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher getMethodsMatcher() { + return named(ENHANCE_CLASS_NAME).and(takesArguments(2)).and(takesArgument(0, List.class)); + } + + @Override + public String getMethodsInterceptor() { + return SYNC_SEND_INTERCEPTOR; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } + + @Override + protected ClassMatch enhanceClass() { + return NameMatch.byName(ENHANCE_CLASS); + } + + @Override + protected String[] witnessClasses() { + return new String[]{"org.eclipse.jetty.client.AbstractHttpClientTransport"}; + } + + @Override + protected List witnessMethods() { + return Collections.singletonList(new WitnessMethod( + "org.eclipse.jetty.http.HttpFields", + named("getQuality") + )); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/resources/skywalking-plugin.def b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/resources/skywalking-plugin.def index d430a9b9b..cc6343bc3 100644 --- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/resources/skywalking-plugin.def +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/main/resources/skywalking-plugin.def @@ -15,3 +15,4 @@ # limitations under the License. jetty-client-9.x=org.apache.skywalking.apm.plugin.jetty.v9.client.define.HttpRequestInstrumentation +jetty-client-9.x=org.apache.skywalking.apm.plugin.jetty.v9.client.define.ResponseNotifierInstrumentation \ No newline at end of file diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/jetty/v9/client/AsyncHttpRequestSendInterceptorTest.java b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/jetty/v9/client/AsyncHttpRequestSendInterceptorTest.java new file mode 100644 index 000000000..ae3dc47db --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-client-9.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/jetty/v9/client/AsyncHttpRequestSendInterceptorTest.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.jetty.v9.client; + +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractTracingSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.context.trace.TraceSegment; +import org.apache.skywalking.apm.agent.core.context.util.TagValuePair; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.test.helper.SegmentHelper; +import org.apache.skywalking.apm.agent.test.helper.SpanHelper; +import org.apache.skywalking.apm.agent.test.tools.AgentServiceRule; +import org.apache.skywalking.apm.agent.test.tools.SegmentStorage; +import org.apache.skywalking.apm.agent.test.tools.SegmentStoragePoint; +import org.apache.skywalking.apm.agent.test.tools.SpanAssert; +import org.apache.skywalking.apm.agent.test.tools.TracingSegmentRunner; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.ResponseNotifier; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +import static org.apache.skywalking.apm.agent.test.tools.SpanAssert.assertComponent; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +@RunWith(TracingSegmentRunner.class) +public class AsyncHttpRequestSendInterceptorTest { + @SegmentStoragePoint + private SegmentStorage segmentStorage; + + @Rule + public AgentServiceRule serviceRule = new AgentServiceRule(); + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + private MockHttpRequest httpRequestEnhancedInstance; + private AsyncHttpRequestSendInterceptor asyncHttpRequestSendInterceptor; + + private MockResponseNotifier responseNotifierEnhancedInstance; + private ResponseNotifierInterceptor responseNotifierInterceptor; + + @Mock + private HttpClient httpClient; + @Mock + private Response response; + private Object[] allArguments; + private Class[] argumentTypes; + private URI uri = URI.create("http://localhost:8080/test"); + + @Before + public void setUp() throws Exception { + httpRequestEnhancedInstance = new MockHttpRequest(httpClient, uri); + responseNotifierEnhancedInstance = new MockResponseNotifier(httpClient); + + Result results = new Result(httpRequestEnhancedInstance, response); + allArguments = new Object[]{(Response.CompleteListener) result -> { }, results}; + argumentTypes = new Class[]{List.class, Result.class}; + + asyncHttpRequestSendInterceptor = new AsyncHttpRequestSendInterceptor(); + responseNotifierInterceptor = new ResponseNotifierInterceptor(); + } + + @Test + public void testHttpMethodsAround() throws Throwable { + asyncHttpRequestSendInterceptor.beforeMethod(httpRequestEnhancedInstance, null, allArguments, argumentTypes, null); + asyncHttpRequestSendInterceptor.afterMethod(httpRequestEnhancedInstance, null, allArguments, argumentTypes, null); + responseNotifierInterceptor.beforeMethod(responseNotifierEnhancedInstance, null, allArguments, argumentTypes, null); + responseNotifierInterceptor.afterMethod(responseNotifierEnhancedInstance, null, allArguments, argumentTypes, null); + + Map attributes = httpRequestEnhancedInstance.getAttributes(); + AbstractSpan asyncSpan = (AbstractSpan) attributes.get(Constants.SW_JETTY_EXIT_SPAN_KEY); + assertNotNull(asyncSpan); + + assertJettySpan(); + + Assert.assertEquals(false, SpanHelper.getErrorOccurred(asyncSpan)); + } + + @Test + public void testMethodsAroundError() throws Throwable { + asyncHttpRequestSendInterceptor.beforeMethod(httpRequestEnhancedInstance, null, allArguments, argumentTypes, null); + asyncHttpRequestSendInterceptor.handleMethodException(httpRequestEnhancedInstance, null, allArguments, argumentTypes, new RuntimeException()); + asyncHttpRequestSendInterceptor.afterMethod(httpRequestEnhancedInstance, null, allArguments, argumentTypes, null); + responseNotifierInterceptor.beforeMethod(responseNotifierEnhancedInstance, null, allArguments, argumentTypes, null); + responseNotifierInterceptor.handleMethodException(responseNotifierEnhancedInstance, null, allArguments, argumentTypes, new RuntimeException()); + responseNotifierInterceptor.afterMethod(responseNotifierEnhancedInstance, null, allArguments, argumentTypes, null); + + Map attributes = httpRequestEnhancedInstance.getAttributes(); + AbstractSpan asyncSpan = (AbstractSpan) attributes.get(Constants.SW_JETTY_EXIT_SPAN_KEY); + assertNotNull(asyncSpan); + + assertJettySpan(); + + Assert.assertEquals(true, SpanHelper.getErrorOccurred(asyncSpan)); + SpanAssert.assertException(SpanHelper.getLogs(asyncSpan).get(0), RuntimeException.class); + } + + private void assertJettySpan() { + assertThat(segmentStorage.getTraceSegments().size(), is(1)); + TraceSegment traceSegment = segmentStorage.getTraceSegments().get(0); + + Assert.assertEquals(1, SegmentHelper.getSpans(traceSegment).size()); + AbstractTracingSpan finishedSpan = SegmentHelper.getSpans(traceSegment).get(0); + assertNotNull(finishedSpan); + assertComponent(finishedSpan, ComponentsDefine.JETTY_CLIENT); + MatcherAssert.assertThat(finishedSpan.isExit(), is(true)); + SpanAssert.assertLayer(finishedSpan, SpanLayer.HTTP); + + List tags = SpanHelper.getTags(finishedSpan); + assertThat(tags.size(), is(2)); + assertThat(tags.get(0).getValue(), is("GET")); + assertThat(tags.get(1).getValue(), is(uri.toString())); + } + + private class MockHttpRequest extends HttpRequest implements EnhancedInstance { + public MockHttpRequest(HttpClient httpClient, URI uri) { + super(httpClient, uri); + } + + @Override + public String getMethod() { + return "GET"; + } + + @Override + public URI getURI() { + return uri; + } + + @Override + public Object getSkyWalkingDynamicField() { + return null; + } + + @Override + public void setSkyWalkingDynamicField(Object value) { + + } + } + + private class MockResponseNotifier extends ResponseNotifier implements EnhancedInstance { + public MockResponseNotifier(HttpClient client) { + super(client); + } + + @Override + public Object getSkyWalkingDynamicField() { + return null; + } + + @Override + public void setSkyWalkingDynamicField(Object value) { + + } + } +} diff --git a/test/plugin/scenarios/jetty-scenario/config/expectedData.yaml b/test/plugin/scenarios/jetty-scenario/config/expectedData.yaml index ea3039c06..99e3bf2e7 100644 --- a/test/plugin/scenarios/jetty-scenario/config/expectedData.yaml +++ b/test/plugin/scenarios/jetty-scenario/config/expectedData.yaml @@ -15,7 +15,7 @@ # limitations under the License. segmentItems: - serviceName: jettyserver-scenario - segmentSize: 1 + segmentSize: ge 3 segments: - segmentId: not null spans: @@ -38,9 +38,100 @@ segmentItems: refType: CrossProcess, parentSpanId: 1, parentTraceSegmentId: not null, parentServiceInstance: not null, parentService: jettyclient-scenario, traceId: not null} skipAnalysis: 'false' + - segmentId: not null + spans: + - operationName: /jettyserver-case/case/receiveContext-1 + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 19 + isError: false + spanType: Entry + peer: '' + tags: + - {key: url, value: 'http://localhost:18080/jettyserver-case/case/receiveContext-1'} + - {key: http.method, value: GET} + - {key: http.status_code, value: '200'} + refs: + - {parentEndpoint: GET:/jettyclient-case/case/jettyclient-case, networkAddress: 'localhost:18080', + refType: CrossProcess, parentSpanId: 2, parentTraceSegmentId: not null, parentServiceInstance: not + null, parentService: jettyclient-scenario, traceId: not null} + skipAnalysis: 'false' + - segmentId: not null + spans: + - operationName: /jettyserver-case/case/receiveContext-0 + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 19 + isError: false + spanType: Entry + peer: '' + skipAnalysis: false + tags: + - { key: url, value: 'http://localhost:18080/jettyserver-case/case/receiveContext-0' } + - { key: http.method, value: GET } + - { key: http.status_code, value: '200' } + refs: + - { parentEndpoint: JettyClient9.x/CompleteListener/onComplete, networkAddress: 'localhost:18080', + refType: CrossProcess, parentSpanId: 1, parentTraceSegmentId: not null, parentServiceInstance: not null, + parentService: jettyclient-scenario, traceId: not null } - serviceName: jettyclient-scenario - segmentSize: 2 + segmentSize: ge 3 segments: + - segmentId: not null + spans: + - operationName: HEAD:/jettyclient-case/case/healthCheck + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 1 + isError: false + spanType: Entry + peer: '' + skipAnalysis: false + tags: + - {key: url, value: 'http://localhost:8080/jettyclient-case/case/healthCheck'} + - {key: http.method, value: HEAD} + - {key: http.status_code, value: '200'} + - segmentId: not null + spans: + - operationName: /jettyserver-case/case/receiveContext-0 + parentSpanId: 0 + spanId: 1 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 2 + isError: false + spanType: Exit + peer: localhost:18080 + skipAnalysis: false + tags: + - {key: url, value: 'http://localhost:18080/jettyserver-case/case/receiveContext-0'} + - {key: http.method, value: GET} + - {key: http.status_code, value: '200'} + - operationName: JettyClient9.x/CompleteListener/onComplete + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 18 + isError: false + spanType: Local + peer: '' + skipAnalysis: false + refs: + - {parentEndpoint: 'GET:/jettyclient-case/case/jettyclient-case', networkAddress: '', + refType: CrossThread, parentSpanId: 2, parentTraceSegmentId: not null, parentServiceInstance: not null, + parentService: jettyclient-scenario, traceId: not null} - segmentId: not null spans: - operationName: /jettyserver-case/case/receiveContext-0 @@ -53,10 +144,24 @@ segmentItems: isError: false spanType: Exit peer: localhost:18080 + skipAnalysis: false tags: - {key: http.method, value: GET} - {key: url, value: 'http://localhost:18080/jettyserver-case/case/receiveContext-0'} - skipAnalysis: 'false' + - operationName: /jettyserver-case/case/receiveContext-1 + parentSpanId: 0 + spanId: 2 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 18 + isError: false + spanType: Exit + peer: localhost:18080 + skipAnalysis: false + tags: + - {key: http.method, value: GET} + - {key: url, value: 'http://localhost:18080/jettyserver-case/case/receiveContext-1'} - operationName: GET:/jettyclient-case/case/jettyclient-case parentSpanId: -1 spanId: 0 @@ -66,8 +171,9 @@ segmentItems: componentId: 1 isError: false spanType: Entry + peer: '' + skipAnalysis: false tags: - {key: url, value: 'http://localhost:8080/jettyclient-case/case/jettyclient-case'} - {key: http.method, value: GET} - - {key: http.status_code, value: '200'} - skipAnalysis: 'false' + - {key: http.status_code, value: '200'} \ No newline at end of file diff --git a/test/plugin/scenarios/jetty-scenario/jettyclient-scenario/pom.xml b/test/plugin/scenarios/jetty-scenario/jettyclient-scenario/pom.xml index bfde57fb3..c48ad9315 100644 --- a/test/plugin/scenarios/jetty-scenario/jettyclient-scenario/pom.xml +++ b/test/plugin/scenarios/jetty-scenario/jettyclient-scenario/pom.xml @@ -38,6 +38,11 @@ jetty-client ${jettyclient.version} + + org.apache.httpcomponents + httpclient + ${httpclient.version} + org.springframework.boot diff --git a/test/plugin/scenarios/jetty-scenario/jettyclient-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyclient/controller/CaseController.java b/test/plugin/scenarios/jetty-scenario/jettyclient-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyclient/controller/CaseController.java index 2b6977875..d45d8adfe 100644 --- a/test/plugin/scenarios/jetty-scenario/jettyclient-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyclient/controller/CaseController.java +++ b/test/plugin/scenarios/jetty-scenario/jettyclient-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyclient/controller/CaseController.java @@ -16,10 +16,16 @@ * */ -package org.apache.skywalking.apm.testcase.jettyclient.controller; +package org.apache.skywalking.apm.testcase.jettyclient.contr; import javax.annotation.PostConstruct; +import java.io.IOException; + +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Response; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Controller; @@ -45,6 +51,16 @@ public class CaseController { @ResponseBody public String jettyClientScenario() throws Exception { client.newRequest("http://" + jettyServerHost + ":18080/jettyserver-case/case/receiveContext-0").send(); + Response.CompleteListener listener = result -> { + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpGet httpget = new HttpGet("http://" + jettyServerHost + ":18080/jettyserver-case/case/receiveContext-0"); + try { + httpclient.execute(httpget); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + client.newRequest("http://" + jettyServerHost + ":18080/jettyserver-case/case/receiveContext-1").send(listener); return "Success"; } diff --git a/test/plugin/scenarios/jetty-scenario/jettyserver-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyserver/Application.java b/test/plugin/scenarios/jetty-scenario/jettyserver-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyserver/Application.java index 6af8c860c..08f465d4d 100644 --- a/test/plugin/scenarios/jetty-scenario/jettyserver-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyserver/Application.java +++ b/test/plugin/scenarios/jetty-scenario/jettyserver-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyserver/Application.java @@ -19,6 +19,7 @@ package org.apache.skywalking.apm.testcase.jettyserver; import java.net.InetSocketAddress; +import org.apache.skywalking.apm.testcase.jettyserver.servlet.AsyncCaseServlet; import org.apache.skywalking.apm.testcase.jettyserver.servlet.CaseServlet; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; @@ -31,6 +32,7 @@ public class Application { ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); servletContextHandler.setContextPath(contextPath); servletContextHandler.addServlet(CaseServlet.class, CaseServlet.SERVLET_PATH); + servletContextHandler.addServlet(AsyncCaseServlet.class, AsyncCaseServlet.SERVLET_PATH); jettyServer.setHandler(servletContextHandler); jettyServer.start(); } diff --git a/test/plugin/scenarios/jetty-scenario/jettyserver-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyserver/servlet/AsyncCaseServlet.java b/test/plugin/scenarios/jetty-scenario/jettyserver-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyserver/servlet/AsyncCaseServlet.java new file mode 100644 index 000000000..667fdafd1 --- /dev/null +++ b/test/plugin/scenarios/jetty-scenario/jettyserver-scenario/src/main/java/org/apache/skywalking/apm/testcase/jettyserver/servlet/AsyncCaseServlet.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.testcase.jettyserver.servlet; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class AsyncCaseServlet extends HttpServlet { + public static String SERVLET_PATH = "/case/receiveContext-1"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + } +} diff --git a/test/plugin/scenarios/jetty-scenario/pom.xml b/test/plugin/scenarios/jetty-scenario/pom.xml index 9700a48f9..f28863c63 100644 --- a/test/plugin/scenarios/jetty-scenario/pom.xml +++ b/test/plugin/scenarios/jetty-scenario/pom.xml @@ -43,6 +43,8 @@ ${test.framework.version} ${test.framework.version} + 4.3.5 + 2.6.2