/* * Copyright 2015-2017 the original author or authors. * * Licensed 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.glowroot.agent.plugin.httpclient; import java.util.concurrent.Executor; import java.util.concurrent.Future; import javax.annotation.Nullable; import org.glowroot.agent.plugin.api.Agent; import org.glowroot.agent.plugin.api.AsyncTraceEntry; import org.glowroot.agent.plugin.api.MessageSupplier; import org.glowroot.agent.plugin.api.ThreadContext; import org.glowroot.agent.plugin.api.Timer; import org.glowroot.agent.plugin.api.TimerName; import org.glowroot.agent.plugin.api.util.FastThreadLocal; import org.glowroot.agent.plugin.api.weaving.BindClassMeta; import org.glowroot.agent.plugin.api.weaving.BindParameter; import org.glowroot.agent.plugin.api.weaving.BindReceiver; import org.glowroot.agent.plugin.api.weaving.BindReturn; import org.glowroot.agent.plugin.api.weaving.BindThrowable; import org.glowroot.agent.plugin.api.weaving.BindTraveler; import org.glowroot.agent.plugin.api.weaving.IsEnabled; import org.glowroot.agent.plugin.api.weaving.Mixin; import org.glowroot.agent.plugin.api.weaving.OnAfter; import org.glowroot.agent.plugin.api.weaving.OnBefore; import org.glowroot.agent.plugin.api.weaving.OnReturn; import org.glowroot.agent.plugin.api.weaving.OnThrow; import org.glowroot.agent.plugin.api.weaving.Pointcut; import org.glowroot.agent.plugin.api.weaving.Shim; public class AsyncHttpClientAspect { @SuppressWarnings("nullness:type.argument.type.incompatible") private static final FastThreadLocal<Boolean> ignoreFutureGet = new FastThreadLocal<Boolean>() { @Override protected Boolean initialValue() { return false; } }; @Shim("org.asynchttpclient.Request|com.ning.http.client.Request") public interface Request { @Nullable String getMethod(); } // the field and method names are verbose to avoid conflict since they will become fields // and methods in all classes that extend org.asynchttpclient.ListenableFuture or // com.ning.http.client.ListenableFuture @Mixin({"org.asynchttpclient.ListenableFuture", "com.ning.http.client.ListenableFuture"}) public abstract static class ListenableFutureImpl implements ListenableFutureMixin { // volatile not needed, only accessed by the main thread private @Nullable AsyncTraceEntry glowroot$asyncTraceEntry; @Override public @Nullable AsyncTraceEntry glowroot$getAsyncTraceEntry() { return glowroot$asyncTraceEntry; } @Override public void glowroot$setAsyncTraceEntry(@Nullable AsyncTraceEntry asyncTraceEntry) { this.glowroot$asyncTraceEntry = asyncTraceEntry; } } // the method names are verbose to avoid conflict since they will become methods in all classes // that extend org.asynchttpclient.ListenableFuture or com.ning.http.client.ListenableFuture public interface ListenableFutureMixin { @Nullable AsyncTraceEntry glowroot$getAsyncTraceEntry(); void glowroot$setAsyncTraceEntry(@Nullable AsyncTraceEntry asyncTraceEntry); } public interface ListenableFutureShim<V> extends Future<V> { Object glowroot$addListener(Runnable listener, Executor exec); } @Shim("org.asynchttpclient.ListenableFuture") public interface NewListenableFutureShim<V> extends ListenableFutureShim<V> { @Override @Shim("org.asynchttpclient.ListenableFuture" + " addListener(java.lang.Runnable, java.util.concurrent.Executor)") Object glowroot$addListener(Runnable listener, Executor exec); } @Shim("com.ning.http.client.ListenableFuture") public interface OldListenableFutureShim<V> extends ListenableFutureShim<V> { @Override @Shim("com.ning.http.client.ListenableFuture" + " addListener(java.lang.Runnable, java.util.concurrent.Executor)") Object glowroot$addListener(Runnable listener, Executor exec); } @Pointcut( className = "org.asynchttpclient.AsyncHttpClient|com.ning.http.client.AsyncHttpClient", methodName = "executeRequest", methodParameterTypes = {"org.asynchttpclient.Request|com.ning.http.client.Request", ".."}, nestingGroup = "http-client", timerName = "http client request") public static class ExecuteRequestAdvice { private static final TimerName timerName = Agent.getTimerName(ExecuteRequestAdvice.class); @OnBefore public static @Nullable AsyncTraceEntry onBefore(ThreadContext context, @BindParameter @Nullable Request request, @BindClassMeta AsyncHttpClientRequestInvoker requestInvoker) { // need to start trace entry @OnBefore in case it is executed in a "same thread // executor" in which case will be over in @OnReturn if (request == null) { return null; } String method = request.getMethod(); if (method == null) { method = ""; } else { method += " "; } String url = requestInvoker.getUrl(request); return context.startAsyncServiceCallEntry("HTTP", method + Uris.stripQueryString(url), MessageSupplier.create("http client request: {}{}", method, url), timerName); } @OnReturn public static void onReturn(@BindReturn @Nullable ListenableFutureMixin future, final @BindTraveler @Nullable AsyncTraceEntry asyncTraceEntry) { if (asyncTraceEntry == null) { return; } asyncTraceEntry.stopSyncTimer(); if (future == null) { asyncTraceEntry.end(); return; } future.glowroot$setAsyncTraceEntry(asyncTraceEntry); final ListenableFutureShim<?> listenableFuture = (ListenableFutureShim<?>) future; listenableFuture.glowroot$addListener(new Runnable() { // suppress warnings is needed because checker framework doesn't see that // asyncTraceEntry must be non-null here @Override @SuppressWarnings("dereference.of.nullable") public void run() { Throwable t = getException(listenableFuture); if (t == null) { asyncTraceEntry.end(); } else { asyncTraceEntry.endWithError(t); } } }, DirectExecutor.INSTANCE); } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler @Nullable AsyncTraceEntry asyncTraceEntry) { if (asyncTraceEntry != null) { asyncTraceEntry.stopSyncTimer(); asyncTraceEntry.endWithError(t); } } // this is hacky way to find out if future ended with exception or not private static @Nullable Throwable getException(ListenableFutureShim<?> future) { ignoreFutureGet.set(true); try { future.get(); } catch (Throwable t) { return t; } finally { ignoreFutureGet.set(false); } return null; } } @Pointcut(className = "java.util.concurrent.Future", subTypeRestriction = "org.asynchttpclient.ListenableFuture" + "|com.ning.http.client.ListenableFuture", methodName = "get", methodParameterTypes = {".."}, suppressionKey = "wait-on-future") public static class FutureGetAdvice { @IsEnabled public static boolean isEnabled() { return !ignoreFutureGet.get(); } @OnBefore public static @Nullable Timer onBefore(ThreadContext threadContext, @BindReceiver ListenableFutureMixin future) { AsyncTraceEntry asyncTraceEntry = future.glowroot$getAsyncTraceEntry(); if (asyncTraceEntry == null) { return null; } return asyncTraceEntry.extendSyncTimer(threadContext); } @OnAfter public static void onAfter(@BindTraveler @Nullable Timer syncTimer) { if (syncTimer != null) { syncTimer.stop(); } } } private static class DirectExecutor implements Executor { private static final DirectExecutor INSTANCE = new DirectExecutor(); @Override public void execute(Runnable command) { command.run(); } } }