package rocks.inspectit.agent.java.sensor.method.http;
import java.lang.management.ThreadMXBean;
import java.sql.Timestamp;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rocks.inspectit.agent.java.config.impl.RegisteredSensorConfig;
import rocks.inspectit.agent.java.core.ICoreService;
import rocks.inspectit.agent.java.core.IPlatformManager;
import rocks.inspectit.agent.java.hooking.IMethodHook;
import rocks.inspectit.agent.java.sensor.method.timer.TimerHook;
import rocks.inspectit.agent.java.util.ClassUtil;
import rocks.inspectit.agent.java.util.StringConstraint;
import rocks.inspectit.agent.java.util.ThreadLocalStack;
import rocks.inspectit.agent.java.util.Timer;
import rocks.inspectit.shared.all.communication.data.HttpTimerData;
/**
* The hook implementation for the http sensor. It uses the {@link ThreadLocalStack} class to save
* the time when the method was called.
* <p>
* This hook measures timer data like the {@link TimerHook} but in addition provides Http
* information. Another difference is that we ensure that only one Http metric per request is
* created.
*
* @author Stefan Siegl
*
*/
public class HttpHook implements IMethodHook {
/**
* The logger of this class. Initialized manually.
*/
private static final Logger LOG = LoggerFactory.getLogger(HttpHook.class);
/**
* The stack containing the start time values.
*/
private final ThreadLocalStack<Double> timeStack = new ThreadLocalStack<Double>();
/**
* The timer used for accurate measuring.
*/
private final Timer timer;
/**
* The Platform manager.
*/
private final IPlatformManager platformManager;
/**
* The thread MX bean.
*/
private final ThreadMXBean threadMXBean;
/**
* Defines if the thread CPU time is supported.
*/
private boolean threadCPUTimeJMXAvailable = false;
/**
* Defines if the thread CPU time is enabled.
*/
private boolean threadCPUTimeEnabled = false;
/**
* The stack containing the start time values.
*/
private final ThreadLocalStack<Long> threadCpuTimeStack = new ThreadLocalStack<Long>();
/**
* Extractor for Http information.
*/
private final HttpInformationExtractor extractor;
/**
* Configuration setting if session data should be captured.
*/
private final boolean captureSessionData;
/**
* Expected name of the HttpServletRequest interface.
*/
private static final String HTTP_SERVLET_REQUEST_CLASS = "javax.servlet.http.HttpServletRequest";
/**
* Expected name of the HttpServletResponse interface.
*/
private static final String HTTP_SERVLET_RESPONSE_CLASS = "javax.servlet.http.HttpServletResponse";
/**
* Whitelist that contains all classes that we already checked if they provide
* HttpServletMetrics and do. We are talking about the class of the ServletRequest here. This
* list is extended if a new Class that provides this interface is found.
*/
private static final CopyOnWriteArrayList<Class<?>> HTTP_REQUEST_WHITE_LIST = new CopyOnWriteArrayList<Class<?>>();
/**
* Blacklist that contains all classes that we already checked if they provide
* HttpServletMetrics and do not. We are talking about the class of the ServletRequest here.
* This list is extended if a new Class that does not provides this interface is found.
*/
private static final CopyOnWriteArrayList<Class<?>> HTTP_REQUEST_BLACK_LIST = new CopyOnWriteArrayList<Class<?>>();
/**
* Whitelist that contains all classes that we already checked if they provide
* HttpServletMetrics and do. We are talking about the class of the ServletResponse here. This
* list is extended if a new Class that provides this interface is found.
*/
private static final CopyOnWriteArrayList<Class<?>> HTTP_RESPONSE_WHITE_LIST = new CopyOnWriteArrayList<Class<?>>();
/**
* Blacklist that contains all classes that we already checked if they provide
* HttpServletMetrics and do not. We are talking about the class of the ServletResponse here.
* This list is extended if a new Class that does not provides this interface is found.
*/
private static final CopyOnWriteArrayList<Class<?>> HTTP_RESPONSE_BLACK_LIST = new CopyOnWriteArrayList<Class<?>>();
/**
* Helps us to ensure that we only store on http metric per request.
*/
private final StartEndMarker refMarker = new StartEndMarker();
/**
* This constructor creates a new instance of a <code>HttpHook</code>.
*
* @param timer
* The timer
* @param platformManager
* The Platform manager
* @param threadMXBean
* the threadMx Bean for cpu timing
* @param parameters
* the map containing the configuration parameters
*/
public HttpHook(Timer timer, IPlatformManager platformManager, Map<String, Object> parameters, ThreadMXBean threadMXBean) {
this.timer = timer;
this.platformManager = platformManager;
this.threadMXBean = threadMXBean;
this.extractor = new HttpInformationExtractor(new StringConstraint(parameters));
if ("true".equals(parameters.get("sessioncapture"))) {
if (LOG.isDebugEnabled()) {
LOG.debug("Enabling session capturing for the http sensor");
}
captureSessionData = true;
} else {
captureSessionData = false;
}
try {
// if it is even supported by this JVM
threadCPUTimeJMXAvailable = threadMXBean.isThreadCpuTimeSupported();
if (threadCPUTimeJMXAvailable) {
// check if its enabled
threadCPUTimeEnabled = threadMXBean.isThreadCpuTimeEnabled();
if (!threadCPUTimeEnabled) {
// try to enable it
threadMXBean.setThreadCpuTimeEnabled(true);
// check again now if it is enabled now
threadCPUTimeEnabled = threadMXBean.isThreadCpuTimeEnabled();
}
}
} catch (RuntimeException e) {
// catching the runtime exceptions which could be thrown by the
// above statements.
LOG.warn("Your environment does not support to capture CPU timings.");
}
}
/**
* {@inheritDoc}
*/
@Override
public void beforeBody(long methodId, long sensorTypeId, Object object, Object[] parameters, RegisteredSensorConfig rsc) {
// We mark the invocation of the first servlet and the calls from within it. This way we
// gather information just once (from the first one) and avoid overhead and inconclusive
// information.
// Only during the first invocation, we make preparations.
if (!refMarker.isMarkerSet()) {
// We expect the first parameter to be of the type javax.servlet.ServletRequest
// If this is not the case then the configuration was wrong.
if (parameters.length >= 2) {
Object httpServletRequest = parameters[0];
Object httpServletResponse = parameters[1];
Class<?> servletRequestClass = httpServletRequest.getClass();
Class<?> servletResponseClass = httpServletResponse.getClass();
// Check if metrics interface provided
if (providesHttpRequestMetrics(servletRequestClass) && providesHttpResponseMetrics(servletResponseClass)) {
// We must take the time as soon as we know that we are dealing with an http
// timer. We cannot do that after we read the information from the request
// object because these methods could be instrumented and thus the whole http
// timer would be off - resulting in very strange results.
timeStack.push(new Double(timer.getCurrentTime()));
if (threadCPUTimeEnabled) {
threadCpuTimeStack.push(Long.valueOf(threadMXBean.getCurrentThreadCpuTime()));
}
// Mark first invocation
refMarker.markCall();
}
}
} else {
// Mark sub invocation, first already marked.
refMarker.markCall();
}
}
/**
* {@inheritDoc}
*/
@Override
public void firstAfterBody(long methodId, long sensorTypeId, Object object, Object[] parameters, Object result, RegisteredSensorConfig rsc) {
// no invocation marked -> skip
if (!refMarker.isMarkerSet()) {
return;
}
// remove mark from sub call
refMarker.markEndCall();
if (refMarker.matchesFirst()) {
// Get the timer and store it.
timeStack.push(new Double(timer.getCurrentTime()));
if (threadCPUTimeEnabled) {
threadCpuTimeStack.push(Long.valueOf(threadMXBean.getCurrentThreadCpuTime()));
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void secondAfterBody(ICoreService coreService, long methodId, long sensorTypeId, Object object, Object[] parameters, Object result, RegisteredSensorConfig rsc) {
// check if in the right(first) invocation
if (refMarker.isMarkerSet() && refMarker.matchesFirst()) {
// call ended, remove the marker.
refMarker.remove();
// double check if nothing changed
if (parameters.length >= 2) {
Object httpServletRequest = parameters[0];
Object httpServletResponse = parameters[1];
Class<?> servletRequestClass = httpServletRequest.getClass();
Class<?> servletResponseClass = httpServletResponse.getClass();
// double check interface
if (providesHttpRequestMetrics(servletRequestClass) && providesHttpResponseMetrics(servletResponseClass)) {
double endTime = timeStack.pop().doubleValue();
double startTime = timeStack.pop().doubleValue();
double duration = endTime - startTime;
// default setting to a negative number
double cpuDuration = -1.0d;
if (threadCPUTimeEnabled) {
long cpuEndTime = threadCpuTimeStack.pop().longValue();
long cpuStartTime = threadCpuTimeStack.pop().longValue();
cpuDuration = (cpuEndTime - cpuStartTime) / 1000000.0d;
}
long platformId = platformManager.getPlatformId();
Timestamp timestamp = new Timestamp(System.currentTimeMillis() - Math.round(duration));
// Creating return data object
HttpTimerData data = new HttpTimerData();
data.setPlatformIdent(platformId);
data.setMethodIdent(methodId);
data.setSensorTypeIdent(sensorTypeId);
data.setTimeStamp(timestamp);
data.setDuration(duration);
data.calculateMin(duration);
data.calculateMax(duration);
data.setCpuDuration(cpuDuration);
data.calculateCpuMax(cpuDuration);
data.calculateCpuMin(cpuDuration);
data.setCount(1L);
// Include additional http information
data.getHttpInfo().setUri(extractor.getRequestUri(servletRequestClass, httpServletRequest));
data.getHttpInfo().setRequestMethod(extractor.getRequestMethod(servletRequestClass, httpServletRequest));
data.getHttpInfo().setScheme(extractor.getScheme(servletRequestClass, httpServletRequest));
data.getHttpInfo().setServerName(extractor.getServerName(servletRequestClass, httpServletRequest));
data.getHttpInfo().setServerPort(extractor.getServerPort(servletRequestClass, httpServletRequest));
data.getHttpInfo().setQueryString(extractor.getQueryString(servletRequestClass, httpServletRequest));
data.setParameters(extractor.getParameterMap(servletRequestClass, httpServletRequest));
data.setAttributes(extractor.getAttributes(servletRequestClass, httpServletRequest));
data.setHeaders(extractor.getHeaders(servletRequestClass, httpServletRequest));
if (captureSessionData) {
data.setSessionAttributes(extractor.getSessionAttributes(servletRequestClass, httpServletRequest));
}
// Include HTTP response information
data.setHttpResponseStatus(extractor.getResponseStatus(servletResponseClass, httpServletResponse));
boolean charting = Boolean.TRUE.equals(rsc.getSettings().get("charting"));
data.setCharting(charting);
// returning gathered information
coreService.addMethodSensorData(sensorTypeId, methodId, String.valueOf(startTime), data);
}
}
}
}
/**
* Checks if the given Class is realizing the HttpServletRequest interface directly or
* indirectly. Only if this interface is realized, we can get Http metric information.
*
* @param c
* The class to check
* @return whether or not the HttpServletRequest interface is realized.
*/
private boolean providesHttpRequestMetrics(Class<?> c) {
return implementsInterface(c, HTTP_SERVLET_REQUEST_CLASS, HTTP_REQUEST_WHITE_LIST, HTTP_REQUEST_BLACK_LIST);
}
/**
* Checks if the given Class is realizing the HttpServletResponse interface directly or
* indirectly. Only if this interface is realized, we can get Http metric information.
*
* @param c
* The class to check
* @return whether or not the HttpServletResponse interface is realized.
*/
private boolean providesHttpResponseMetrics(Class<?> c) {
return implementsInterface(c, HTTP_SERVLET_RESPONSE_CLASS, HTTP_RESPONSE_WHITE_LIST, HTTP_RESPONSE_BLACK_LIST);
}
/**
* Checks if the given class implements the given interface.
*
* @param c
* The class to check.
* @param interfaceName
* The name of the target interface.
* @param whiteList
* A whitelist (cache) of classes from which we know that they implement the
* interface.
* @param blackList
* A blacklist (cache) of classes from which we know that they do not implement the
* interface.
* @return True, if the given class implements the given interface.
*/
private boolean implementsInterface(Class<?> c, String interfaceName, CopyOnWriteArrayList<Class<?>> whiteList, CopyOnWriteArrayList<Class<?>> blackList) {
if (whiteList.contains(c)) {
return true;
}
if (blackList.contains(c)) {
return false;
}
Class<?> intf = ClassUtil.searchInterface(c, interfaceName);
if (null != intf) {
whiteList.addIfAbsent(c);
return true;
} else {
blackList.addIfAbsent(c);
return false;
}
}
}