diff --git a/appmap-java-maven-plugin/README.md b/appmap-java-maven-plugin/README.md new file mode 100644 index 00000000..b9f3b8b6 --- /dev/null +++ b/appmap-java-maven-plugin/README.md @@ -0,0 +1,67 @@ +AppLand AppMap Maven Plugin for Java +-------------------------------- + + +- [Building](#building) +- [Agent Configuration](#agent-configuration) +- [Maven Plugin Configuration](#maven-plugin-config) + - [Plugin Goals](#plugin-goals) + - [Plugin configuration options](#plugin-configuration) + - [Example](#example) + + +# Building +Artifacts will be written to `target/` use `appmap-java-plugin-[VERSION].jar`. as your maven plugin. +```bash +$ mvn clean install +``` + +# Agent Configuration +When you run your program, the agent reads configuration settings from `appmap.yml` by default. + +Please read configuration options from [AppMap Java Agent README.md](../README.md) + +# Maven Plugin Configuration + +## Plugin goals +prepare-agent : adds appmap.jar to JVM execution as javaagent + +## Plugin configuration options +outputDirectory (default: ./target/appmap/) +configFile (default: ./appmap.yml) +debug (enabled|disabled, default: disabled) +eventValueSize (integer, default 1024) +skip(Boolean, default false) + +## Example plugin config in a standard POM.xml file +```xml + + + com.appland + appmap-maven-plugin + ${appmap-java.version} + + + appmap.yml + enabled + 1024 + false + + + + + prepare-agent + + + + +``` + + +# Running +To run the java agent with correct plugin configuration you only need to build your project as usual without skipping +the test goal. + +```bash +$ mvn clean install +``` diff --git a/appmap-java-maven-plugin/pom.xml b/appmap-java-maven-plugin/pom.xml new file mode 100644 index 00000000..751e5921 --- /dev/null +++ b/appmap-java-maven-plugin/pom.xml @@ -0,0 +1,70 @@ + + 4.0.0 + com.appland + appmap-maven-plugin + maven-plugin + 0.5.0-SNAPSHOT + Appland Java Recorder Maven Plugin + This maven plugin helps you automatically generate an Appland AppMap using the Java Recorder. + + https://github.com/applandinc/appmap-java/tree/master/java-maven-plugin + + + UTF-8 + UTF-8 + + + + + com.appland + appmap-agent + 0.5.0 + + + + org.apache.maven + maven-plugin-api + 3.6.1 + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.6.0 + + + org.apache.maven + maven-core + 3.5.4 + + + + junit + junit + 4.13.1 + test + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.6.0 + + + true + + + + mojo-descriptor + + descriptor + + + + + + + diff --git a/appmap-java-maven-plugin/src/main/java/com/appland/appmap/AppMapAgentMojo.java b/appmap-java-maven-plugin/src/main/java/com/appland/appmap/AppMapAgentMojo.java new file mode 100644 index 00000000..dfeeeb30 --- /dev/null +++ b/appmap-java-maven-plugin/src/main/java/com/appland/appmap/AppMapAgentMojo.java @@ -0,0 +1,123 @@ +package com.appland.appmap; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +import java.io.File; +import java.util.*; + +import static java.lang.String.format; + +public abstract class AppMapAgentMojo extends AbstractMojo { + + static final String APPMAP_AGENT_ARTIFACT_NAME = "com.appland:appmap-agent"; + static final String SUREFIRE_ARG_LINE = "argLine"; + + @Parameter(property = "skip") + protected boolean skip = false; + + @Parameter(property = "project.outputDirectory") + protected File outputDirectory = new File("tmp"); + + @Parameter(property = "project.configFile") + protected File configFile = new File("appmap.yml"); + + @Parameter(property = "project.debug") + protected String debug = "disabled"; + + @Parameter(property = "project.eventValueSize") + protected Integer eventValueSize = 1024; + + @Parameter(property = "plugin.artifactMap") + protected Map pluginArtifactMap; + + @Parameter(property = "project") + private MavenProject project; + + public abstract void execute() throws MojoExecutionException; + + protected void skipMojo() { + } + + protected void loadAppMapJavaAgent() { + final String newValue = buildArguments(); + setProjectArgLineProperty(newValue); + getLog().info(SUREFIRE_ARG_LINE + + " set to " + StringEscapeUtils.unescapeJava(newValue)); + } + + /** + * This method builds the needed parameter to run the Agent, if previous configuration is found is also attached in + * the SUREFIRE_ARG_LINE, if previous version of the AppMap agent is found is removed and replaced with the version + * of this maven plugin + * + * @return formatted and escaped arguments to run on command line + */ + private String buildArguments() { + List args = new ArrayList(); + final String oldConfig = getCurrentArgLinePropertyValue(); + if (oldConfig != null) { + final List oldArgs = Arrays.asList(oldConfig.split(" ")); + removeOldAppMapAgentFromCommandLine(oldArgs); + args.addAll(oldArgs); + } + addMvnAppMapCommandLineArgsFirst(args); + StringBuilder builder = new StringBuilder(); + for ( String arg : args) { + builder.append(arg).append(" "); + } + return builder.toString(); + } + + /** + * Generate required quotes JVM argument based on current configuration and + * prepends it to the given argument command line. If a agent with the same + * JAR file is already specified this parameter is removed from the existing + * command line, does the same for xbootclasspath command. + */ + private void removeOldAppMapAgentFromCommandLine(List oldArgs) { + final String plainAgent = format("-javaagent:%s", getAppMapAgentJar()); + final String xbootClasspath = format("-Xbootclasspath/a:%s", getAppMapAgentJar()); + for (final Iterator i = oldArgs.iterator(); i.hasNext(); ) { + final String oldCommand = i.next(); + if (oldCommand.startsWith(plainAgent) || oldCommand.startsWith(xbootClasspath)) { + i.remove(); + } + } + } + + private void addMvnAppMapCommandLineArgsFirst(List args) { + args.add(StringEscapeUtils.escapeJava( + format("-Xbootclasspath/a:%s", getAppMapAgentJar(), this) + )); + args.add(StringEscapeUtils.escapeJava( + format("-javaagent:%s=%s", getAppMapAgentJar(), this) + )); + + args.add(0, "-Dappmap.debug=" + StringEscapeUtils.escapeJava(debug)); + args.add(0, "-Dappmap.output.directory=" + StringEscapeUtils.escapeJava(format("%s", outputDirectory))); + args.add(0, "-Dappmap.config.file=" + StringEscapeUtils.escapeJava(format("%s", configFile))); + args.add(0, "-Dappmap.event.valueSize=" + eventValueSize); + } + + + private Object setProjectArgLineProperty(String newValue) { + return project.getProperties().setProperty(SUREFIRE_ARG_LINE, newValue); + } + + private String getCurrentArgLinePropertyValue() { + return project.getProperties().getProperty(SUREFIRE_ARG_LINE); + } + + protected File getAppMapAgentJar() { + return pluginArtifactMap.get(APPMAP_AGENT_ARTIFACT_NAME).getFile(); + } + + public MavenProject getProject() { + return project; + } +} diff --git a/appmap-java-maven-plugin/src/main/java/com/appland/appmap/LoadJavaAppMapAgentMojo.java b/appmap-java-maven-plugin/src/main/java/com/appland/appmap/LoadJavaAppMapAgentMojo.java new file mode 100644 index 00000000..2a6e8728 --- /dev/null +++ b/appmap-java-maven-plugin/src/main/java/com/appland/appmap/LoadJavaAppMapAgentMojo.java @@ -0,0 +1,32 @@ +package com.appland.appmap; + + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; + +/** + * Goal that adds appmap.jar to JVM execution as javaagent, + * right before the test execution begins. + */ +@Mojo(name = "prepare-agent", defaultPhase = LifecyclePhase.TEST_COMPILE) +public class LoadJavaAppMapAgentMojo extends AppMapAgentMojo { + + @Override + public void execute() + throws MojoExecutionException { + try { + if (skip) { + getLog().info("Skipping AppLand AppMap execution because property skip is set."); + skipMojo(); + return; + } else { + getLog().info("Initializing AppLand AppMap Java Recorder." ); + loadAppMapJavaAgent(); + } + } catch (Exception e) { + getLog().error("Error initializing AppLand AppMap Java Recorder"); + e.printStackTrace(); + } + } +} diff --git a/appmap-java-maven-plugin/src/test/java/com/appland/appmap/LoadJavaMapAgentMojoTest.java b/appmap-java-maven-plugin/src/test/java/com/appland/appmap/LoadJavaMapAgentMojoTest.java new file mode 100644 index 00000000..e8ca9d78 --- /dev/null +++ b/appmap-java-maven-plugin/src/test/java/com/appland/appmap/LoadJavaMapAgentMojoTest.java @@ -0,0 +1,51 @@ +package com.appland.appmap; + +import static org.junit.Assert.assertEquals; + +import com.appland.appmap.output.v1.Event; + +import com.appland.appmap.record.IRecordingSession; +import com.appland.appmap.record.Recorder; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; + +public class LoadJavaMapAgentMojoTest { + + @Before + public void before() throws Exception { + final IRecordingSession.Metadata metadata = + new IRecordingSession.Metadata(); + + Recorder.getInstance().start(metadata); + } + + @Test + public void testAllEventsWritten() { + final Recorder recorder = Recorder.getInstance(); + final Long threadId = Thread.currentThread().getId(); + final Event[] events = new Event[] { + new Event(), + new Event(), + new Event(), + }; + + for (int i = 0; i < events.length; i++) { + final Event event = events[i]; + event + .setDefinedClass("SomeClass") + .setMethodId("SomeMethod") + .setStatic(false) + .setLineNumber(315) + .setThreadId(threadId); + + recorder.add(event); + assertEquals(event, recorder.getLastEvent()); + } + + final String appmapJson = recorder.stop(); + final String expectedJson = "\"thread_id\":" + threadId.toString(); + final int numMatches = StringUtils.countMatches(appmapJson, expectedJson); + assertEquals(numMatches, events.length); + } +}