/* * Copyright 2016-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.netty; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.glowroot.agent.plugin.api.Agent; import org.glowroot.agent.plugin.api.AuxThreadContext; import org.glowroot.agent.plugin.api.OptionalThreadContext; import org.glowroot.agent.plugin.api.ThreadContext; import org.glowroot.agent.plugin.api.TimerName; import org.glowroot.agent.plugin.api.TraceEntry; import org.glowroot.agent.plugin.api.weaving.BindParameter; import org.glowroot.agent.plugin.api.weaving.BindReceiver; 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.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 Netty3Aspect { // the field and method names are verbose to avoid conflict since they will become fields // and methods in all classes that extend org.jboss.netty.channel.Channel @Mixin({"org.jboss.netty.channel.Channel"}) public abstract static class ChannelImpl implements ChannelMixin { private volatile boolean glowroot$completeAsyncTransaction; @Override public boolean glowroot$getCompleteAsyncTransaction() { return glowroot$completeAsyncTransaction; } @Override public void glowroot$setCompleteAsyncTransaction(boolean completeAsyncTransaction) { this.glowroot$completeAsyncTransaction = completeAsyncTransaction; } } // the method names are verbose to avoid conflict since they will become methods in all classes // that extend org.jboss.netty.channel.Channel public interface ChannelMixin { boolean glowroot$getCompleteAsyncTransaction(); void glowroot$setCompleteAsyncTransaction(boolean completeAsyncTransaction); } // the field and method names are verbose to avoid conflict since they will become fields // and methods in all classes that extend org.jboss.netty.channel.ChannelFutureListener @Mixin({"org.jboss.netty.channel.ChannelFutureListener"}) public abstract static class ListenerImpl implements ListenerMixin { private volatile @Nullable AuxThreadContext glowroot$auxContext; @Override public @Nullable AuxThreadContext glowroot$getAuxContext() { return glowroot$auxContext; } @Override public void glowroot$setAuxContext(@Nullable AuxThreadContext auxContext) { this.glowroot$auxContext = auxContext; } } // the method names are verbose to avoid conflict since they will become methods in all classes // that extend org.jboss.netty.channel.ChannelFutureListener public interface ListenerMixin { @Nullable AuxThreadContext glowroot$getAuxContext(); void glowroot$setAuxContext(@Nullable AuxThreadContext auxContext); } @Shim("org.jboss.netty.channel.ChannelHandlerContext") public interface ChannelHandlerContext { @Shim("org.jboss.netty.channel.Channel getChannel()") @Nullable ChannelMixin glowroot$getChannel(); } @Shim("org.jboss.netty.handler.codec.http.HttpRequest") public interface HttpRequest { @Shim("org.jboss.netty.handler.codec.http.HttpMethod getMethod()") HttpMethod glowroot$getMethod(); @Nullable String getUri(); } @Shim("org.jboss.netty.handler.codec.http.HttpMethod") public interface HttpMethod { @Nullable String getName(); } @Shim("org.jboss.netty.channel.MessageEvent") public interface MessageEvent { @Nullable Object getMessage(); } @Shim("org.jboss.netty.handler.codec.http.HttpMessage") public interface HttpMessage { boolean isChunked(); } @Shim("org.jboss.netty.handler.codec.http.HttpChunk") public interface HttpChunk { boolean isLast(); } @Pointcut(className = "org.jboss.netty.channel.ChannelHandlerContext", methodName = "sendUpstream", methodParameterTypes = {"org.jboss.netty.channel.ChannelEvent"}, nestingGroup = "netty-inbound", timerName = "http request") public static class InboundAdvice { private static final TimerName timerName = Agent.getTimerName(InboundAdvice.class); @IsEnabled public static boolean isEnabled(@BindReceiver ChannelHandlerContext channelHandlerContext, @BindParameter @Nullable Object channelEvent) { return channelHandlerContext.glowroot$getChannel() != null && channelEvent != null && channelEvent instanceof MessageEvent && ((MessageEvent) channelEvent).getMessage() instanceof HttpRequest; } @OnBefore public static TraceEntry onBefore(OptionalThreadContext context, @BindReceiver ChannelHandlerContext channelHandlerContext, // not null, just checked above in isEnabled() @BindParameter Object channelEvent) { @SuppressWarnings("nullness") // just checked above in isEnabled() @Nonnull ChannelMixin channel = channelHandlerContext.glowroot$getChannel(); // just checked valid cast above in isEnabled() @SuppressWarnings("nullness") // just checked above in isEnabled() @Nonnull Object msg = ((MessageEvent) channelEvent).getMessage(); // just checked valid cast above in isEnabled() HttpRequest request = (HttpRequest) msg; HttpMethod method = request.glowroot$getMethod(); String methodName = method == null ? null : method.getName(); channel.glowroot$setCompleteAsyncTransaction(true); return NettyAspect.startAsyncTransaction(context, methodName, request.getUri(), timerName); } @OnReturn public static void onReturn(@BindTraveler TraceEntry traceEntry) { traceEntry.end(); } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler TraceEntry traceEntry) { traceEntry.endWithError(t); } } @Shim("com.typesafe.netty.http.pipelining.OrderedDownstreamChannelEvent") public interface OrderedDownstreamChannelEvent { boolean isLast(); } @Pointcut(className = "org.jboss.netty.channel.ChannelDownstreamHandler", methodName = "handleDownstream", methodParameterTypes = {"org.jboss.netty.channel.ChannelHandlerContext", "org.jboss.netty.channel.ChannelEvent"}) public static class OutboundAdvice { @IsEnabled public static boolean isEnabled( @BindParameter @Nullable ChannelHandlerContext channelHandlerContext) { if (channelHandlerContext == null) { return false; } ChannelMixin channel = channelHandlerContext.glowroot$getChannel(); return channel != null && channel.glowroot$getCompleteAsyncTransaction(); } @OnBefore public static void onBefore(ThreadContext context, @BindParameter @Nullable ChannelHandlerContext channelHandlerContext, @BindParameter @Nullable Object channelEvent) { if (channelHandlerContext == null) { return; } if (channelEvent instanceof OrderedDownstreamChannelEvent) { // play 2.2.x and later implements its own chunked transfer, not using netty's // MessageEvent/HttpMessage/HttpChunk if (((OrderedDownstreamChannelEvent) channelEvent).isLast()) { completeAsyncTransaction(context, channelHandlerContext); } return; } if (!(channelEvent instanceof MessageEvent)) { return; } Object messageEvent = ((MessageEvent) channelEvent).getMessage(); if (messageEvent instanceof HttpMessage) { if (!((HttpMessage) messageEvent).isChunked()) { completeAsyncTransaction(context, channelHandlerContext); } return; } if (messageEvent instanceof HttpChunk) { if (((HttpChunk) messageEvent).isLast()) { completeAsyncTransaction(context, channelHandlerContext); } return; } } private static void completeAsyncTransaction(ThreadContext context, ChannelHandlerContext channelHandlerContext) { context.setTransactionAsyncComplete(); ChannelMixin channel = channelHandlerContext.glowroot$getChannel(); if (channel != null) { channel.glowroot$setCompleteAsyncTransaction(false); } } } @Pointcut(className = "org.jboss.netty.channel.ChannelFuture", methodName = "addListener", methodParameterTypes = {"org.jboss.netty.channel.ChannelFutureListener"}) public static class AddListenerAdvice { @OnBefore public static void onBefore(ThreadContext context, @BindParameter ListenerMixin listener) { AuxThreadContext auxContext = context.createAuxThreadContext(); listener.glowroot$setAuxContext(auxContext); } } @Pointcut(className = "org.jboss.netty.channel.ChannelFutureListener", methodName = "operationComplete", methodParameterTypes = {"org.jboss.netty.channel.ChannelFuture"}) public static class OperationCompleteAdvice { @IsEnabled public static boolean isEnabled(@BindReceiver ListenerMixin listener) { return listener.glowroot$getAuxContext() != null; } @OnBefore public static TraceEntry onBefore(@BindReceiver ListenerMixin listener) { @SuppressWarnings("nullness") // just checked above in isEnabled() @Nonnull AuxThreadContext auxContext = listener.glowroot$getAuxContext(); listener.glowroot$setAuxContext(null); return auxContext.start(); } @OnReturn public static void onReturn(@BindTraveler TraceEntry traceEntry) { traceEntry.end(); } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler TraceEntry traceEntry) { traceEntry.endWithError(t); } } @Pointcut(className = "org.jboss.netty.channel.Channel", methodName = "close", methodParameterTypes = {}) public static class CloseAdvice { @OnBefore public static void onBefore(ThreadContext context) { context.setTransactionAsyncComplete(); } } }