Skip to content

Commit 8e0c440

Browse files
authored
Static instrumenter logic - a new agent distro (#319)
* static agent distro * add app.jar to integration test & update README.md * update javadoc * add libs * add test classes * review part 1 * add comment to JarTest * review part 2 * review part 3 * remove libs * move repository definition * define snapshot repo in jmx-metrics & switch to new dependency in agent-extension
1 parent f319d32 commit 8e0c440

File tree

29 files changed

+678
-35
lines changed

29 files changed

+678
-35
lines changed

dependencyManagement/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ val DEPENDENCY_BOMS = listOf(
2020
"io.grpc:grpc-bom:1.42.1",
2121
"io.opentelemetry:opentelemetry-bom:1.13.0",
2222
"io.opentelemetry:opentelemetry-bom-alpha:1.13.0-alpha",
23+
"io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:1.14.0-alpha-SNAPSHOT",
2324
"org.testcontainers:testcontainers-bom:1.16.3"
2425
)
2526

jmx-metrics/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ repositories {
1818
includeGroupByRegex("""org\.terracotta.*""")
1919
}
2020
}
21+
maven {
22+
url = uri("https://oss.sonatype.org/content/repositories/snapshots")
23+
}
2124
mavenLocal()
2225
}
2326

settings.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ plugins {
1414
dependencyResolutionManagement {
1515
repositories {
1616
mavenCentral()
17+
maven {
18+
url = uri("https://oss.sonatype.org/content/repositories/snapshots")
19+
}
1720
mavenLocal()
1821
}
1922
}
@@ -47,4 +50,6 @@ include(":samplers")
4750
include(":static-instrumenter:agent-instrumenter")
4851
include(":static-instrumenter:gradle-plugin")
4952
include(":static-instrumenter:maven-plugin")
53+
include(":static-instrumenter:agent-extension")
54+
include(":static-instrumenter:bootstrap")
5055
include(":static-instrumenter:test-app")

static-instrumenter/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@
66

77
Module enhancing OpenTelemetry Java Agent for static instrumentation. The modified agent is capable of instrumenting and saving a new JAR with all relevant instrumentations applied and necessary helper class-code included.
88

9-
In order to statically instrument a JAR, modified agent needs to be both attached (`-javaagent:`) and run as the main method (`io.opentelemetry.contrib.staticinstrumenter.Main` class).
9+
In order to statically instrument a JAR, modified agent needs to be both attached (`-javaagent:`) and run as the main method (`io.opentelemetry.contrib.staticinstrumenter.agent.main.Main` class).
10+
:
11+
12+
To instrument you app use `opentelemetry-static-agent.jar` and pass the main class of the agent:
13+
14+
`java -javaagent:opentelemetry-static-agent.jar -cp your-app.jar io.opentelemetry.contrib.staticinstrumenter.agent.main.Main output-folder`
15+
16+
To run an instrumented app pass the `-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.contextStorageProvider=default` option and add `no-inst-agent.jar` to the classpath:
17+
18+
`java -Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.contextStorageProvider=default -cp output-folder/your-app.jar:no-inst-agent.jar org.example.YourMainClass`
1019

1120
### Gradle plugin
1221

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
plugins {
2+
id("otel.java-conventions")
3+
}
4+
5+
description = "Extension for OpenTelemetry Java Agent"
6+
7+
dependencies {
8+
annotationProcessor("com.google.auto.service:auto-service")
9+
compileOnly("com.google.auto.service:auto-service")
10+
11+
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling")
12+
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")
13+
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api")
14+
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
15+
compileOnly("io.opentelemetry.javaagent:opentelemetry-muzzle")
16+
17+
compileOnly(project(":static-instrumenter:bootstrap"))
18+
}
19+
20+
tasks {
21+
withType<JavaCompile>().configureEach {
22+
with(options) {
23+
release.set(11)
24+
}
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.staticinstrumenter.config;
7+
8+
import com.google.auto.service.AutoService;
9+
import io.opentelemetry.instrumentation.api.config.Config;
10+
import io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesBuilder;
11+
import io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesConfigurer;
12+
13+
/**
14+
* Makes classes from {@link io.opentelemetry.contrib.staticinstrumenter.agent.main} package
15+
* available both in agent's premain and static instrumenter's main.
16+
*/
17+
@AutoService(BootstrapPackagesConfigurer.class)
18+
public class StaticPackagesConfigurer implements BootstrapPackagesConfigurer {
19+
20+
@Override
21+
public void configure(Config config, BootstrapPackagesBuilder builder) {
22+
builder.add("io.opentelemetry.contrib.staticinstrumenter.agent.main");
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.staticinstrumenter.extension;
7+
8+
import io.opentelemetry.contrib.staticinstrumenter.agent.main.AdditionalClasses;
9+
import io.opentelemetry.javaagent.tooling.HelperInjectorListener;
10+
import java.util.Map;
11+
12+
/**
13+
* A listener to be registered in {@link io.opentelemetry.javaagent.tooling.HelperInjector}. It
14+
* saves all additional classes created by the agent to the AdditionalClasses class.
15+
*/
16+
public class AdditionalClassesInjectorListener implements HelperInjectorListener {
17+
18+
@Override
19+
public void onInjection(Map<String, byte[]> classnameToBytes) {
20+
for (Map.Entry<String, byte[]> classEntry : classnameToBytes.entrySet()) {
21+
String classFileName = classEntry.getKey().replace(".", "/") + ".class";
22+
AdditionalClasses.put(classFileName, classEntry.getValue());
23+
}
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.staticinstrumenter.extension;
7+
8+
import com.google.auto.service.AutoService;
9+
import io.opentelemetry.instrumentation.api.config.Config;
10+
import io.opentelemetry.javaagent.extension.AgentListener;
11+
import io.opentelemetry.javaagent.tooling.HelperInjector;
12+
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
13+
14+
/**
15+
* Configures a listener on {@link io.opentelemetry.javaagent.tooling.HelperInjector} before the
16+
* agents starts. The listener enables passing additional classes created by the agent to the static
17+
* instrumenter, which in turn saves them into the app jar.
18+
*/
19+
@AutoService(AgentListener.class)
20+
public class AdditionalClassesInjectorListenerInstaller implements AgentListener {
21+
22+
@Override
23+
public void beforeAgent(
24+
Config config, AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
25+
HelperInjector.setHelperInjectorListener(new AdditionalClassesInjectorListener());
26+
}
27+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,134 @@
1+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2+
13
plugins {
24
id("otel.java-conventions")
5+
id("com.github.johnrengelman.shadow") version "7.1.2"
6+
id("org.unbroken-dome.test-sets") version "4.0.0"
37
}
48

59
description = "OpenTelemetry Java Static Instrumentation Agent"
610

11+
val javaagentLibs: Configuration by configurations.creating {
12+
isCanBeResolved = true
13+
isCanBeConsumed = false
14+
}
15+
16+
val bootstrapLibs: Configuration by configurations.creating
17+
configurations.getByName("implementation").extendsFrom(bootstrapLibs)
18+
19+
val javaagent: Configuration by configurations.creating
20+
configurations.getByName("implementation").extendsFrom(javaagent)
21+
722
dependencies {
23+
annotationProcessor("com.google.auto.service:auto-service")
24+
compileOnly("com.google.auto.service:auto-service")
25+
826
implementation("org.slf4j:slf4j-api")
927
runtimeOnly("org.slf4j:slf4j-simple")
28+
29+
// TODO: remove snapshot once new agent is released
30+
javaagent("io.opentelemetry.javaagent:opentelemetry-javaagent:1.14.0-SNAPSHOT")
31+
32+
bootstrapLibs(project(":static-instrumenter:bootstrap"))
33+
javaagentLibs(project(":static-instrumenter:agent-extension"))
34+
}
35+
36+
// TODO: migrate to JVM test suite plugin
37+
testSets {
38+
create("integrationTest")
1039
}
1140

1241
tasks {
42+
43+
val relocateJavaagentLibs by registering(ShadowJar::class) {
44+
configurations = listOf(javaagentLibs)
45+
duplicatesStrategy = DuplicatesStrategy.FAIL
46+
archiveFileName.set("javaagentLibs-relocated.jar")
47+
}
48+
49+
shadowJar {
50+
configurations = listOf(javaagent, bootstrapLibs)
51+
52+
dependsOn(relocateJavaagentLibs)
53+
isolateClasses(relocateJavaagentLibs.get().outputs.files)
54+
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
55+
56+
manifest {
57+
attributes.put("Main-Class", "io.opentelemetry.contrib.staticinstrumenter.agent.main.Main")
58+
attributes.put("Agent-Class", "io.opentelemetry.contrib.staticinstrumenter.agent.OpenTelemetryStaticAgent")
59+
attributes.put("Premain-Class", "io.opentelemetry.contrib.staticinstrumenter.agent.OpenTelemetryStaticAgent")
60+
attributes.put("Can-Redefine-Classes", "true")
61+
attributes.put("Can-Retransform-Classes", "true")
62+
attributes.put("Implementation-Vendor", "OpenTelemetry")
63+
attributes.put("Implementation-Version", "demo-${project.version}")
64+
}
65+
}
66+
67+
// TODO: the resulting jar contains both inst and unpacked inst,
68+
// but we need only the unpacked inst here
69+
val createNoInstAgent by registering(ShadowJar::class) {
70+
71+
configurations = listOf(javaagent)
72+
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
73+
74+
archiveClassifier.set("no-inst")
75+
76+
inputs.files.forEach {
77+
if (it.name.endsWith(".jar")) {
78+
from(zipTree(it)) {
79+
include("inst/")
80+
eachFile {
81+
// drop inst/
82+
relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray())
83+
rename("(.*)\\.classdata\$", "\$1.class")
84+
}
85+
}
86+
}
87+
}
88+
}
89+
90+
withType<ShadowJar>().configureEach {
91+
// we depend on opentelemetry-instrumentation-api in agent-extension, so we need to relocate its usage
92+
relocate("io.opentelemetry.instrumentation.api", "io.opentelemetry.javaagent.shaded.instrumentation.api")
93+
}
94+
1395
withType<JavaCompile>().configureEach {
1496
with(options) {
1597
release.set(11)
1698
}
1799
}
100+
101+
assemble {
102+
dependsOn(shadowJar, createNoInstAgent)
103+
}
104+
105+
val integrationTest by existing(Test::class) {
106+
dependsOn(assemble)
107+
jvmArgumentProviders.add(AgentJarsProvider(shadowJar.flatMap { it.archiveFile }, createNoInstAgent.flatMap { it.archiveFile }))
108+
}
109+
}
110+
111+
fun CopySpec.isolateClasses(jars: Iterable<File>) {
112+
jars.forEach {
113+
from(zipTree(it)) {
114+
into("inst")
115+
rename("^(.*)\\.class\$", "\$1.classdata")
116+
// Rename LICENSE file since it clashes with license dir on non-case sensitive FSs (i.e. Mac)
117+
rename("""^LICENSE$""", "LICENSE.renamed")
118+
exclude("META-INF/INDEX.LIST")
119+
exclude("META-INF/*.DSA")
120+
exclude("META-INF/*.SF")
121+
}
122+
}
123+
}
124+
125+
class AgentJarsProvider(
126+
@InputFile
127+
@PathSensitive(PathSensitivity.RELATIVE)
128+
val agentJar: Provider<RegularFile>,
129+
@InputFile
130+
@PathSensitive(PathSensitivity.RELATIVE)
131+
val noInstAgentJar: Provider<RegularFile>
132+
) : CommandLineArgumentProvider {
133+
override fun asArguments(): Iterable<String> = listOf("-Dagent=${file(agentJar).path}", "-Dno.inst.agent=${file(noInstAgentJar).path}")
18134
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.net.URL;
11+
import java.nio.charset.StandardCharsets;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
import java.util.concurrent.TimeUnit;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.io.TempDir;
17+
18+
final class JarTest {
19+
20+
private static final String INSTRUMENTATION_MAIN =
21+
"io.opentelemetry.contrib.staticinstrumenter.agent.main.Main";
22+
private static final String APP_MAIN = "org.example.SingleGetter";
23+
24+
@TempDir public Path outPath;
25+
26+
@Test
27+
void testSampleJar() throws IOException, InterruptedException {
28+
Path agentPath = Path.of(System.getProperty("agent"));
29+
Path noInstAgentPath = Path.of(System.getProperty("no.inst.agent"));
30+
// jar created in test-app module
31+
Path appPath = getPath("app.jar");
32+
33+
ProcessBuilder instrumentationProcessBuilder =
34+
new ProcessBuilder(
35+
"java",
36+
"-javaagent:" + agentPath,
37+
"-cp",
38+
appPath.toString(),
39+
INSTRUMENTATION_MAIN,
40+
outPath.toString());
41+
42+
Process instrumentationProcess = instrumentationProcessBuilder.start();
43+
InputStream instrumentationIn = instrumentationProcess.getErrorStream();
44+
instrumentationProcess.waitFor(10, TimeUnit.SECONDS);
45+
46+
String instrumentationLog =
47+
new String(instrumentationIn.readAllBytes(), StandardCharsets.UTF_8);
48+
assertThat(instrumentationProcess.exitValue()).isEqualTo(0);
49+
assertThat(instrumentationLog).isNotEmpty();
50+
51+
Path resultAppPath = outPath.resolve("app.jar");
52+
assertThat(Files.exists(resultAppPath)).isTrue();
53+
54+
ProcessBuilder runtimeProcessBuilder =
55+
new ProcessBuilder(
56+
"java",
57+
"-Dotel.traces.exporter=logging",
58+
"-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.contextStorageProvider=default",
59+
"-cp",
60+
String.format("%s:%s", resultAppPath, noInstAgentPath),
61+
APP_MAIN);
62+
63+
Process runtimeProcess = runtimeProcessBuilder.start();
64+
InputStream err = runtimeProcess.getErrorStream();
65+
runtimeProcess.waitFor(10, TimeUnit.SECONDS);
66+
67+
assertThat(runtimeProcess.exitValue()).isEqualTo(0);
68+
assertThat(new String(err.readAllBytes(), StandardCharsets.UTF_8))
69+
.startsWith(
70+
"[main] INFO io.opentelemetry.exporter.logging.LoggingSpanExporter - 'HTTP GET'");
71+
}
72+
73+
private static Path getPath(String resourceName) {
74+
URL resourceURL = JarTest.class.getResource(resourceName);
75+
assertThat(resourceURL).isNotNull();
76+
return Path.of(resourceURL.getPath());
77+
}
78+
}

0 commit comments

Comments
 (0)