package rocks.inspectit.agent.java.sensor.method.invocationsequence; import java.net.ConnectException; import java.sql.Timestamp; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.collections.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rocks.inspectit.agent.java.buffer.IBufferStrategy; import rocks.inspectit.agent.java.config.IPropertyAccessor; import rocks.inspectit.agent.java.config.impl.RegisteredSensorConfig; import rocks.inspectit.agent.java.core.ICoreService; import rocks.inspectit.agent.java.core.IObjectStorage; import rocks.inspectit.agent.java.core.IPlatformManager; import rocks.inspectit.agent.java.core.ListListener; import rocks.inspectit.agent.java.hooking.IConstructorHook; import rocks.inspectit.agent.java.hooking.IMethodHook; import rocks.inspectit.agent.java.sdk.opentracing.internal.impl.TracerImpl; import rocks.inspectit.agent.java.sending.ISendingStrategy; import rocks.inspectit.agent.java.sensor.exception.ExceptionSensor; import rocks.inspectit.agent.java.sensor.method.IMethodSensor; import rocks.inspectit.agent.java.sensor.method.jdbc.ConnectionSensor; import rocks.inspectit.agent.java.sensor.method.jdbc.PreparedStatementParameterSensor; import rocks.inspectit.agent.java.sensor.method.jdbc.PreparedStatementSensor; import rocks.inspectit.agent.java.sensor.method.logging.Log4JLoggingSensor; import rocks.inspectit.agent.java.sensor.method.remote.client.http.ApacheHttpClientV40Sensor; import rocks.inspectit.agent.java.sensor.method.remote.client.http.JettyHttpClientV61Sensor; import rocks.inspectit.agent.java.sensor.method.remote.client.http.SpringRestTemplateClientSensor; import rocks.inspectit.agent.java.sensor.method.remote.client.http.UrlConnectionSensor; import rocks.inspectit.agent.java.sensor.method.remote.client.mq.JmsRemoteClientSensor; import rocks.inspectit.agent.java.sensor.method.remote.server.http.JavaHttpRemoteServerSensor; import rocks.inspectit.agent.java.sensor.method.remote.server.manual.ManualRemoteServerSensor; import rocks.inspectit.agent.java.sensor.method.remote.server.mq.JmsListenerRemoteServerSensor; import rocks.inspectit.agent.java.tracing.core.transformer.SpanContextTransformer; 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.DefaultData; import rocks.inspectit.shared.all.communication.MethodSensorData; import rocks.inspectit.shared.all.communication.SystemSensorData; import rocks.inspectit.shared.all.communication.data.ExceptionSensorData; import rocks.inspectit.shared.all.communication.data.HttpTimerData; import rocks.inspectit.shared.all.communication.data.InvocationSequenceData; import rocks.inspectit.shared.all.communication.data.JmxSensorValueData; import rocks.inspectit.shared.all.communication.data.LoggingData; import rocks.inspectit.shared.all.communication.data.ParameterContentData; import rocks.inspectit.shared.all.communication.data.SqlStatementData; import rocks.inspectit.shared.all.communication.data.TimerData; import rocks.inspectit.shared.all.instrumentation.config.impl.MethodSensorTypeConfig; import rocks.inspectit.shared.all.instrumentation.config.impl.PlatformSensorTypeConfig; import rocks.inspectit.shared.all.tracing.data.AbstractSpan; /** * The invocation sequence hook stores the record of the invocation sequences in a * {@link ThreadLocal} object. * <p> * This hook implements the {@link ICoreService} interface which simulates the core service to all * other hooks which are called during the execution of this invocation. The * <code>defaultCoreService</code> field is used to delegate some calls directly to the original * core service and later sending of the data to the server. * * @author Patrice Bouillet * */ public class InvocationSequenceHook implements IMethodHook, IConstructorHook, ICoreService { /** * The logger of this class. Initialized manually. */ private static final Logger LOG = LoggerFactory.getLogger(InvocationSequenceHook.class); /** * List of remote sensor names needed for * {@link #removeDueToNoData(RegisteredSensorConfig, InvocationSequenceData)}. */ private static final Set<String> REMOVE_SENSOR_CLASS_NAMES = new HashSet<String>( Arrays.asList(ApacheHttpClientV40Sensor.class.getName(), JettyHttpClientV61Sensor.class.getName(), SpringRestTemplateClientSensor.class.getName(), UrlConnectionSensor.class.getName(), JavaHttpRemoteServerSensor.class.getName(), JmsRemoteClientSensor.class.getName(), JmsListenerRemoteServerSensor.class.getName(), ManualRemoteServerSensor.class.getName())); /** * The Platform manager. */ private final IPlatformManager platformManager; /** * The real core service needed to delegate spans to. */ private final ICoreService realCoreService; /** * Current tracer for the spans. */ private final TracerImpl tracer; /** * The property accessor. */ private final IPropertyAccessor propertyAccessor; /** * The {@link ThreadLocal} object which holds an {@link InvocationSequenceData} object if an * invocation record is started. */ private final ThreadLocal<InvocationSequenceData> threadLocalInvocationData = new ThreadLocal<InvocationSequenceData>(); /** * Stores the value of the method ID in the {@link ThreadLocal} object. Used to identify the * correct start and end of the record. */ private final ThreadLocal<Long> invocationStartId = new ThreadLocal<Long>(); /** * Stores the count of the of the starting method being called in the same invocation sequence * so that closing is done on the right end. */ private final ThreadLocal<Long> invocationStartIdCount = new ThreadLocal<Long>(); /** * The timer used for accurate measuring. */ private final Timer timer; /** * The stack containing the start time values. */ private final ThreadLocalStack<Double> timeStack = new ThreadLocalStack<Double>(); /** * Saves the min duration for faster access of the values. */ private final Map<Long, Double> minDurationMap = new HashMap<Long, Double>(); /** * The StringConstraint to ensure a maximum length of strings. */ private final StringConstraint strConstraint; /** * If enhanced exception sensor is ON. */ private final boolean enhancedExceptionSensor; /** * The default constructor is initialized with a reference to the original {@link ICoreService} * implementation to delegate all calls to if the data needs to be sent. * * @param timer * The timer. * @param platformManager * The Platform manager. * @param coreService * The real core service needed to delegate spans to. * @param tracer * Current tracer for the spans. * @param propertyAccessor * The property accessor. * @param param * Additional parameters. * @param enhancedExceptionSensor * If enhanced exception sensor is ON. */ public InvocationSequenceHook(Timer timer, IPlatformManager platformManager, ICoreService coreService, TracerImpl tracer, IPropertyAccessor propertyAccessor, Map<String, Object> param, boolean enhancedExceptionSensor) { this.timer = timer; this.platformManager = platformManager; this.realCoreService = coreService; this.tracer = tracer; this.propertyAccessor = propertyAccessor; this.strConstraint = new StringConstraint(param); this.enhancedExceptionSensor = enhancedExceptionSensor; } /** * {@inheritDoc} */ @Override public void beforeBody(long methodId, long sensorTypeId, Object object, Object[] parameters, RegisteredSensorConfig rsc) { if (skip(rsc)) { return; } long platformId = platformManager.getPlatformId(); Timestamp timestamp = new Timestamp(System.currentTimeMillis()); if (null == threadLocalInvocationData.get()) { // the sensor type is only available in the beginning of the // sequence trace // save the start time timeStack.push(new Double(timer.getCurrentTime())); // no invocation tracer is currently started, so we do that now. InvocationSequenceData invocationSequenceData = new InvocationSequenceData(timestamp, platformId, sensorTypeId, methodId); threadLocalInvocationData.set(invocationSequenceData); invocationStartId.set(Long.valueOf(methodId)); invocationStartIdCount.set(Long.valueOf(1)); } else { if (methodId == invocationStartId.get().longValue()) { long count = invocationStartIdCount.get().longValue(); invocationStartIdCount.set(Long.valueOf(count + 1)); } // A subsequent call to the before body method where an // invocation tracer is already started. InvocationSequenceData invocationSequenceData = threadLocalInvocationData.get(); invocationSequenceData.setChildCount(invocationSequenceData.getChildCount() + 1L); InvocationSequenceData nestedInvocationSequenceData = new InvocationSequenceData(timestamp, platformId, invocationSequenceData.getSensorTypeIdent(), methodId); nestedInvocationSequenceData.setStart(timer.getCurrentTime()); nestedInvocationSequenceData.setParentSequence(invocationSequenceData); invocationSequenceData.getNestedSequences().add(nestedInvocationSequenceData); threadLocalInvocationData.set(nestedInvocationSequenceData); } } /** * {@inheritDoc} */ @Override public void firstAfterBody(long methodId, long sensorTypeId, Object object, Object[] parameters, Object result, RegisteredSensorConfig rsc) { if (skip(rsc)) { return; } InvocationSequenceData invocationSequenceData = threadLocalInvocationData.get(); if (null != invocationSequenceData) { if (methodId == invocationStartId.get().longValue()) { long count = invocationStartIdCount.get().longValue(); invocationStartIdCount.set(Long.valueOf(count - 1)); if (0 == (count - 1)) { timeStack.push(new Double(timer.getCurrentTime())); } } } } /** * {@inheritDoc} */ @Override public void secondAfterBody(ICoreService coreService, long methodId, long sensorTypeId, Object object, Object[] parameters, Object result, RegisteredSensorConfig rsc) { if (skip(rsc)) { return; } InvocationSequenceData invocationSequenceData = threadLocalInvocationData.get(); if (null != invocationSequenceData) { // check if some properties need to be accessed and saved if (rsc.isPropertyAccess()) { List<ParameterContentData> parameterContentData = propertyAccessor.getParameterContentData(rsc.getPropertyAccessorList(), object, parameters, result); // crop the content strings of all ParameterContentData for (ParameterContentData contentData : parameterContentData) { contentData.setContent(strConstraint.crop(contentData.getContent())); } } if ((methodId == invocationStartId.get().longValue()) && (0 == invocationStartIdCount.get().longValue())) { double endTime = timeStack.pop().doubleValue(); double startTime = timeStack.pop().doubleValue(); double duration = endTime - startTime; // check if we belong to a span if (tracer.isCurrentContextExisting()) { invocationSequenceData.setSpanIdent(SpanContextTransformer.transformSpanContext(tracer.getCurrentContext())); } // complete the sequence and store the data object in the 'true' // core service so that it can be transmitted to the server. we // just need an arbitrary prefix so that this sequence will // never be overwritten in the core service! if (minDurationMap.containsKey(invocationStartId.get())) { checkForSavingOrNot(coreService, methodId, sensorTypeId, rsc, invocationSequenceData, startTime, endTime, duration); } else { // maybe not saved yet in the map if (rsc.getSettings().containsKey("minduration")) { Long minDuration = (Long) rsc.getSettings().get("minduration"); minDurationMap.put(invocationStartId.get(), minDuration.doubleValue()); checkForSavingOrNot(coreService, methodId, sensorTypeId, rsc, invocationSequenceData, startTime, endTime, duration); } else { invocationSequenceData.setDuration(duration); invocationSequenceData.setStart(startTime); invocationSequenceData.setEnd(endTime); coreService.addMethodSensorData(sensorTypeId, methodId, String.valueOf(startTime), invocationSequenceData); } } threadLocalInvocationData.set(null); } else { // check for the correct id we must be sure that // we are closing the right sequence if (methodId != invocationSequenceData.getMethodIdent()) { return; } // just close the nested sequence and set the correct child count InvocationSequenceData parentSequence = invocationSequenceData.getParentSequence(); // check if we should not include this invocation because of exception delegation, // SQL wrapping or empty logging if (removeDueToExceptionDelegation(rsc, invocationSequenceData) || removeDueToNoData(rsc, invocationSequenceData)) { parentSequence.getNestedSequences().remove(invocationSequenceData); parentSequence.setChildCount(parentSequence.getChildCount() - 1); // but connect all possible children to the parent then we are eliminating one // level here if (CollectionUtils.isNotEmpty(invocationSequenceData.getNestedSequences())) { for (InvocationSequenceData child : invocationSequenceData.getNestedSequences()) { child.setParentSequence(parentSequence); parentSequence.getNestedSequences().add(child); } parentSequence.setChildCount(parentSequence.getChildCount() + invocationSequenceData.getChildCount()); } } else { invocationSequenceData.setEnd(timer.getCurrentTime()); invocationSequenceData.setDuration(invocationSequenceData.getEnd() - invocationSequenceData.getStart()); parentSequence.setChildCount(parentSequence.getChildCount() + invocationSequenceData.getChildCount()); } threadLocalInvocationData.set(parentSequence); } } } /** * Returns if the given {@link InvocationSequenceData} should be removed due to the exception * constructor delegation. * * @param rsc * {@link RegisteredSensorConfig} * @param invocationSequenceData * {@link InvocationSequenceData} to check. * @return True if the invocation should be removed. */ private boolean removeDueToExceptionDelegation(RegisteredSensorConfig rsc, InvocationSequenceData invocationSequenceData) { List<IMethodSensor> sensors = rsc.getMethodSensors(); if (1 == sensors.size()) { MethodSensorTypeConfig methodSensorTypeConfig = sensors.get(0).getSensorTypeConfig(); if (ExceptionSensor.class.getName().equals(methodSensorTypeConfig.getClassName())) { return CollectionUtils.isEmpty(invocationSequenceData.getExceptionSensorDataObjects()); } } return false; } /** * Returns if the given {@link InvocationSequenceData} should be removed due to no data. Can be * in case of * <ul> * <li>the wrapping of the prepared SQL statements. * <li>having an empty logging element (if the logging occurred with a lower logging level than * the configuration) * <li>running remote sensor that provided no span * </ul> * * @param rsc * {@link RegisteredSensorConfig} * @param invocationSequenceData * {@link InvocationSequenceData} to check. * @return True if the invocation should be removed. */ private boolean removeDueToNoData(RegisteredSensorConfig rsc, InvocationSequenceData invocationSequenceData) { List<IMethodSensor> sensors = rsc.getMethodSensors(); int sensorsSize = sensors.size(); if ((1 == sensorsSize) || ((2 == sensorsSize) && enhancedExceptionSensor)) { for (IMethodSensor methodSensor : sensors) { String className = methodSensor.getSensorTypeConfig().getClassName(); // check if class name is null, return then nothing to check if (null == className) { return false; } if (PreparedStatementSensor.class.getName().equals(className)) { if ((null == invocationSequenceData.getSqlStatementData()) || (0 == invocationSequenceData.getSqlStatementData().getCount())) { return true; } } else if (Log4JLoggingSensor.class.getName().equals(className)) { return null == invocationSequenceData.getLoggingData(); } else if (REMOVE_SENSOR_CLASS_NAMES.contains(className)) { return null == invocationSequenceData.getSpanIdent(); } } } return false; } /** * Defines if the invocation container should skip the creation and processing of the invocation * for the given object and {@link RegisteredSensorConfig}. We will skip if any of following * conditions are met: * <ul> * <li>{@link RegisteredSensorConfig} has only exception sensor and object class does not match * the target class name. * <li>{@link RegisteredSensorConfig} has only prepared statement parameter sensor. * <li>{@link RegisteredSensorConfig} has only connection sensor. * <li>{@link RegisteredSensorConfig} has only connection meta data sensor. * </ul> * * @param rsc * {@link RegisteredSensorConfig}. * * @return Return <code>true</code> if hook should skip creation and processing, false * otherwise. */ private boolean skip(RegisteredSensorConfig rsc) { List<IMethodSensor> sensors = rsc.getMethodSensors(); if ((1 == sensors.size()) || ((2 == sensors.size()) && enhancedExceptionSensor)) { for (IMethodSensor methodSensor : sensors) { MethodSensorTypeConfig methodSensorTypeConfig = methodSensor.getSensorTypeConfig(); if (PreparedStatementParameterSensor.class.getName().equals(methodSensorTypeConfig.getClassName())) { return true; } if (ConnectionSensor.class.getName().equals(methodSensorTypeConfig.getClassName())) { return true; } } } return false; } /** * This checks if the invocation has to be saved or not (like the min duration is set and the * invocation is faster than the specified time). * * @param coreService * The reference to the core service which holds the data objects etc. * @param methodId * The unique method id. * @param sensorTypeId * The unique sensor type id. * @param rsc * The {@link RegisteredSensorConfig} object which holds all the information of the * executed method. * @param invocationSequenceData * The invocation sequence data object. * @param startTime * The start time. * @param endTime * The end time. * @param duration * The actual duration. */ private void checkForSavingOrNot(ICoreService coreService, long methodId, long sensorTypeId, RegisteredSensorConfig rsc, InvocationSequenceData invocationSequenceData, double startTime, // NOCHK double endTime, double duration) { double minduration = minDurationMap.get(invocationStartId.get()).doubleValue(); if (duration >= minduration) { if (LOG.isDebugEnabled()) { LOG.debug("Saving invocation. " + duration + " > " + minduration + " ID(local): " + rsc.getId()); } invocationSequenceData.setDuration(duration); invocationSequenceData.setStart(startTime); invocationSequenceData.setEnd(endTime); coreService.addMethodSensorData(sensorTypeId, methodId, String.valueOf(startTime), invocationSequenceData); } else { if (LOG.isDebugEnabled()) { LOG.debug("Not saving invocation. " + duration + " < " + minduration + " ID(local): " + rsc.getId()); } } } /** * {@inheritDoc} */ @Override public void beforeConstructor(long methodId, long sensorTypeId, Object[] parameters, RegisteredSensorConfig rsc) { beforeBody(methodId, sensorTypeId, null, parameters, rsc); } /** * {@inheritDoc} */ @Override public void afterConstructor(ICoreService coreService, long methodId, long sensorTypeId, Object object, Object[] parameters, RegisteredSensorConfig rsc) { firstAfterBody(methodId, sensorTypeId, object, parameters, null, rsc); secondAfterBody(coreService, methodId, sensorTypeId, object, parameters, null, rsc); } /** * Save the data objects which are coming from all the different sensor types in the current * invocation tracer context. * * @param dataObject * The data object to save. */ private void saveDataObject(DefaultData dataObject) { InvocationSequenceData invocationSequenceData = threadLocalInvocationData.get(); if (dataObject.getClass().equals(SqlStatementData.class)) { // don't overwrite an already existing sql statement data object. if (null == invocationSequenceData.getSqlStatementData()) { invocationSequenceData.setSqlStatementData((SqlStatementData) dataObject); } } if (dataObject.getClass().equals(HttpTimerData.class)) { // don't overwrite ourself but overwrite timers if ((null == invocationSequenceData.getTimerData()) || invocationSequenceData.getTimerData().getClass().equals(TimerData.class)) { invocationSequenceData.setTimerData((HttpTimerData) dataObject); } } if (dataObject.getClass().equals(TimerData.class)) { // don't overwrite an already existing timerdata or httptimerdata // object. if (null == invocationSequenceData.getTimerData()) { invocationSequenceData.setTimerData((TimerData) dataObject); } } if (dataObject.getClass().equals(ExceptionSensorData.class)) { ExceptionSensorData exceptionSensorData = (ExceptionSensorData) dataObject; invocationSequenceData.addExceptionSensorData(exceptionSensorData); } if (dataObject.getClass().equals(LoggingData.class)) { LoggingData loggingData = (LoggingData) dataObject; invocationSequenceData.setLoggingData(loggingData); } if (AbstractSpan.class.isAssignableFrom(dataObject.getClass())) { AbstractSpan span = (AbstractSpan) dataObject; invocationSequenceData.setSpanIdent(span.getSpanIdent()); } } // ////////////////////////////////////////////// // All methods from the ICoreService are below // // ////////////////////////////////////////////// /** * {@inheritDoc} */ @Override public void addMethodSensorData(long sensorTypeId, long methodId, String prefix, MethodSensorData methodSensorData) { if (null == threadLocalInvocationData.get()) { LOG.error("thread data NULL!!!!"); return; } saveDataObject(methodSensorData.finalizeData()); // delegate to real core service in case of the span if (AbstractSpan.class.isAssignableFrom(methodSensorData.getClass())) { realCoreService.addMethodSensorData(sensorTypeId, methodId, prefix, methodSensorData); } } /** * {@inheritDoc} */ @Override public void addObjectStorage(long sensorTypeId, long methodId, String prefix, IObjectStorage objectStorage) { if (null == threadLocalInvocationData.get()) { LOG.error("thread data NULL!!!!"); return; } DefaultData defaultData = objectStorage.finalizeDataObject(); saveDataObject(defaultData.finalizeData()); } /** * {@inheritDoc} */ @Override public void addPlatformSensorData(long sensorTypeIdent, SystemSensorData systemSensorData) { saveDataObject(systemSensorData.finalizeData()); } /** * {@inheritDoc} */ @Override public void addExceptionSensorData(long sensorTypeIdent, long throwableIdentityHashCode, ExceptionSensorData exceptionSensorData) { if (null == threadLocalInvocationData.get()) { LOG.info("thread data NULL!!!!"); return; } saveDataObject(exceptionSensorData.finalizeData()); } // ///////////////////////////////////////////////// // // Return NULL because no saved data can be returned // // ///////////////////////////////////////////////// // /** * {@inheritDoc} */ @Override public ExceptionSensorData getExceptionSensorData(long sensorTypeIdent, long throwableIdentityHashCode) { return null; } /** * {@inheritDoc} */ @Override public MethodSensorData getMethodSensorData(long sensorTypeIdent, long methodIdent, String prefix) { return null; } /** * {@inheritDoc} */ @Override public IObjectStorage getObjectStorage(long sensorTypeIdent, long methodIdent, String prefix) { return null; } /** * {@inheritDoc} */ public SystemSensorData getPlatformSensorData(long sensorTypeIdent) { return null; } // ////////////////////////////////////////////// // All unsupported methods are below from here // // ////////////////////////////////////////////// /** * {@inheritDoc} */ @Override public void addListListener(ListListener<?> listener) { throw new UnsupportedMethodException(); } /** * {@inheritDoc} */ public void addSendStrategy(ISendingStrategy strategy) { throw new UnsupportedMethodException(); } /** * {@inheritDoc} */ public void connect() throws ConnectException { throw new UnsupportedMethodException(); } /** * {@inheritDoc} */ @Override public void removeListListener(ListListener<?> listener) { throw new UnsupportedMethodException(); } /** * {@inheritDoc} */ @Override public void sendData() { throw new UnsupportedMethodException(); } /** * {@inheritDoc} */ public void setBufferStrategy(IBufferStrategy<DefaultData> bufferStrategy) { throw new UnsupportedMethodException(); } /** * {@inheritDoc} */ public void startSendingStrategies() { throw new UnsupportedMethodException(); } /** * {@inheritDoc} */ public void addPlatformSensorType(PlatformSensorTypeConfig platformSensorTypeConfig) { throw new UnsupportedMethodException(); } /** * {@inheritDoc} */ @Override public void start() { throw new UnsupportedMethodException(); } /** * {@inheritDoc} */ @Override public void stop() { throw new UnsupportedMethodException(); } /** * {@inheritDoc} */ @Override public void addJmxSensorValueData(long sensorTypeIdent, String objectName, String attributeName, JmxSensorValueData jmxSensorValueData) { throw new UnsupportedMethodException(); } }