Skip to content

Commit 77aafe6

Browse files
kuba-wuanuraagaMateusz Rzeszutektrask
authored
Static instrumenter - core (#236)
* static instrumenter core * Apply suggestions from code review Co-authored-by: Anuraag Agrawal <[email protected]> * post review changes * Apply suggestions from code review Co-authored-by: Mateusz Rzeszutek <[email protected]> * code review * Apply suggestions from code review Co-authored-by: Trask Stalnaker <[email protected]> * code review * code review * code review * code review * code review * code review * code review - file cleanup * forcefuly adding binary file Co-authored-by: Anuraag Agrawal <[email protected]> Co-authored-by: Mateusz Rzeszutek <[email protected]> Co-authored-by: Trask Stalnaker <[email protected]>
1 parent 2828016 commit 77aafe6

File tree

15 files changed

+535
-1
lines changed

15 files changed

+535
-1
lines changed

static-instrumenter/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
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.javaagent.StaticInstrumenter` 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.Main` class).
1010

1111
### Gradle plugin
1212

static-instrumenter/agent-instrumenter/build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,16 @@ plugins {
33
}
44

55
description = "OpenTelemetry Java Static Instrumentation Agent"
6+
7+
dependencies {
8+
implementation("org.slf4j:slf4j-api")
9+
runtimeOnly("org.slf4j:slf4j-simple")
10+
}
11+
12+
tasks {
13+
withType<JavaCompile>().configureEach {
14+
with(options) {
15+
release.set(11)
16+
}
17+
}
18+
}

static-instrumenter/agent-instrumenter/src/main/java/io/opentelemetry/contrib/.gitkeep

Whitespace-only changes.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.staticinstrumenter;
7+
8+
class ArchiveEntry {
9+
10+
private static final ArchiveEntry NOT_CLASS =
11+
new ArchiveEntry("", "", /* shouldInstrument= */ false);
12+
13+
private final String name;
14+
private final String path;
15+
private final boolean shouldInstrument;
16+
17+
private ArchiveEntry(String name, String path, boolean shouldInstrument) {
18+
this.name = name;
19+
this.path = path;
20+
this.shouldInstrument = shouldInstrument;
21+
}
22+
23+
static ArchiveEntry fromZipEntryName(String zipEntryName) {
24+
if (!isClass(zipEntryName)) {
25+
return NOT_CLASS;
26+
}
27+
String path = zipEntryName.substring(0, zipEntryName.indexOf(".class"));
28+
return new ArchiveEntry(className(path), path, !shouldBeSkipped(zipEntryName));
29+
}
30+
31+
private static boolean isClass(String path) {
32+
return path.endsWith(".class");
33+
}
34+
35+
private static String className(String path) {
36+
return path.replace("/", ".");
37+
}
38+
39+
private static boolean shouldBeSkipped(String zipEntryName) {
40+
return zipEntryName.startsWith("io.opentelemetry");
41+
}
42+
43+
String getName() {
44+
return name;
45+
}
46+
47+
String getPath() {
48+
return path;
49+
}
50+
51+
boolean shouldInstrument() {
52+
return shouldInstrument;
53+
}
54+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.staticinstrumenter;
7+
8+
import java.io.ByteArrayInputStream;
9+
import java.io.IOException;
10+
import java.io.InputStream;
11+
import java.util.Enumeration;
12+
import java.util.Map;
13+
import java.util.jar.JarEntry;
14+
import java.util.jar.JarFile;
15+
import java.util.jar.JarOutputStream;
16+
import java.util.zip.ZipEntry;
17+
import java.util.zip.ZipException;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
/** Represents an archive storing classes (JAR, WAR). */
22+
class ClassArchive {
23+
24+
interface Factory {
25+
ClassArchive createFor(JarFile source, Map<String, byte[]> instrumentedClasses);
26+
}
27+
28+
private static final Logger logger = LoggerFactory.getLogger(ClassArchive.class);
29+
30+
private final JarFile source;
31+
private final Map<String, byte[]> instrumentedClasses;
32+
33+
ClassArchive(JarFile source, Map<String, byte[]> instrumentedClasses) {
34+
this.source = source;
35+
this.instrumentedClasses = instrumentedClasses;
36+
}
37+
38+
void copyAllClassesTo(JarOutputStream outJar) throws IOException {
39+
40+
Enumeration<JarEntry> inEntries = source.entries();
41+
while (inEntries.hasMoreElements()) {
42+
copyEntry(inEntries.nextElement(), outJar);
43+
}
44+
}
45+
46+
private void copyEntry(JarEntry inEntry, JarOutputStream outJar) throws IOException {
47+
String inEntryName = inEntry.getName();
48+
ZipEntry outEntry =
49+
inEntryName.endsWith(".jar") ? new ZipEntry(inEntry) : new ZipEntry(inEntryName);
50+
51+
try (InputStream entryInputStream = getInputStreamForEntry(inEntry, outEntry)) {
52+
outJar.putNextEntry(outEntry);
53+
entryInputStream.transferTo(outJar);
54+
outJar.closeEntry();
55+
} catch (ZipException e) {
56+
if (!isEntryDuplicate(e)) {
57+
logger.error("Error while creating entry: {}", outEntry.getName(), e);
58+
throw e;
59+
}
60+
}
61+
}
62+
63+
private static boolean isEntryDuplicate(ZipException ze) {
64+
return ze.getMessage() != null && ze.getMessage().contains("duplicate");
65+
}
66+
67+
private InputStream getInputStreamForEntry(JarEntry inEntry, ZipEntry outEntry)
68+
throws IOException {
69+
70+
InputStream entryIn = null;
71+
ArchiveEntry entry = ArchiveEntry.fromZipEntryName(inEntry.getName());
72+
if (entry.shouldInstrument()) {
73+
String className = entry.getName();
74+
byte[] modified = instrumentedClasses.get(entry.getPath());
75+
if (modified != null) {
76+
logger.debug("Found instrumented class: " + className);
77+
entryIn = new ByteArrayInputStream(modified);
78+
outEntry.setSize(modified.length);
79+
}
80+
}
81+
return entryIn == null ? source.getInputStream(inEntry) : entryIn;
82+
}
83+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.staticinstrumenter;
7+
8+
class CurrentClass {
9+
10+
private static final ThreadLocal<TransformedClass> currentClass = new ThreadLocal<>();
11+
12+
private CurrentClass() {}
13+
14+
static TransformedClass getAndRemove() {
15+
TransformedClass tc = currentClass.get();
16+
currentClass.remove();
17+
return tc;
18+
}
19+
20+
static void set(TransformedClass clazz) {
21+
currentClass.set(clazz);
22+
}
23+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.staticinstrumenter;
7+
8+
import java.io.File;
9+
import java.io.FileOutputStream;
10+
import java.io.IOException;
11+
import java.lang.instrument.ClassFileTransformer;
12+
import java.util.Map;
13+
import java.util.concurrent.ConcurrentHashMap;
14+
import java.util.jar.JarFile;
15+
import java.util.jar.JarOutputStream;
16+
import java.util.zip.ZipEntry;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
20+
public class Main {
21+
22+
private static final Logger logger = LoggerFactory.getLogger(Main.class);
23+
24+
private static final Main INSTANCE = new Main(ClassArchive::new);
25+
26+
private final ClassArchive.Factory classArchiveFactory;
27+
28+
// key is slashy name, not dotty
29+
private final Map<String, byte[]> instrumentedClasses = new ConcurrentHashMap<>();
30+
private final Map<String, byte[]> additionalClasses = new ConcurrentHashMap<>();
31+
32+
public static void main(String[] args) throws Exception {
33+
34+
if (args.length != 1) {
35+
printUsage();
36+
return;
37+
}
38+
39+
File outDir = new File(args[0]);
40+
if (!outDir.exists()) {
41+
outDir.mkdir();
42+
}
43+
44+
String classPath = System.getProperty("java.class.path");
45+
logger.debug("Classpath (jars list): " + classPath);
46+
String[] jarsList = classPath.split(File.pathSeparator);
47+
48+
getInstance().saveTransformedJarsTo(jarsList, outDir);
49+
}
50+
51+
@SuppressWarnings("SystemOut")
52+
private static void printUsage() {
53+
System.out.println(
54+
"OpenTelemetry Java Static Instrumenter\n"
55+
+ "Usage:\njava "
56+
+ Main.class.getCanonicalName()
57+
+ " <output directory> (where instrumented archives will be stored)");
58+
}
59+
60+
public static Main getInstance() {
61+
return INSTANCE;
62+
}
63+
64+
public static ClassFileTransformer getPreTransformer() {
65+
return new PreTransformer();
66+
}
67+
68+
public static ClassFileTransformer getPostTransformer() {
69+
return new PostTransformer();
70+
}
71+
72+
// for testing purposes
73+
Main(ClassArchive.Factory classArchiveFactory) {
74+
this.classArchiveFactory = classArchiveFactory;
75+
}
76+
77+
// FIXME: java 9 / jmod support, proper handling of directories
78+
// FIXME: jmod in particular introduces weirdness with adding helpers to the dependencies
79+
80+
/**
81+
* Copies all class archives (JARs, WARs) to outDir. Classes that were instrumented and stored in
82+
* instrumentedClasses will get replaced with the new version. All classes added to
83+
* additionalClasses will be added to the new archive.
84+
*
85+
* @param outDir directory where jars will be written
86+
* @throws IOException in case of file operation problem
87+
*/
88+
public void saveTransformedJarsTo(String[] jarsList, File outDir) throws IOException {
89+
90+
for (String pathItem : jarsList) {
91+
logger.info("Classpath item processed: " + pathItem);
92+
if (isArchive(pathItem)) {
93+
saveArchiveTo(new File(pathItem), outDir);
94+
}
95+
}
96+
}
97+
98+
private static boolean isArchive(String pathItem) {
99+
return (pathItem.endsWith(".jar") || pathItem.endsWith(".war"));
100+
}
101+
102+
// FIXME: don't "instrument" our agent jar
103+
// FIXME: detect and warn on signed jars (and drop the signing bits)
104+
// FIXME: multiple jars with same name
105+
private void saveArchiveTo(File inFile, File outDir) throws IOException {
106+
107+
try (JarFile inJar = new JarFile(inFile);
108+
JarOutputStream outJar = jarOutputStreamFor(outDir, inFile.getName())) {
109+
ClassArchive inClassArchive = classArchiveFactory.createFor(inJar, instrumentedClasses);
110+
inClassArchive.copyAllClassesTo(outJar);
111+
injectAdditionalClassesTo(outJar);
112+
}
113+
}
114+
115+
private static JarOutputStream jarOutputStreamFor(File outDir, String fileName)
116+
throws IOException {
117+
File outFile = new File(outDir, fileName);
118+
return new JarOutputStream(new FileOutputStream(outFile));
119+
}
120+
121+
// FIXME: only relevant additional classes should be injected
122+
private void injectAdditionalClassesTo(JarOutputStream outJar) throws IOException {
123+
for (Map.Entry<String, byte[]> entry : additionalClasses.entrySet()) {
124+
String className = entry.getKey();
125+
byte[] classData = entry.getValue();
126+
127+
ZipEntry newEntry = new ZipEntry(className);
128+
outJar.putNextEntry(newEntry);
129+
if (classData != null) {
130+
newEntry.setSize(classData.length);
131+
outJar.write(classData);
132+
}
133+
outJar.closeEntry();
134+
135+
logger.debug("Additional class added: {}", className);
136+
}
137+
}
138+
139+
public Map<String, byte[]> getInstrumentedClasses() {
140+
return instrumentedClasses;
141+
}
142+
143+
public Map<String, byte[]> getAdditionalClasses() {
144+
return additionalClasses;
145+
}
146+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.staticinstrumenter;
7+
8+
import java.lang.instrument.ClassFileTransformer;
9+
import java.security.ProtectionDomain;
10+
import java.util.Arrays;
11+
import javax.annotation.Nullable;
12+
13+
public class PostTransformer implements ClassFileTransformer {
14+
@Override
15+
@Nullable
16+
public byte[] transform(
17+
ClassLoader loader,
18+
String className,
19+
Class<?> classBeingRedefined,
20+
ProtectionDomain protectionDomain,
21+
byte[] classfileBuffer) {
22+
23+
TransformedClass pre = CurrentClass.getAndRemove();
24+
25+
if (pre != null
26+
&& pre.getName().equals(className)
27+
&& !Arrays.equals(pre.getClasscode(), classfileBuffer)) {
28+
Main.getInstance().getInstrumentedClasses().put(className, classfileBuffer);
29+
}
30+
return null;
31+
}
32+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.staticinstrumenter;
7+
8+
import java.lang.instrument.ClassFileTransformer;
9+
import java.security.ProtectionDomain;
10+
import javax.annotation.Nullable;
11+
12+
public class PreTransformer implements ClassFileTransformer {
13+
14+
@Override
15+
@Nullable
16+
public byte[] transform(
17+
ClassLoader loader,
18+
String className,
19+
Class<?> classBeingRedefined,
20+
ProtectionDomain protectionDomain,
21+
byte[] classfileBuffer) {
22+
23+
CurrentClass.set(new TransformedClass(className, classfileBuffer));
24+
return null;
25+
}
26+
}

0 commit comments

Comments
 (0)