package rocks.inspectit.agent.java.analyzer.impl;
import info.novatec.inspectit.org.objectweb.asm.ClassReader;
import info.novatec.inspectit.org.objectweb.asm.ClassWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import com.google.common.io.ByteStreams;
import rocks.inspectit.agent.java.analyzer.IByteCodeAnalyzer;
import rocks.inspectit.agent.java.config.IConfigurationStorage;
import rocks.inspectit.agent.java.config.StorageException;
import rocks.inspectit.agent.java.config.impl.AbstractSensorConfig;
import rocks.inspectit.agent.java.config.impl.RegisteredSensorConfig;
import rocks.inspectit.agent.java.config.impl.SpecialSensorConfig;
import rocks.inspectit.agent.java.connection.IConnection;
import rocks.inspectit.agent.java.core.IPlatformManager;
import rocks.inspectit.agent.java.hooking.IHookDispatcherMapper;
import rocks.inspectit.agent.java.instrumentation.InstrumenterFactory;
import rocks.inspectit.agent.java.instrumentation.asm.ClassAnalyzer;
import rocks.inspectit.agent.java.instrumentation.asm.ClassInstrumenter;
import rocks.inspectit.agent.java.instrumentation.asm.LoaderAwareClassWriter;
import rocks.inspectit.agent.java.sensor.method.IMethodSensor;
import rocks.inspectit.shared.all.instrumentation.classcache.Type;
import rocks.inspectit.shared.all.instrumentation.config.impl.InstrumentationDefinition;
import rocks.inspectit.shared.all.instrumentation.config.impl.MethodInstrumentationConfig;
import rocks.inspectit.shared.all.instrumentation.config.impl.PropertyPathStart;
import rocks.inspectit.shared.all.instrumentation.config.impl.SensorInstrumentationPoint;
import rocks.inspectit.shared.all.instrumentation.config.impl.SpecialInstrumentationPoint;
import rocks.inspectit.shared.all.spring.logger.Log;
/**
* {@link IByteCodeAnalyzer} that uses {@link IConnection} to connect to the CMR and send the
* analyzed type. If needed performs instrumentation based on the result of the CMR answer.
*
* @author Ivan Senic
*
*/
@Component
public class ByteCodeAnalyzer implements IByteCodeAnalyzer, InitializingBean {
/**
* Amount of milliseconds to wait for the result of the {@link AnalyzeCallable}.
*/
private static final int ANALYZE_TIMEOUT_MILLIS = 2000;
/**
* Log for the class.
*/
@Log
Logger log;
/**
* Platform manager.
*/
@Autowired
private IPlatformManager platformManager;
/**
* {@link IConfigurationStorage} for getting if exception sensor is active or not.
*/
@Autowired
private IConfigurationStorage configurationStorage;
/**
* {@link IConnection}.
*/
@Autowired
private IConnection connection;
/**
* {@link IHookDispatcherMapper}.
*/
@Autowired
private IHookDispatcherMapper hookDispatcherMapper;
/**
* {@link IClassHashHelper}.
*/
@Autowired
private ClassHashHelper classHashHelper;
/**
* Core-service executor service.
*/
@Autowired
@Qualifier("coreServiceExecutorService")
private ExecutorService executorService;
/**
* {@link InstrumenterFactory} needed for the instrumentation process.
*/
@Autowired
private InstrumenterFactory instrumenterFactory;
/**
* All initialized {@link IMethodSensor}s.
*/
@Autowired
private List<IMethodSensor> methodSensors;
/**
* Map of {@link IMethodSensor}s to their IDs for faster lookups.
*/
private Map<Long, IMethodSensor> methodSensorMap;
/**
* {@inheritDoc}
*/
@Override
public byte[] analyzeAndInstrument(byte[] byteCode, String className, final ClassLoader classLoader) {
return analyzeAndInstrumentInternal(byteCode, className, classLoader, true);
}
/**
* Internal implementation of the {@link #analyzeAndInstrument(byte[], String, ClassLoader)}.
* Provides option to define if the instrumentation needs to be performed or not.
*
* @param byteCode
* The byte-code of the class to analyze. If <code>null</code> is passed byte code
* will be loaded using {@link #getByteCodeFromClassLoader(String, ClassLoader)}.
* @param className
* The class name.
* @param classLoader
* The class loader.
* @param performInstrumentation
* If instrumentation should be performed with the instrumentation result sent by the
* server.
* @return The instrumented byte code or <code>null</code> if instrumentation was not performed
* (or in case of error).
*/
private byte[] analyzeAndInstrumentInternal(byte[] byteCode, String className, final ClassLoader classLoader, boolean performInstrumentation) {
// clear any interrupted flag that might be there on the thread loading the class
boolean isInterrupted = Thread.interrupted();
try {
if (null == byteCode) {
// try to read from class loader, if it fails just return
byteCode = getByteCodeFromClassLoader(className, classLoader);
if (null == byteCode) {
return null;
}
}
// no matter what first register class being analyzed with class loader
classHashHelper.registerAnalyzed(className);
// create the hash
String hash = DigestUtils.sha256Hex(byteCode);
InstrumentationDefinition instrumentationResult = null;
if (classHashHelper.isSent(className, hash)) {
// if sent load instrumentation result from the class hash helper
instrumentationResult = classHashHelper.getInstrumentationDefinition(className);
} else {
// if not sent we go for the sending
if (!connection.isConnected()) {
// we will not do anything else if there is no connection
if (log.isDebugEnabled()) {
log.debug("Not parsing and sending data for " + className + " as connection to server does not exist.");
}
return null;
}
// parse first, do not use internFQNs
ClassReader classReader = new ClassReader(byteCode);
ClassAnalyzer classAnalyzer = new ClassAnalyzer(hash);
classReader.accept(classAnalyzer, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
Type type = (Type) classAnalyzer.getType();
// analyze all necessary depending classes before
analyzeDependingTypes(type, classLoader);
// try connecting to server
Callable<InstrumentationDefinition> analyzeCallable = new AnalyzeCallable(connection, platformManager.getPlatformId(), hash, type);
try {
instrumentationResult = executorService.submit(analyzeCallable).get(ANALYZE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
isInterrupted = true;
if (log.isWarnEnabled()) {
log.warn("Error occurred instrumenting the byte code of class " + className + ". Thread loading the class was interrupted during communication with the server.", e);
}
return null;
} catch (TimeoutException e) {
if (log.isWarnEnabled()) {
log.warn("Error occurred instrumenting the byte code of class " + className + ". Sending the class structure to the CMR resulted in a time-out.", e);
}
return null;
}
// register type as sent
classHashHelper.registerSent(className, hash);
classHashHelper.registerInstrumentationDefinition(className, instrumentationResult);
}
// execute instrumentation if needed
if (performInstrumentation) {
return performInstrumentation(byteCode, classLoader, instrumentationResult);
} else {
return null;
}
} catch (StorageException storageException) {
log.error("Error occurred instrumenting the byte code of class " + className, storageException);
return null;
} catch (ExecutionException executionException) {
log.error("Error occurred instrumenting the byte code of class " + className, executionException);
return null;
} finally {
if (isInterrupted) {
Thread.currentThread().interrupt();
}
}
}
/**
* Analyze the depending types of the given type and sends the results to the server if needed.
*
* @param type
* {@link Type}
* @param classLoader
* {@link ClassLoader} used for loading the given type.
*/
private void analyzeDependingTypes(Type type, ClassLoader classLoader) {
Collection<Type> dependingTypes = type.getDependingTypes();
if (CollectionUtils.isNotEmpty(dependingTypes)) {
for (Type dependingType : dependingTypes) {
if (!classHashHelper.isAnalyzed(dependingType.getFQN())) {
analyzeAndInstrumentInternal(null, dependingType.getFQN(), classLoader, false);
}
}
}
}
/**
* Performs the instrumentation. No instrumentation will be performed if instrumentation result
* is <code>null</code> or {@link InstrumentationDefinition#isEmpty()} returns <code>true</code>
* .
*
* @param byteCode
* original byte code
* @param classLoader
* class loader loading the class
* @param instrumentationResult
* {@link InstrumentationDefinition} holding instrumentation properties.
* @return instrumented byte code or <code>null</code> if instrumentation result is
* <code>null</code> or contains no instrumentation points
* @throws StorageException
* If storage exception occurs when reading if enhanced exception sensor is active
*/
private byte[] performInstrumentation(byte[] byteCode, ClassLoader classLoader, InstrumentationDefinition instrumentationResult) throws StorageException {
// if no instrumentation result or empty return null
if ((null == instrumentationResult) || instrumentationResult.isEmpty()) {
return null;
}
Collection<MethodInstrumentationConfig> instrumentationConfigs = instrumentationResult.getMethodInstrumentationConfigs();
// here do the instrumentation
ClassReader classReader = new ClassReader(byteCode);
LoaderAwareClassWriter classWriter = new LoaderAwareClassWriter(classReader, ClassWriter.COMPUTE_FRAMES, classLoader);
ClassInstrumenter classInstrumenter = new ClassInstrumenter(instrumenterFactory, classWriter, instrumentationConfigs, configurationStorage.isEnhancedExceptionSensorActivated());
classReader.accept(classInstrumenter, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
// return changed byte code if we did actually add some byte code
if (classInstrumenter.isByteCodeAdded()) {
Map<Long, long[]> methodToSensorMap = new HashMap<Long, long[]>(0);
// map the instrumentation points if we have them
for (MethodInstrumentationConfig config : classInstrumenter.getAppliedInstrumentationConfigs()) {
RegisteredSensorConfig registeredSensorConfig = createRegisteredSensorConfig(config);
if (null != registeredSensorConfig) {
SensorInstrumentationPoint sensorInstrumentationPoint = config.getSensorInstrumentationPoint();
hookDispatcherMapper.addMapping(registeredSensorConfig.getId(), registeredSensorConfig);
methodToSensorMap.put(Long.valueOf(registeredSensorConfig.getId()), sensorInstrumentationPoint.getSensorIds());
}
SpecialSensorConfig specialSensorConfig = createSpecialSensorConfig(config);
if (null != specialSensorConfig) {
SpecialInstrumentationPoint specialInstrumentationPoint = config.getSpecialInstrumentationPoint();
hookDispatcherMapper.addMapping(specialSensorConfig.getId(), specialSensorConfig);
methodToSensorMap.put(Long.valueOf(specialSensorConfig.getId()), new long[] { specialInstrumentationPoint.getSensorId() });
}
}
// inform CMR of the applied instrumentation ids
if (MapUtils.isNotEmpty(methodToSensorMap)) {
executorService.submit(new InstrumentationAppliedRunnable(connection, platformManager.getPlatformId(), methodToSensorMap));
}
return classWriter.toByteArray();
} else {
return null;
}
}
/**
* Creates the new {@link RegisteredSensorConfig} from the {@link MethodInstrumentationConfig}
* if the {@link SensorInstrumentationPoint} is defined in the configuration.
* <p>
* Data from the {@link SensorInstrumentationPoint} is copied to the
* {@link RegisteredSensorConfig} and method sensors are resolved and attached to the returned
* sensor configuration.
*
* @param config
* {@link MethodInstrumentationConfig}
* @return {@link RegisteredSensorConfig} or <code>null</code> if this instrumentation config
* does not defined the {@link SensorInstrumentationPoint}.
*/
private RegisteredSensorConfig createRegisteredSensorConfig(MethodInstrumentationConfig config) {
SensorInstrumentationPoint sensorInstrumentationPoint = config.getSensorInstrumentationPoint();
if (null == sensorInstrumentationPoint) {
return null;
}
// copy properties
RegisteredSensorConfig rsc = new RegisteredSensorConfig();
this.copyInfo(rsc, config);
rsc.setId(sensorInstrumentationPoint.getId());
rsc.setStartsInvocation(sensorInstrumentationPoint.isStartsInvocation());
rsc.setSettings(sensorInstrumentationPoint.getSettings());
// accessor list must be thread safe
if (CollectionUtils.isNotEmpty(sensorInstrumentationPoint.getPropertyAccessorList())) {
rsc.setPropertyAccessorList(new CopyOnWriteArrayList<PropertyPathStart>(sensorInstrumentationPoint.getPropertyAccessorList()));
} else {
rsc.setPropertyAccessorList(Collections.<PropertyPathStart> emptyList());
}
// resolve sensors
for (long sensorId : sensorInstrumentationPoint.getSensorIds()) {
IMethodSensor sensor = methodSensorMap.get(sensorId);
if (null != sensor) {
rsc.addMethodSensor(sensor);
} else {
String methodFull = config.getTargetClassFqn() + "#" + config.getTargetMethodName();
log.error("Sensor with the id " + sensorId + " does not exists on the agent, but it's defined for the method: " + methodFull);
}
}
return rsc;
}
/**
* Creates the new {@link SpecialSensorConfig} from the {@link MethodInstrumentationConfig} if
* the {@link SpecialInstrumentationPoint} is defined in the configuration.
* <p>
* Data from the {@link SpecialSensorConfig} is copied to the {@link SpecialSensorConfig} and
* special method sensor is resolved and attached to the returned sensor configuration.
*
* @param config
* {@link MethodInstrumentationConfig}
* @return {@link SpecialSensorConfig} or <code>null</code> if this instrumentation config does
* not defined the {@link SpecialInstrumentationPoint}.
*/
private SpecialSensorConfig createSpecialSensorConfig(MethodInstrumentationConfig config) {
SpecialInstrumentationPoint specialInstrumentationPoint = config.getSpecialInstrumentationPoint();
if (null == specialInstrumentationPoint) {
return null;
}
SpecialSensorConfig ssc = new SpecialSensorConfig();
this.copyInfo(ssc, config);
ssc.setId(specialInstrumentationPoint.getId());
// resolve sensor
long sensorId = specialInstrumentationPoint.getSensorId();
IMethodSensor sensor = methodSensorMap.get(sensorId);
if (null != sensor) {
ssc.setSensor(sensor);
} else {
String methodFull = config.getTargetClassFqn() + "#" + config.getTargetMethodName();
log.error("Sensor with the id " + sensorId + " does not exists on the agent, but it's defined for the method: " + methodFull);
}
return ssc;
}
/**
* Copies all the class/method information from the {@link MethodInstrumentationConfig} to the
* {@link AbstractSensorConfig}.
*
* @param asc
* {@link AbstractSensorConfig}
* @param config
* {@link MethodInstrumentationConfig}
*/
private void copyInfo(AbstractSensorConfig asc, MethodInstrumentationConfig config) {
asc.setTargetClassFqn(config.getTargetClassFqn());
asc.setTargetMethodName(config.getTargetMethodName());
asc.setReturnType(config.getReturnType());
asc.setParameterTypes(config.getParameterTypes());
}
/**
* Tries to read the byte code form the input stream provided by the given class loader. If the
* class loader is <code>null</code>, then {@link ClassLoader#getResourceAsStream(String)} will
* be called in order to find byte code.
* <p>
* This method returns provided byte code or <code>null</code> if reading was not successful.
*
* @param className
* Class name defined by the class object.
* @param classLoader
* Class loader loading the class.
* @return Byte code or <code>null</code> if reading was not successful
*/
private byte[] getByteCodeFromClassLoader(String className, ClassLoader classLoader) {
InputStream is = null;
try {
if (null != classLoader) {
is = classLoader.getResourceAsStream(className.replace('.', '/') + ".class");
} else {
is = ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class");
}
if (null == is) {
// nothing we can do here
return null;
}
return ByteStreams.toByteArray(is);
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug("Can not load byte-code for the class " + className + " and class loader " + classLoader + ". Class will be ignored and not instrumented.", e);
} else {
log.info("Can not load byte-code for the class " + className + " and class loader " + classLoader + ". Class will be ignored and not instrumented.");
}
return null;
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) { // NOPMD //NOCHK
// ignore
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void afterPropertiesSet() throws Exception {
methodSensorMap = new HashMap<Long, IMethodSensor>();
for (IMethodSensor methodSensor : methodSensors) {
methodSensorMap.put(methodSensor.getSensorTypeConfig().getId(), methodSensor);
}
}
}