All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glowroot.agent.plugin.servlet.ServletAspect Maven / Gradle / Ivy

There is a newer version: 0.14.0-beta.3
Show newest version
/*
 * Copyright 2011-2018 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.servlet;

import java.security.Principal;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;

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.ThreadContext.Priority;
import org.glowroot.agent.plugin.api.TimerName;
import org.glowroot.agent.plugin.api.TraceEntry;
import org.glowroot.agent.plugin.api.checker.Nullable;
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.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;
import org.glowroot.agent.plugin.servlet.DetailCapture.RequestHostAndPortDetail;
import org.glowroot.agent.plugin.servlet.ServletPluginProperties.SessionAttributePath;

// this plugin is careful not to rely on request or session objects being thread-safe
public class ServletAspect {

    private static final FastThreadLocal sendError =
            new FastThreadLocal();

    @Shim("javax.servlet.http.HttpServletRequest")
    public interface HttpServletRequest {

        @Shim("javax.servlet.http.HttpSession getSession(boolean)")
        @Nullable
        HttpSession glowroot$getSession(boolean create);

        @Nullable
        String getMethod();

        @Nullable
        String getContextPath();

        @Nullable
        String getServletPath();

        @Nullable
        String getPathInfo();

        @Nullable
        String getRequestURI();

        @Nullable
        String getQueryString();

        @Nullable
        Enumeration getHeaderNames();

        @Nullable
        Enumeration getHeaders(String name);

        @Nullable
        String getHeader(String name);

        // the map values should be String[], but typing as Object to be safe
        @Nullable
        Map getParameterMap();

        @Nullable
        Enumeration getParameterNames();

        @Nullable
        String /*@Nullable*/ [] getParameterValues(String name);

        @Nullable
        Object getAttribute(String name);

        void removeAttribute(String name);

        @Nullable
        String getRemoteAddr();

        @Nullable
        String getRemoteHost();

        @Nullable
        String getServerName();

        int getServerPort();
    }

    @Shim("javax.servlet.http.HttpServletResponse")
    public interface HttpServletResponse {}

    @Shim("javax.servlet.http.HttpSession")
    public interface HttpSession {

        @Nullable
        Object getAttribute(String name);

        @Nullable
        Enumeration getAttributeNames();

        @Nullable
        String getId();
    }

    @Pointcut(className = "javax.servlet.Servlet", methodName = "service",
            methodParameterTypes = {"javax.servlet.ServletRequest",
                    "javax.servlet.ServletResponse"},
            nestingGroup = "outer-servlet-or-filter", timerName = "http request")
    public static class ServiceAdvice {
        private static final TimerName timerName = Agent.getTimerName(ServiceAdvice.class);
        @OnBefore
        public static @Nullable TraceEntry onBefore(OptionalThreadContext context,
                @BindParameter @Nullable Object req, @BindClassMeta RequestInvoker requestInvoker) {
            return onBeforeCommon(context, req, null, requestInvoker);
        }
        @OnReturn
        public static void onReturn(OptionalThreadContext context,
                @BindTraveler @Nullable TraceEntry traceEntry,
                @SuppressWarnings("unused") @BindParameter @Nullable Object req,
                @BindParameter @Nullable Object res,
                @BindClassMeta ResponseInvoker responseInvoker) {
            if (traceEntry == null) {
                return;
            }
            if (res == null || !(res instanceof HttpServletResponse)) {
                // seems nothing sensible to do here other than ignore
                return;
            }
            ServletMessageSupplier messageSupplier =
                    (ServletMessageSupplier) context.getServletRequestInfo();
            if (messageSupplier != null && responseInvoker.hasGetStatusMethod()) {
                messageSupplier.setResponseCode(responseInvoker.getStatus(res));
            }
            FastThreadLocal.Holder errorMessageHolder = sendError.getHolder();
            String errorMessage = errorMessageHolder.get();
            if (errorMessage != null) {
                traceEntry.endWithError(errorMessage);
                errorMessageHolder.set(null);
            } else {
                traceEntry.end();
            }
            context.setServletRequestInfo(null);
        }
        @OnThrow
        public static void onThrow(@BindThrowable Throwable t, OptionalThreadContext context,
                @BindTraveler @Nullable TraceEntry traceEntry,
                @SuppressWarnings("unused") @BindParameter @Nullable Object req,
                @BindParameter @Nullable Object res) {
            if (traceEntry == null) {
                return;
            }
            if (res == null || !(res instanceof HttpServletResponse)) {
                // seems nothing sensible to do here other than ignore
                return;
            }
            ServletMessageSupplier messageSupplier =
                    (ServletMessageSupplier) context.getServletRequestInfo();
            if (messageSupplier != null) {
                // container will set this unless headers are already flushed
                messageSupplier.setResponseCode(500);
            }
            // ignoring potential sendError since this seems worse
            sendError.set(null);
            traceEntry.endWithError(t);
            context.setServletRequestInfo(null);
        }
        private static @Nullable TraceEntry onBeforeCommon(OptionalThreadContext context,
                @Nullable Object req, @Nullable String transactionTypeOverride,
                RequestInvoker requestInvoker) {
            if (context.getServletRequestInfo() != null) {
                return null;
            }
            if (req == null || !(req instanceof HttpServletRequest)) {
                // seems nothing sensible to do here other than ignore
                return null;
            }
            HttpServletRequest request = (HttpServletRequest) req;
            AuxThreadContext auxContextObj = (AuxThreadContext) request
                    .getAttribute(AsyncServletAspect.GLOWROOT_AUX_CONTEXT_REQUEST_ATTRIBUTE);
            if (auxContextObj != null) {
                request.removeAttribute(AsyncServletAspect.GLOWROOT_AUX_CONTEXT_REQUEST_ATTRIBUTE);
                AuxThreadContext auxContext = auxContextObj;
                return auxContext.startAndMarkAsyncTransactionComplete();
            }
            // request parameter map is collected in GetParameterAdvice
            // session info is collected here if the request already has a session
            ServletMessageSupplier messageSupplier;
            HttpSession session = request.glowroot$getSession(false);
            String requestUri = Strings.nullToEmpty(request.getRequestURI());
            // don't convert null to empty, since null means no query string, while empty means
            // url ended with ? but nothing after that
            String requestQueryString = request.getQueryString();
            String requestMethod = Strings.nullToEmpty(request.getMethod());
            String requestContextPath = Strings.nullToEmpty(request.getContextPath());
            String requestServletPath = Strings.nullToEmpty(request.getServletPath());
            String requestPathInfo = request.getPathInfo();
            Map requestHeaders = DetailCapture.captureRequestHeaders(request);
            RequestHostAndPortDetail requestHostAndPortDetail =
                    DetailCapture.captureRequestHostAndPortDetail(request, requestInvoker);
            if (session == null) {
                messageSupplier = new ServletMessageSupplier(requestMethod, requestContextPath,
                        requestServletPath, requestPathInfo, requestUri, requestQueryString,
                        requestHeaders, requestHostAndPortDetail,
                        Collections.emptyMap());
            } else {
                Map sessionAttributes = HttpSessions.getSessionAttributes(session);
                messageSupplier = new ServletMessageSupplier(requestMethod, requestContextPath,
                        requestServletPath, requestPathInfo, requestUri, requestQueryString,
                        requestHeaders, requestHostAndPortDetail, sessionAttributes);
            }
            String user = null;
            if (session != null) {
                SessionAttributePath userAttributePath =
                        ServletPluginProperties.userAttributePath();
                if (userAttributePath != null) {
                    // capture user now, don't use a lazy supplier
                    Object val = HttpSessions.getSessionAttribute(session, userAttributePath);
                    user = val == null ? null : val.toString();
                }
            }
            String transactionType;
            boolean setWithCoreMaxPriority = false;
            String transactionTypeHeader = request.getHeader("Glowroot-Transaction-Type");
            if ("Synthetic".equals(transactionTypeHeader)) {
                // Glowroot-Transaction-Type header currently only accepts "Synthetic", in order to
                // prevent spamming of transaction types, which could cause some issues
                transactionType = transactionTypeHeader;
                setWithCoreMaxPriority = true;
            } else if (transactionTypeOverride != null) {
                transactionType = transactionTypeOverride;
            } else {
                transactionType = "Web";
            }
            TraceEntry traceEntry = context.startTransaction(transactionType, requestUri,
                    messageSupplier, timerName);
            if (setWithCoreMaxPriority) {
                context.setTransactionType(transactionType, Priority.CORE_MAX);
            }
            context.setServletRequestInfo(messageSupplier);
            // Glowroot-Transaction-Name header is useful for automated tests which want to send a
            // more specific name for the transaction
            String transactionNameOverride = request.getHeader("Glowroot-Transaction-Name");
            if (transactionNameOverride != null) {
                context.setTransactionName(transactionNameOverride, Priority.CORE_MAX);
            }
            if (user != null) {
                context.setTransactionUser(user, Priority.CORE_PLUGIN);
            }
            return traceEntry;
        }
    }

    @Pointcut(className = "javax.servlet.Filter", methodName = "doFilter",
            methodParameterTypes = {"javax.servlet.ServletRequest", "javax.servlet.ServletResponse",
                    "javax.servlet.FilterChain"},
            nestingGroup = "outer-servlet-or-filter", timerName = "http request")
    public static class DoFilterAdvice {
        @OnBefore
        public static @Nullable TraceEntry onBefore(OptionalThreadContext context,
                @BindParameter @Nullable Object req, @BindClassMeta RequestInvoker requestInvoker) {
            return ServiceAdvice.onBeforeCommon(context, req, null, requestInvoker);
        }
        @OnReturn
        public static void onReturn(OptionalThreadContext context,
                @BindTraveler @Nullable TraceEntry traceEntry,
                @BindParameter @Nullable Object req,
                @BindParameter @Nullable Object res,
                @BindClassMeta ResponseInvoker responseInvoker) {
            ServiceAdvice.onReturn(context, traceEntry, req, res, responseInvoker);
        }
        @OnThrow
        public static void onThrow(@BindThrowable Throwable t, OptionalThreadContext context,
                @BindTraveler @Nullable TraceEntry traceEntry,
                @BindParameter @Nullable Object req,
                @BindParameter @Nullable Object res) {
            ServiceAdvice.onThrow(t, context, traceEntry, req, res);
        }
    }

    @Pointcut(className = "org.eclipse.jetty.server.Handler"
            + "|wiremock.org.eclipse.jetty.server.Handler",
            subTypeRestriction = "/(?!org\\.eclipse\\.jetty.)"
                    + "(?!wiremock.org\\.eclipse\\.jetty.).*/",
            methodName = "handle",
            methodParameterTypes = {"java.lang.String",
                    "org.eclipse.jetty.server.Request|wiremock.org.eclipse.jetty.server.Request",
                    "javax.servlet.http.HttpServletRequest",
                    "javax.servlet.http.HttpServletResponse"},
            nestingGroup = "outer-servlet-or-filter", timerName = "http request")
    public static class JettyHandlerAdvice {
        @OnBefore
        public static @Nullable TraceEntry onBefore(OptionalThreadContext context,
                @SuppressWarnings("unused") @BindParameter @Nullable String target,
                @SuppressWarnings("unused") @BindParameter @Nullable Object baseRequest,
                @BindParameter @Nullable Object req, @BindClassMeta RequestInvoker requestInvoker) {
            return ServiceAdvice.onBeforeCommon(context, req, null, requestInvoker);
        }
        @OnReturn
        public static void onReturn(OptionalThreadContext context,
                @BindTraveler @Nullable TraceEntry traceEntry,
                @SuppressWarnings("unused") @BindParameter @Nullable String target,
                @SuppressWarnings("unused") @BindParameter @Nullable Object baseRequest,
                @BindParameter @Nullable Object req,
                @BindParameter @Nullable Object res,
                @BindClassMeta ResponseInvoker responseInvoker) {
            ServiceAdvice.onReturn(context, traceEntry, req, res, responseInvoker);
        }
        @OnThrow
        public static void onThrow(@BindThrowable Throwable t, OptionalThreadContext context,
                @BindTraveler @Nullable TraceEntry traceEntry,
                @SuppressWarnings("unused") @BindParameter @Nullable String target,
                @SuppressWarnings("unused") @BindParameter @Nullable Object baseRequest,
                @BindParameter @Nullable Object req,
                @BindParameter @Nullable Object res) {
            ServiceAdvice.onThrow(t, context, traceEntry, req, res);
        }
    }

    // this pointcut makes sure to only set the transaction type to WireMock if WireMock is the
    // first servlet encountered
    @Pointcut(className = "javax.servlet.Servlet",
            subTypeRestriction = "com.github.tomakehurst.wiremock.jetty9"
                    + ".JettyHandlerDispatchingServlet",
            methodName = "service",
            methodParameterTypes = {"javax.servlet.ServletRequest",
                    "javax.servlet.ServletResponse"},
            nestingGroup = "outer-servlet-or-filter", timerName = "http request", order = -1)
    public static class WireMockAdvice {
        @OnBefore
        public static @Nullable TraceEntry onBefore(OptionalThreadContext context,
                @BindParameter @Nullable Object req, @BindClassMeta RequestInvoker requestInvoker) {
            return ServiceAdvice.onBeforeCommon(context, req, "WireMock", requestInvoker);
        }
        @OnReturn
        public static void onReturn(OptionalThreadContext context,
                @BindTraveler @Nullable TraceEntry traceEntry,
                @BindParameter @Nullable Object req,
                @BindParameter @Nullable Object res,
                @BindClassMeta ResponseInvoker responseInvoker) {
            ServiceAdvice.onReturn(context, traceEntry, req, res, responseInvoker);
        }
        @OnThrow
        public static void onThrow(@BindThrowable Throwable t, OptionalThreadContext context,
                @BindTraveler @Nullable TraceEntry traceEntry,
                @BindParameter @Nullable Object req,
                @BindParameter @Nullable Object res) {
            ServiceAdvice.onThrow(t, context, traceEntry, req, res);
        }
    }

    @Pointcut(className = "javax.servlet.http.HttpServletResponse", methodName = "sendError",
            methodParameterTypes = {"int", ".."}, nestingGroup = "servlet-inner-call")
    public static class SendErrorAdvice {
        // wait until after because sendError throws IllegalStateException if the response has
        // already been committed
        @OnAfter
        public static void onAfter(ThreadContext context, @BindParameter int statusCode) {
            ServletMessageSupplier messageSupplier =
                    (ServletMessageSupplier) context.getServletRequestInfo();
            if (messageSupplier != null) {
                messageSupplier.setResponseCode(statusCode);
            }
            if (captureAsError(statusCode)) {
                FastThreadLocal.Holder errorMessageHolder =
                        sendError.getHolder();
                if (errorMessageHolder.get() == null) {
                    context.addErrorEntry("sendError, HTTP status code " + statusCode);
                    errorMessageHolder.set("sendError, HTTP status code " + statusCode);
                }
            }
        }

        private static boolean captureAsError(int statusCode) {
            return statusCode >= 500
                    || ServletPluginProperties.traceErrorOn4xxResponseCode() && statusCode >= 400;
        }
    }

    @Pointcut(className = "javax.servlet.http.HttpServletResponse", methodName = "sendRedirect",
            methodParameterTypes = {"java.lang.String"}, nestingGroup = "servlet-inner-call")
    public static class SendRedirectAdvice {
        // wait until after because sendError throws IllegalStateException if the response has
        // already been committed
        @OnAfter
        public static void onAfter(ThreadContext context, @BindReceiver Object response,
                @BindParameter @Nullable String location,
                @BindClassMeta ResponseInvoker responseInvoker) {
            ServletMessageSupplier messageSupplier =
                    (ServletMessageSupplier) context.getServletRequestInfo();
            if (messageSupplier != null) {
                messageSupplier.setResponseCode(302);
                if (responseInvoker.hasGetHeaderMethod()) {
                    // get the header as set by the container (e.g. after it converts relative to
                    // absolute path)
                    String header = responseInvoker.getHeader(response, "Location");
                    messageSupplier.addResponseHeader("Location", header);
                } else if (location != null) {
                    messageSupplier.addResponseHeader("Location", location);
                }
            }
        }
    }

    @Pointcut(className = "javax.servlet.http.HttpServletResponse", methodName = "setStatus",
            methodParameterTypes = {"int", ".."}, nestingGroup = "servlet-inner-call")
    public static class SetStatusAdvice {
        // wait until after because sendError throws IllegalStateException if the response has
        // already been committed
        @OnAfter
        public static void onAfter(ThreadContext context, @BindParameter int statusCode) {
            ServletMessageSupplier messageSupplier =
                    (ServletMessageSupplier) context.getServletRequestInfo();
            if (messageSupplier != null) {
                messageSupplier.setResponseCode(statusCode);
            }
            if (SendErrorAdvice.captureAsError(statusCode)) {
                FastThreadLocal.Holder errorMessageHolder =
                        sendError.getHolder();
                if (errorMessageHolder.get() == null) {
                    context.addErrorEntry("setStatus, HTTP status code " + statusCode);
                    errorMessageHolder.set("setStatus, HTTP status code " + statusCode);
                }
            }
        }
    }

    @Pointcut(className = "javax.servlet.http.HttpServletRequest", methodName = "getUserPrincipal",
            methodParameterTypes = {}, methodReturnType = "java.security.Principal",
            nestingGroup = "servlet-inner-call")
    public static class GetUserPrincipalAdvice {
        @OnReturn
        public static void onReturn(@BindReturn @Nullable Principal principal,
                ThreadContext context) {
            if (principal != null) {
                context.setTransactionUser(principal.getName(), Priority.CORE_PLUGIN);
            }
        }
    }

    @Pointcut(className = "javax.servlet.http.HttpServletRequest", methodName = "getSession",
            methodParameterTypes = {}, nestingGroup = "servlet-inner-call")
    public static class GetSessionAdvice {
        @OnReturn
        public static void onReturn(@BindReturn @Nullable HttpSession session,
                ThreadContext context) {
            if (session == null) {
                return;
            }
            if (ServletPluginProperties.sessionUserAttributeIsId()) {
                context.setTransactionUser(session.getId(), Priority.CORE_PLUGIN);
            }
            if (ServletPluginProperties.captureSessionAttributeNamesContainsId()) {
                ServletMessageSupplier messageSupplier =
                        (ServletMessageSupplier) context.getServletRequestInfo();
                if (messageSupplier != null) {
                    messageSupplier.putSessionAttributeChangedValue(
                            ServletPluginProperties.HTTP_SESSION_ID_ATTR, session.getId());
                }
            }
        }
    }

    @Pointcut(className = "javax.servlet.http.HttpServletRequest", methodName = "getSession",
            methodParameterTypes = {"boolean"}, nestingGroup = "servlet-inner-call")
    public static class GetSessionOneArgAdvice {
        @OnReturn
        public static void onReturn(@BindReturn @Nullable HttpSession session,
                ThreadContext context) {
            GetSessionAdvice.onReturn(session, context);
        }
    }

    @Pointcut(className = "javax.servlet.Servlet", methodName = "init",
            methodParameterTypes = {"javax.servlet.ServletConfig"})
    public static class ServiceInitAdvice {
        @OnBefore
        public static void onBefore() {
            ContainerStartup.initPlatformMBeanServer();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy