/*
* Copyright 2011-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.servlet;
import java.security.Principal;
import java.util.Enumeration;
import java.util.Map;
import javax.annotation.Nullable;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import org.glowroot.agent.plugin.api.Agent;
import org.glowroot.agent.plugin.api.AuxThreadContext;
import org.glowroot.agent.plugin.api.MessageSupplier;
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.util.FastThreadLocal;
import org.glowroot.agent.plugin.api.weaving.BindParameter;
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.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;
// only the calls to the top-most Filter and to the top-most Servlet are captured
//
// this plugin is careful not to rely on request or session objects being thread-safe
public class ServletAspect {
// the life of this thread local is tied to the life of the topLevel thread local
// it is only created if the topLevel thread local exists, and it is cleared when topLevel
// thread local is cleared
private static final FastThreadLocal</*@Nullable*/ String> sendError =
new FastThreadLocal</*@Nullable*/ String>();
@Shim("javax.servlet.http.HttpServletRequest")
public interface HttpServletRequest {
@Shim("javax.servlet.http.HttpSession getSession(boolean)")
@Nullable
HttpSession glowroot$getSession(boolean create);
@Nullable
String getRequestURI();
@Nullable
String getQueryString();
@Nullable
String getMethod();
@Nullable
Enumeration</*@Nullable*/ String> getHeaderNames();
@Nullable
Enumeration</*@Nullable*/ String> getHeaders(String name);
@Nullable
String getHeader(String name);
@Nullable
Map</*@Nullable*/ String, /*@Nullable*/ String /*@Nullable*/[]> getParameterMap();
@Nullable
Enumeration<? extends /*@Nullable*/Object> getParameterNames();
@Nullable
String /*@Nullable*/[] getParameterValues(String name);
@Nullable
Object getAttribute(String name);
void removeAttribute(String name);
// not currently used by servlet plugin, but here to be available for other plugins
@Nullable
String getRemoteAddr();
}
@Shim("javax.servlet.http.HttpSession")
public interface HttpSession {
@Nullable
Object getAttribute(String name);
@Nullable
Enumeration<? extends /*@Nullable*/Object> 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) {
if (context.getServletMessageSupplier() != 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());
ImmutableMap<String, Object> requestHeaders =
DetailCapture.captureRequestHeaders(request);
if (session == null) {
messageSupplier = new ServletMessageSupplier(requestMethod, requestUri,
requestQueryString, requestHeaders, ImmutableMap.<String, String>of());
} else {
ImmutableMap<String, String> sessionAttributes =
HttpSessions.getSessionAttributes(session);
messageSupplier = new ServletMessageSupplier(requestMethod, requestUri,
requestQueryString, requestHeaders, sessionAttributes);
}
String user = null;
if (session != null) {
String sessionUserAttributePath =
ServletPluginProperties.sessionUserAttributePath();
if (!sessionUserAttributePath.isEmpty()) {
// capture user now, don't use a lazy supplier
user = HttpSessions.getSessionAttributeTextValue(session,
sessionUserAttributePath);
}
}
TraceEntry traceEntry =
context.startTransaction("Web", requestUri, messageSupplier, timerName);
context.setServletMessageSupplier(messageSupplier);
// Glowroot-Transaction-Type header currently only accepts "Synthetic", in order to
// prevent spamming of transaction types, which could cause some issues
String transactionTypeOverride = request.getHeader("Glowroot-Transaction-Type");
if (transactionTypeOverride != null && transactionTypeOverride.equals("Synthetic")) {
context.setTransactionType(transactionTypeOverride, Priority.CORE_MAX);
}
// 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;
}
@OnReturn
public static void onReturn(OptionalThreadContext context,
@BindTraveler @Nullable TraceEntry traceEntry) {
if (traceEntry == null) {
return;
}
FastThreadLocal.Holder</*@Nullable*/ String> errorMessageHolder = sendError.getHolder();
String errorMessage = errorMessageHolder.get();
if (errorMessage != null) {
traceEntry.endWithError(errorMessage);
errorMessageHolder.set(null);
} else {
traceEntry.end();
}
context.setServletMessageSupplier(null);
}
@OnThrow
public static void onThrow(@BindThrowable Throwable t, OptionalThreadContext context,
@BindTraveler @Nullable TraceEntry traceEntry) {
if (traceEntry == null) {
return;
}
// ignoring potential sendError since this seems worse
sendError.set(null);
traceEntry.endWithError(t);
context.setServletMessageSupplier(null);
}
}
@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 request) {
return ServiceAdvice.onBefore(context, request);
}
@OnReturn
public static void onReturn(OptionalThreadContext context,
@BindTraveler @Nullable TraceEntry traceEntry) {
ServiceAdvice.onReturn(context, traceEntry);
}
@OnThrow
public static void onThrow(@BindThrowable Throwable t, OptionalThreadContext context,
@BindTraveler @Nullable TraceEntry traceEntry) {
ServiceAdvice.onThrow(t, context, traceEntry);
}
}
@Pointcut(className = "org.eclipse.jetty.server.Handler", methodName = "handle",
methodParameterTypes = {"java.lang.String", "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 request) {
return ServiceAdvice.onBefore(context, request);
}
@OnReturn
public static void onReturn(OptionalThreadContext context,
@BindTraveler @Nullable TraceEntry traceEntry) {
ServiceAdvice.onReturn(context, traceEntry);
}
@OnThrow
public static void onThrow(@BindThrowable Throwable t, OptionalThreadContext context,
@BindTraveler @Nullable TraceEntry traceEntry) {
ServiceAdvice.onThrow(t, context, traceEntry);
}
}
@Pointcut(className = "javax.servlet.http.HttpServletResponse", methodName = "sendError",
methodParameterTypes = {"int", ".."}, nestingGroup = "servlet-inner-call")
public static class SendErrorAdvice {
@OnAfter
public static void onAfter(ThreadContext context, @BindParameter Integer statusCode) {
FastThreadLocal.Holder</*@Nullable*/ String> errorMessageHolder = sendError.getHolder();
// only capture 5xx server errors
if (statusCode >= 500 && errorMessageHolder.get() == null) {
context.addErrorEntry("sendError, HTTP status code " + statusCode);
errorMessageHolder.set("sendError, HTTP status code " + statusCode);
}
}
}
@Pointcut(className = "javax.servlet.http.HttpServletResponse", methodName = "setStatus",
methodParameterTypes = {"int", ".."}, nestingGroup = "servlet-inner-call")
public static class SetStatusAdvice {
// using @IsEnabled like this avoids ThreadContext lookup for common case
@IsEnabled
public static boolean isEnabled(@BindParameter Integer statusCode) {
return statusCode >= 500;
}
@OnAfter
public static void onAfter(ThreadContext context, @BindParameter Integer statusCode) {
FastThreadLocal.Holder</*@Nullable*/ String> 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()) {
MessageSupplier messageSupplier = context.getServletMessageSupplier();
if (messageSupplier instanceof ServletMessageSupplier) {
((ServletMessageSupplier) 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);
}
}
}