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

com.appland.appmap.Agent Maven / Gradle / Ivy

The newest version!
package com.appland.appmap;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.tinylog.TaggedLogger;
import org.tinylog.provider.ProviderRegistry;

import com.appland.appmap.config.AppMapConfig;
import com.appland.appmap.config.Properties;
import com.appland.appmap.process.hooks.MethodCall;
import com.appland.appmap.process.hooks.MethodReturn;
import com.appland.appmap.record.Recorder;
import com.appland.appmap.record.Recorder.Metadata;
import com.appland.appmap.record.Recording;
import com.appland.appmap.runtime.HookFunctions;
import com.appland.appmap.transform.ClassFileTransformer;
import com.appland.appmap.transform.annotations.HookFactory;
import com.appland.appmap.transform.instrumentation.BBTransformer;
import com.appland.appmap.util.GitUtil;

import javassist.CtClass;

public class Agent {

  public static final TaggedLogger logger = AppMapConfig.getLogger(null);

  /**
   * premain is the entry point for the AppMap Java agent.
   *
   * @param agentArgs agent options
   * @param inst services needed to instrument Java programming language code
   * @see Package
   *      java.lang.instrument
   */
  public static void premain(String agentArgs, Instrumentation inst) {

    long start = System.currentTimeMillis();
    try {
      AppMapConfig.initialize(FileSystems.getDefault());
    } catch (IOException e) {
      logger.warn(e, "Initialization failed");
      System.exit(1);
    }
    if (Properties.DisableLogFile == null) {
      // The user hasn't made a choice about disabling logging, let them know
      // they can.
      logger.info(
          "To disable the automatic creation of this log file, set the system property {} to 'true'",
          Properties.DISABLE_LOG_FILE_KEY);
    }
    logger.info("Agent version {}, current time mills: {}",
        Agent.class.getPackage().getImplementationVersion(), start);
    logger.info("config: {}", AppMapConfig.get());
    logger.info("System properties: {}", System.getProperties());
    logger.debug(new Exception(), "whereAmI");

    addAgentJars(agentArgs, inst);


    if (!Properties.DisableGit) {
      try {
        GitUtil.findSourceRoots();
        logger.debug("done finding source roots, {}", () -> {
          long now = System.currentTimeMillis();
          return String.format("%d, %d", now - start, start);
        });
      } catch (IOException e) {
        logger.warn(e);
      }
    }

    if (Properties.SaveInstrumented) {
      CtClass.debugDump =
          Paths.get(System.getProperty("java.io.tmpdir"), "appmap", "ja").toString();
      logger.info("Saving instrumented files to {}", CtClass.debugDump);

    }

    // First, install a javassist-based transformer that will annotate app
    // methods that require instrumentation.
    ClassFileTransformer methodCallTransformer =
        new ClassFileTransformer("method call", HookFactory.APP_HOOKS_FACTORY);
    inst.addTransformer(methodCallTransformer);

    // Next, install a bytebuddy-based transformer that will instrument the
    // annotated methods.
    BBTransformer.installOn(inst);

    // Finally, install another javassist-based transformer that will instrument
    // non-app methods that have been hooked, i.e. those that have been
    // specified in com.appland.appmap.process.
    ClassFileTransformer systemHookTransformer =
        new ClassFileTransformer("system hook", HookFactory.AGENT_HOOKS_FACTORY);
    inst.addTransformer(systemHookTransformer);

    Runnable logShutdown = () -> {
      try {
        ClassFileTransformer.logStatistics();

        ProviderRegistry.getLoggingProvider().shutdown();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    };

    if (Properties.RecordingAuto) {
      startAutoRecording(logShutdown);
    }
    else {
      Runtime.getRuntime().addShutdownHook(new Thread(logShutdown));
    }
  }

  private static void startAutoRecording(Runnable logShutdown) {
    String appmapName = Properties.RecordingName;
    final Date date = new Date();
    final SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddHHmmss");
    final String timestamp = dateFormat.format(date);
    final Metadata metadata = new Metadata("java", "process");
    final Recorder recorder = Recorder.getInstance();
    if (appmapName == null || appmapName.trim().isEmpty()) {
      appmapName = timestamp;
    }
    metadata.scenarioName = appmapName;
    recorder.start(metadata);
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
      String fileName = Properties.RecordingFile;

      if (fileName == null || fileName.trim().isEmpty()) {
        fileName = String.format("%s.appmap.json", timestamp);
      }

      Recording recording = recorder.stop();
      recording.moveTo(fileName);

      logShutdown.run();
    }));
  }

  private static void addAgentJars(String agentArgs, Instrumentation inst) {

    Path agentJarPath = null;
    try {
      Class agentClass = Agent.class;
      URL resourceURL = agentClass.getClassLoader()
          .getResource(agentClass.getName().replace('.', '/') + ".class");
      // During testing of the agent itself, classes get loaded from a directory, and will have the
      // protocol "file". The rest of the time (i.e. when it's actually deployed), they'll always
      // come from a jar file.
      if (resourceURL.getProtocol().equals("jar")) {
        String resourcePath = resourceURL.getPath();
        URL jarURL = new URL(resourcePath.substring(0, resourcePath.indexOf('!')));
        logger.debug("jarURL: {}", jarURL);
        agentJarPath = Paths.get(jarURL.toURI());
      }
    } catch (URISyntaxException | MalformedURLException e) {
      // Doesn't seem like these should ever happen....
      logger.error(e, "Failed getting path to agent jar");
      System.exit(1);
    }
    if (agentJarPath != null) {
      try {
        JarFile agentJar = new JarFile(agentJarPath.toFile());
        inst.appendToSystemClassLoaderSearch(agentJar);

        setupRuntime(agentJarPath, agentJar, inst);
      } catch (IOException | SecurityException | IllegalArgumentException e) {
        logger.error(e, "Failed loading agent jars");
        System.exit(1);
      }
    }
  }

  private static void setupRuntime(Path agentJarPath, JarFile agentJar, Instrumentation inst)
      throws IOException, FileNotFoundException {
    Path runtimeJarPath = null;
    for (Enumeration entries = agentJar.entries(); entries.hasMoreElements();) {
      JarEntry entry = entries.nextElement();
      String entryName = entry.getName();
      if (entryName.startsWith("runtime-")) {
        Path installDir = agentJarPath.getParent();
        runtimeJarPath = installDir.resolve(FilenameUtils.getBaseName(entryName) + ".jar");
        if (!Files.exists(runtimeJarPath)) {
          IOUtils.copy(agentJar.getInputStream(entry),
              new FileOutputStream(runtimeJarPath.toFile()));
        }
        break;
      }
    }

    if (runtimeJarPath == null) {
      logger.error("Couldn't find runtime jar in {}", runtimeJarPath);
      System.exit(1);
    }

    // Adding the runtime jar to the boot class loader means the classes it
    // contains will be available everywhere. This avoids issues caused by any
    // filtering the app's class loader might be doing (e.g. the Scala runtime
    // when running a Play app).
    JarFile runtimeJar = new JarFile(runtimeJarPath.toFile());
    inst.appendToSystemClassLoaderSearch(runtimeJar);
    // inst.appendToBootstrapClassLoaderSearch(runtimeJar);

    // HookFunctions can only be referenced after the runtime jar has been
    // appended to the boot class loader.
    HookFunctions.onMethodCall = MethodCall::onCall;
    HookFunctions.onMethodReturn = MethodReturn::onReturn;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy