From 7072c0e3e2b6d6affc9c2479b72880a0ef4f3fc6 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 23:19:50 -0700 Subject: [PATCH] Add ability to pass options to javaagents (#50) Support the `-javaagent:=` form of the flag by introducing a `javaagent { }` extension with an `agentOptions` map keyed by dependency coordinate (`group:name` for module dependencies, project path for project dependencies). Keying by coordinate keeps options independent of the resolved jar file name so they survive version bumps. A shared `AgentOptionsResolver` resolves the coordinate-keyed options against the configuration's resolved artifacts into a `fileName -> options` map, which is threaded into all four contexts that build a `-javaagent:` argument: the application `run` task, the `test` task, application distribution start scripts, and jib container entrypoints. Agents without a matching entry are attached with no suffix, preserving existing behavior. Per the design discussion on the issue, options are a single project-level value shared across contexts; per-context overrides are left as a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FBTxQkVXBjJoBSfKPWyY3K --- README.md | 32 +++++++ .../jib/JibExtensionConfiguration.java | 14 +++ .../javaagent/jib/JavaagentJibExtension.kt | 9 +- .../JavaagentJibExtensionFunctionalTest.kt | 38 ++++++++ .../javaagent/jib/JavaagentJibPlugin.kt | 7 ++ .../JavaagentPluginFunctionalTest.kt | 87 +++++++++++++++++++ .../javaagent/AgentOptionsResolver.java | 83 ++++++++++++++++++ .../javaagent/JavaForkOptionsConfigurer.java | 12 ++- .../JavaagentApplicationDistributionPlugin.kt | 9 +- .../JavaagentApplicationRunPlugin.kt | 10 ++- .../JavaagentAwareStartScriptGenerator.kt | 17 +++- .../ryandens/javaagent/JavaagentBasePlugin.kt | 6 ++ .../ryandens/javaagent/JavaagentExtension.kt | 34 ++++++++ .../ryandens/javaagent/JavaagentTestPlugin.kt | 6 ++ .../main/java/com/ryandens/SimpleAgent.java | 3 + 15 files changed, 359 insertions(+), 8 deletions(-) create mode 100644 plugin/src/main/java/com/ryandens/javaagent/AgentOptionsResolver.java create mode 100644 plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentExtension.kt diff --git a/README.md b/README.md index a5700528..5033253c 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,38 @@ by the `javaagent` Gradle configuration! In addition, the distributions created ApplicationPlugin tasks include the javaagent JAR in the archive's dependency directory. Finally, the distribution start scripts have been modified to automatically attach the agent via the command line `-javaagent` flag. +## Passing options to javaagents + +Some javaagents accept options via the `-javaagent:=` form of the flag (see the +[`java.lang.instrument` documentation](https://docs.oracle.com/en/java/javase/21/docs/api/java.instrument/java/lang/instrument/package-summary.html#starting-an-agent-from-the-command-line-interface-heading)). +For example, the [Prometheus JMX Exporter](https://github.com/prometheus/jmx_exporter) agent is configured as +`-javaagent:jmx_prometheus_javaagent.jar=12345:config.yaml`. + +Use the `javaagent` extension to associate options with an agent. Options are keyed by the dependency +coordinate you declared, so they are independent of the resolved jar file name and survive version bumps: + +* module dependencies are keyed by `group:name` (the version is intentionally excluded) +* project dependencies are keyed by their project path, e.g. `:my-agent` + +```kotlin +plugins { + application + id("com.ryandens.javaagent-application") version "0.11.0" +} + +dependencies { + javaagent("io.prometheus.jmx:jmx_prometheus_javaagent:0.20.0") +} + +javaagent { + agentOptions.put("io.prometheus.jmx:jmx_prometheus_javaagent", "12345:config.yaml") +} +``` + +The same options are applied consistently across every context this plugin configures: the application `run` +task, the application distribution start scripts, the `test` task, and jib container entrypoints. Agents without +a matching entry are attached without an options suffix, exactly as before. + ## Jib integration [Jib](https://github.com/GoogleContainerTools/jib) is a build tool for containerizing Java applications without a Docker diff --git a/jib-common/src/main/java/com/ryandens/javaagent/jib/JibExtensionConfiguration.java b/jib-common/src/main/java/com/ryandens/javaagent/jib/JibExtensionConfiguration.java index 6f54e2d2..6bc4eb9d 100644 --- a/jib-common/src/main/java/com/ryandens/javaagent/jib/JibExtensionConfiguration.java +++ b/jib-common/src/main/java/com/ryandens/javaagent/jib/JibExtensionConfiguration.java @@ -5,6 +5,7 @@ import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; import org.gradle.api.tasks.Input; /** @@ -18,6 +19,8 @@ public class JibExtensionConfiguration { private final ListProperty javaagentFiles; + private final MapProperty agentOptions; + /** * Instantiated by Jib's plugin extension mechanism * @@ -40,6 +43,7 @@ public JibExtensionConfiguration(final Project project) { */ public JibExtensionConfiguration(final ObjectFactory objectFactory) { javaagentFiles = objectFactory.listProperty(File.class); + agentOptions = objectFactory.mapProperty(String.class, String.class); } /** @@ -52,4 +56,14 @@ public JibExtensionConfiguration(final ObjectFactory objectFactory) { ListProperty getJavaagentFiles() { return javaagentFiles; } + + /** + * Returns the agent options keyed by agent file name. When an agent's file name has an entry + * here, the corresponding value is appended to its {@code -javaagent} flag as {@code =}. + * Package-private: intended to be accessed only by {@link JavaagentJibExtension}. + */ + @Input + MapProperty getAgentOptions() { + return agentOptions; + } } diff --git a/jib-common/src/main/kotlin/com/ryandens/javaagent/jib/JavaagentJibExtension.kt b/jib-common/src/main/kotlin/com/ryandens/javaagent/jib/JavaagentJibExtension.kt index ea8adba2..0f531a19 100644 --- a/jib-common/src/main/kotlin/com/ryandens/javaagent/jib/JavaagentJibExtension.kt +++ b/jib-common/src/main/kotlin/com/ryandens/javaagent/jib/JavaagentJibExtension.kt @@ -46,12 +46,19 @@ class JavaagentJibExtension : } val localAgentPaths = extraConfig.get().javaagentFiles.get() + val agentOptions = extraConfig.get().agentOptions.get() val planBuilder = buildPlan.toBuilder() val newEntrypoint = buildList { addAll(entrypoint) - addAll(1, localAgentPaths.map { localAgentPath -> "-javaagent:/opt/jib-agents/${localAgentPath.name}" }) + addAll( + 1, + localAgentPaths.map { localAgentPath -> + val suffix = agentOptions[localAgentPath.name]?.let { "=$it" } ?: "" + "-javaagent:/opt/jib-agents/${localAgentPath.name}$suffix" + }, + ) } val javaagentFileEntries = localAgentPaths.map { localAgentPath -> diff --git a/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt b/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt index c8aa543e..dd29bfb5 100644 --- a/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt +++ b/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt @@ -59,6 +59,41 @@ class JavaagentJibExtensionFunctionalTest { } } + @Test fun `can pass agent options to jib entrypoint`() { + val dependencies = """ + javaagent project(':simple-agent') + runtimeOnly 'commons-lang:commons-lang:2.6' + """ + + val result = + createAndBuildJavaagentProject( + dependencies, + listOf("jibBuildTar"), + extraBuildScript = + """ + javaagent { + agentOptions.put(':simple-agent', '12345:config.yaml') + } + """, + ) + + assertTrue(result.output.contains("Running extension: com.ryandens.javaagent.jib.JavaagentJibExtension")) + assertTrue(File(functionalTestDir, JIB_IMAGE).exists()) + + FileInputStream(File(functionalTestDir, JIB_IMAGE)).use { fis -> + ArchiveStreamFactory().createArchiveInputStream>(ArchiveStreamFactory.TAR, fis).use { ais -> + var entry = ais.nextEntry + while (entry != null) { + if ("config.json" == entry.name) { + val json = ais.readBytes().toString(Charsets.UTF_8) + assertTrue(json.contains("-javaagent:/opt/jib-agents/simple-agent.jar=12345:config.yaml")) + } + entry = ais.nextEntry + } + } + } + } + @Test fun `works even without any javaagent dependencies`() { val dependencies = """ runtimeOnly 'commons-lang:commons-lang:2.6' @@ -89,6 +124,7 @@ class JavaagentJibExtensionFunctionalTest { private fun createAndBuildJavaagentProject( dependencies: String, buildArgs: List, + extraBuildScript: String = "", ): BuildResult { val helloWorldDir = File(functionalTestDir, "hello-world") File( @@ -150,6 +186,8 @@ class JavaagentJibExtensionFunctionalTest { image = "javaagent-hello-world" } } + + $extraBuildScript """, ) diff --git a/jib/src/main/kotlin/com/ryandens/javaagent/jib/JavaagentJibPlugin.kt b/jib/src/main/kotlin/com/ryandens/javaagent/jib/JavaagentJibPlugin.kt index e11944dd..51e448e2 100644 --- a/jib/src/main/kotlin/com/ryandens/javaagent/jib/JavaagentJibPlugin.kt +++ b/jib/src/main/kotlin/com/ryandens/javaagent/jib/JavaagentJibPlugin.kt @@ -1,7 +1,9 @@ package com.ryandens.javaagent.jib import com.google.cloud.tools.jib.gradle.JibExtension +import com.ryandens.javaagent.AgentOptionsResolver import com.ryandens.javaagent.JavaagentBasePlugin +import com.ryandens.javaagent.JavaagentExtension import org.gradle.api.Action import org.gradle.api.Plugin import org.gradle.api.Project @@ -21,6 +23,10 @@ class JavaagentJibPlugin : Plugin { val javaagentConfiguration = project.configurations.named(JavaagentBasePlugin.CONFIGURATION_NAME) + val javaagentExtension = project.extensions.getByType(JavaagentExtension::class.java) + val optionsByFileName = + AgentOptionsResolver.optionsByFileName(javaagentConfiguration.get(), javaagentExtension.agentOptions) + val destinationDirectory = project.tasks .named( @@ -44,6 +50,7 @@ class JavaagentJibPlugin : Plugin { .toList() }, ) + extensionConfiguration.agentOptions.set(optionsByFileName) }, ) } diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index d0bc56ce..dde977b5 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -211,6 +211,90 @@ DEFAULT_JVM_OPTS="\"-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar\" \"-X ) } + @Test fun `can pass agent options to application run task`() { + val dependencies = """ + javaagent project(':simple-agent') + """ + + createJavaagentProject( + dependencies, + extraBuildScript = + """ + javaagent { + agentOptions.put(':simple-agent', '12345:config.yaml') + } + """, + ) + val result = runBuild(listOf("assemble", "run")) + + assertTrue(result.output.contains("Hello World!")) + assertTrue(result.output.contains("Hello from my simple agent!")) + // the simple agent echoes the agentArgs it was launched with (the `=` portion) + assertTrue(result.output.contains("Simple agent args: 12345:config.yaml")) + } + + @Test fun `passes agent options only to the agent they are keyed to`() { + // Two agents declared, but options only keyed to the project agent. The other agent must not receive them. + val otelVersion = "2.29.0" + val dependencies = """ + javaagent project(':simple-agent') + javaagent 'io.opentelemetry.javaagent:opentelemetry-javaagent:$otelVersion' + """ + + createJavaagentProject( + dependencies, + extraBuildScript = + """ + javaagent { + agentOptions.put(':simple-agent', '12345:config.yaml') + } + """, + ) + val result = runBuild(listOf("assemble", "run")) + + assertTrue(result.output.contains("Hello World!")) + assertTrue(result.output.contains("Simple agent args: 12345:config.yaml")) + // the OpenTelemetry agent still installs; it simply receives no options + assertTrue( + result.output.contains("io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: $otelVersion"), + ) + } + + @Test fun `can pass agent options to application distribution start script`() { + val dependencies = """ + javaagent project(':simple-agent') + """ + + createJavaagentProject( + dependencies, + extraBuildScript = + """ + javaagent { + agentOptions.put(':simple-agent', '12345:config.yaml') + } + """, + ) + val result = runBuild(listOf("--configuration-cache", "build", "installDist", "execStartScript")) + + val applicationDistributionScript = + File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world$scriptExtension") + if (isWindows) { + val expectedWindowsDefaultJvmOpts = + """set DEFAULT_JVM_OPTS="-javaagent:%APP_HOME%\agent-libs\simple-agent.jar=12345:config.yaml" "-Xmx256m"""" + assertTrue(applicationDistributionScript.readText().contains(expectedWindowsDefaultJvmOpts)) + } else { + // The option string rides inside the agent's own backslash-escaped quoted token -- see issue #95. + val expectedDefaultJavaOpts = + """ +DEFAULT_JVM_OPTS="\"-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar=12345:config.yaml\" \"-Xmx256m\"" +""" + assertTrue(applicationDistributionScript.readText().contains(expectedDefaultJavaOpts)) + } + + // Verify the option was actually applied at runtime + assertTrue(result.output.contains("Simple agent args: 12345:config.yaml")) + } + @Test fun `can handle upgrade of agent with build cache`() { createJavaagentProject( """ @@ -266,6 +350,7 @@ DEFAULT_JVM_OPTS="\"-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar\" \"-X private fun createJavaagentProject( dependencies: String, applicationDefaultJvmArgs: String = "['-Xmx256m']", + extraBuildScript: String = "", ) { val helloWorldDir = File(functionalTestDir, "hello-world") Paths.get("src", "functionalTest", "resources", "hello-world-project").toFile().copyRecursively(helloWorldDir) @@ -344,6 +429,8 @@ DEFAULT_JVM_OPTS="\"-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar\" \"-X testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } + + $extraBuildScript """, ) } diff --git a/plugin/src/main/java/com/ryandens/javaagent/AgentOptionsResolver.java b/plugin/src/main/java/com/ryandens/javaagent/AgentOptionsResolver.java new file mode 100644 index 00000000..5f1149eb --- /dev/null +++ b/plugin/src/main/java/com/ryandens/javaagent/AgentOptionsResolver.java @@ -0,0 +1,83 @@ +package com.ryandens.javaagent; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.component.ComponentIdentifier; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; +import org.gradle.api.provider.Provider; + +/** + * Resolves the {@link JavaagentExtension#getAgentOptions() agent options} declared by coordinate + * into a map keyed by the resolved artifact file name, which is the join key every {@code + * -javaagent:} argument site can compute (the run/test sites hold the {@link File}, the + * distribution and jib sites use {@code file.getName()}). + * + *

This class is written in Java for the same reason as {@link JavaForkOptionsConfigurer}: to + * avoid the Gradle + * task input serialization limitation with Kotlin lambdas. + */ +public final class AgentOptionsResolver { + + /** Prevent instantiation for utility class. */ + private AgentOptionsResolver() {} + + /** + * Builds a {@code fileName -> options} map from the resolved artifacts of the provided + * configuration and the coordinate-keyed options declared on the {@link JavaagentExtension}. Only + * agents that have options declared appear in the resulting map. + * + *

The returned {@link Provider} is lazy and configuration-cache safe: it reads the + * configuration's resolved artifacts and the options map without touching {@code Project} at + * execution time. + * + * @param configuration the resolvable javaagent configuration whose artifacts should be matched + * @param coordinateOptions options keyed by dependency coordinate (see {@link + * JavaagentExtension}) + * @return provider of a map from resolved artifact file name to its option string + */ + public static Provider> optionsByFileName( + final Configuration configuration, final Provider> coordinateOptions) { + return configuration + .getIncoming() + .getArtifacts() + .getResolvedArtifacts() + .map( + artifacts -> { + final Map coordinates = coordinateOptions.getOrElse(new HashMap<>()); + final Map byFileName = new HashMap<>(); + if (coordinates.isEmpty()) { + return byFileName; + } + for (final ResolvedArtifactResult artifact : artifacts) { + final String key = coordinateKey(artifact.getId().getComponentIdentifier()); + final String options = coordinates.get(key); + if (options != null) { + byFileName.put(artifact.getFile().getName(), options); + } + } + return byFileName; + }); + } + + /** + * Derives the coordinate key used to look up options for a resolved artifact. Module dependencies + * are keyed by {@code group:name} (version intentionally excluded so options survive version + * bumps), project dependencies by their project path, and anything else by its display name as a + * best-effort fallback. + */ + private static String coordinateKey(final ComponentIdentifier identifier) { + if (identifier instanceof ModuleComponentIdentifier) { + final ModuleComponentIdentifier module = (ModuleComponentIdentifier) identifier; + return module.getGroup() + ":" + module.getModule(); + } + if (identifier instanceof ProjectComponentIdentifier) { + return ((ProjectComponentIdentifier) identifier).getProjectPath(); + } + return identifier.getDisplayName(); + } +} diff --git a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java index 5912925d..062b5933 100644 --- a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java +++ b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java @@ -5,6 +5,7 @@ import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.gradle.api.provider.Provider; @@ -28,9 +29,13 @@ private JavaForkOptionsConfigurer() {} * * @param javaForkOptions to be configured * @param javaagentConfiguration files to be added as javaagents + * @param optionsByFileName options to append after {@code =}, keyed by agent file name (see + * {@link AgentOptionsResolver}) */ public static void configureJavaForkOptions( - JavaForkOptions javaForkOptions, Provider> javaagentConfiguration) { + JavaForkOptions javaForkOptions, + Provider> javaagentConfiguration, + Provider> optionsByFileName) { final List list = new ArrayList<>(); //noinspection Convert2Lambda @@ -38,11 +43,14 @@ public static void configureJavaForkOptions( new CommandLineArgumentProvider() { @Override public Iterable asArguments() { + final Map options = optionsByFileName.get(); return javaagentConfiguration.get().stream() .map( file -> { try { - return "-javaagent:" + file.getCanonicalPath(); + final String options0 = options.get(file.getName()); + final String suffix = options0 == null ? "" : "=" + options0; + return "-javaagent:" + file.getCanonicalPath() + suffix; } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt index f7174a37..82256440 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt @@ -30,6 +30,10 @@ class JavaagentApplicationDistributionPlugin : project: Project, javaagentConfiguration: NamedDomainObjectProvider, ) { + val extension = project.extensions.getByType(JavaagentExtension::class.java) + val optionsByFileName = + AgentOptionsResolver.optionsByFileName(javaagentConfiguration.get(), extension.agentOptions) + project.extensions .getByType(DistributionContainer::class.java) .named(DistributionPlugin.MAIN_DISTRIBUTION_NAME) @@ -52,10 +56,11 @@ class JavaagentApplicationDistributionPlugin : listOf("-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar") .plus(it.defaultJvmOpts ?: listOf()) it.inputs.files(javaagentConfiguration) + it.inputs.property("agentOptions", extension.agentOptions) // custom start script generator that replaces the placeholder val agentFiles: Provider> = javaagentConfiguration.map { configuration -> configuration.files } - it.unixStartScriptGenerator = JavaagentAwareStartScriptGenerator(agentFiles, Platform.UNIX) - it.windowsStartScriptGenerator = JavaagentAwareStartScriptGenerator(agentFiles, Platform.WINDOWS) + it.unixStartScriptGenerator = JavaagentAwareStartScriptGenerator(agentFiles, Platform.UNIX, optionsByFileName) + it.windowsStartScriptGenerator = JavaagentAwareStartScriptGenerator(agentFiles, Platform.WINDOWS, optionsByFileName) } } } diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt index d446e04c..757bab87 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt @@ -19,6 +19,9 @@ class JavaagentApplicationRunPlugin : project: Project, javaagentConfiguration: NamedDomainObjectProvider, ) { + val extension = project.extensions.getByType(JavaagentExtension::class.java) + val optionsByFileName = + AgentOptionsResolver.optionsByFileName(javaagentConfiguration.get(), extension.agentOptions) // configure the run task to use the `javaagent` flag pointing to the dependency stored in the local Maven repository project.tasks.named(ApplicationPlugin.TASK_RUN_NAME, JavaExec::class.java).configure { // The agent jars are passed to the JVM via a CommandLineArgumentProvider (see @@ -34,7 +37,12 @@ class JavaagentApplicationRunPlugin : // carry its producing-task dependency (builtBy); consumers that synthesize a plain File must // preserve it (see the otel example's extendedAgent wiring). it.inputs.files(javaagentConfiguration) - JavaForkOptionsConfigurer.configureJavaForkOptions(it, javaagentConfiguration.map { configuration -> configuration.files }) + it.inputs.property("agentOptions", extension.agentOptions) + JavaForkOptionsConfigurer.configureJavaForkOptions( + it, + javaagentConfiguration.map { configuration -> configuration.files }, + optionsByFileName, + ) } } } diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt index 8a51619a..ffe084e6 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt @@ -12,6 +12,7 @@ import java.io.Writer class JavaagentAwareStartScriptGenerator( private val javaagentConfiguration: Provider>, private val platform: Platform, + private val optionsByFileName: Provider>, private val inner: ScriptGenerator = DefaultTemplateBasedStartScriptGenerator( platform.lineSeparator, @@ -25,7 +26,14 @@ class JavaagentAwareStartScriptGenerator( ) { inner.generateScript( details, - Fake(destination, javaagentConfiguration, platform.pathSeparator, platform.appHomeVar, platform.agentArgSeparator), + Fake( + destination, + javaagentConfiguration, + platform.pathSeparator, + platform.appHomeVar, + platform.agentArgSeparator, + optionsByFileName, + ), ) } @@ -53,6 +61,7 @@ class JavaagentAwareStartScriptGenerator( private val pathSeparator: String, private val appHomeVar: String, private val agentArgSeparator: String, + private val optionsByFileName: Provider>, ) : Writer() { override fun close() { inner.close() @@ -97,11 +106,15 @@ class JavaagentAwareStartScriptGenerator( .replace("-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar ", "") .replace("-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar", "") } else { + val options = optionsByFileName.get() str.replace( "-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar", files.joinToString( agentArgSeparator, - ) { jar -> "-javaagent:$appHomeVar${pathSeparator}agent-libs$pathSeparator${jar.name}" }, + ) { jar -> + val suffix = options[jar.name]?.let { "=$it" } ?: "" + "-javaagent:$appHomeVar${pathSeparator}agent-libs$pathSeparator${jar.name}$suffix" + }, ) } super.write(replace) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentBasePlugin.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentBasePlugin.kt index b60b1f63..6f964a00 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentBasePlugin.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentBasePlugin.kt @@ -9,6 +9,7 @@ import org.gradle.api.Project class JavaagentBasePlugin : Plugin { companion object { const val CONFIGURATION_NAME = "javaagent" + const val EXTENSION_NAME = "javaagent" } override fun apply(project: Project) { @@ -17,5 +18,10 @@ class JavaagentBasePlugin : Plugin { // we expect javaagents to come as shaded JARs it.isTransitive = false } + // Register the extension used to configure agent options. Created once here because the base plugin is + // applied idempotently by JavaagentPlugin, so the `javaagent { }` block is available to every plugin + // (application, distribution, test, jib). The extension name does not collide with the `javaagent` + // configuration, which is reached via the `configurations` container. + project.extensions.create(EXTENSION_NAME, JavaagentExtension::class.java) } } diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentExtension.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentExtension.kt new file mode 100644 index 00000000..bdf4ac93 --- /dev/null +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentExtension.kt @@ -0,0 +1,34 @@ +package com.ryandens.javaagent + +import org.gradle.api.provider.MapProperty + +/** + * Extension for configuring the javaagents attached by this plugin. + * + * Currently exposes [agentOptions], which associates agent option strings (the `=` portion of the + * `-javaagent:=` JVM argument, see the + * [java.lang.instrument package documentation](https://docs.oracle.com/en/java/javase/21/docs/api/java.instrument/java/lang/instrument/package-summary.html#starting-an-agent-from-the-command-line-interface-heading)) + * with the agent they apply to. + * + * Options are keyed by the dependency coordinate the user declared in the `javaagent` (or `testJavaagent`) + * configuration, so they survive version bumps and do not depend on the resolved jar file name: + * - module dependencies are keyed by `group:name` (version is intentionally excluded) + * - project dependencies are keyed by their project path, e.g. `:simple-agent` + * + * ``` + * dependencies { + * javaagent 'io.prometheus.jmx:jmx_prometheus_javaagent:0.20.0' + * } + * + * javaagent { + * agentOptions.put('io.prometheus.jmx:jmx_prometheus_javaagent', '12345:config.yaml') + * } + * ``` + */ +abstract class JavaagentExtension { + /** + * Maps an agent's dependency coordinate (`group:name` for module dependencies, project path for project + * dependencies) to the option string appended after `=` in the `-javaagent:=` argument. + */ + abstract val agentOptions: MapProperty +} diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentTestPlugin.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentTestPlugin.kt index 955859b2..80fa3d77 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentTestPlugin.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentTestPlugin.kt @@ -46,14 +46,20 @@ class JavaagentTestPlugin : val enabled = calculateEnabled(project, extension) + val javaagentExtension = project.extensions.getByType(JavaagentExtension::class.java) + val optionsByFileName = + AgentOptionsResolver.optionsByFileName(javaagentTestConfiguration.get(), javaagentExtension.agentOptions) + // configure the run task to use the `javaagent` flag pointing to the dependency stored in the local Maven repository project.tasks.named(JavaPlugin.TEST_TASK_NAME, Test::class.java).configure { if (enabled.get()) { + it.inputs.property("agentOptions", javaagentExtension.agentOptions) JavaForkOptionsConfigurer.configureJavaForkOptions( it, javaagentTestConfiguration.map { configuration -> configuration.files }, + optionsByFileName, ) } } diff --git a/simple-agent/src/main/java/com/ryandens/SimpleAgent.java b/simple-agent/src/main/java/com/ryandens/SimpleAgent.java index 064566c0..b25df492 100644 --- a/simple-agent/src/main/java/com/ryandens/SimpleAgent.java +++ b/simple-agent/src/main/java/com/ryandens/SimpleAgent.java @@ -6,5 +6,8 @@ public final class SimpleAgent { public static void premain(final String args, final Instrumentation instrumentation) { System.out.println("Hello from my simple agent!"); + if (args != null) { + System.out.println("Simple agent args: " + args); + } } }