Skip to content

Commit 1029455

Browse files
authored
fix: Fixed jar shading (#1554)
Signed-off-by: dhoard <doug.hoard@gmail.com>
1 parent ecc2e5d commit 1029455

10 files changed

Lines changed: 275 additions & 31 deletions

File tree

collector/src/main/java/io/prometheus/jmx/JmxScraper.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -689,17 +689,39 @@ public void recordBean(
689689
String attrType,
690690
String attrDescription,
691691
Object value) {
692-
System.out.println(new StringBuilder(256)
692+
System.out.println(escapeControlCharacters(new StringBuilder(256)
693693
.append(domain)
694694
.append(beanProperties)
695695
.append(attrKeys)
696696
.append(attrName)
697697
.append(": ")
698698
.append(value)
699-
.toString());
699+
.toString()));
700700
}
701701
}
702702

703+
/**
704+
* Escapes every C0 control character (U+0000-U+001F) and DEL (U+007F) in
705+
* {@code value} as a two-digit lowercase hexadecimal escape (for example
706+
* {@code \x01}), so the resulting string is plain text. All other
707+
* characters are returned unchanged.
708+
*
709+
* @param value the string to escape
710+
* @return the escaped string
711+
*/
712+
private static String escapeControlCharacters(String value) {
713+
StringBuilder escaped = new StringBuilder(value.length());
714+
for (int i = 0; i < value.length(); i++) {
715+
char c = value.charAt(i);
716+
if (c < 0x20 || c == 0x7f) {
717+
escaped.append('\\').append('x').append(String.format("%02x", (int) c));
718+
} else {
719+
escaped.append(c);
720+
}
721+
}
722+
return escaped.toString();
723+
}
724+
703725
/**
704726
* Convenience method to run standalone.
705727
*/

collector/src/test/java/io/prometheus/jmx/JmxScraperTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,37 @@ void stdoutWriterRecordsBeanToStdout() {
502502
System.setOut(originalOut);
503503
}
504504
}
505+
506+
@Test
507+
void stdoutWriterEscapesControlCharactersInOutput() {
508+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
509+
PrintStream originalOut = System.out;
510+
System.setOut(new PrintStream(baos, true));
511+
try {
512+
JmxScraper.MBeanReceiver stdoutWriter = createStdoutWriter();
513+
514+
LinkedHashMap<String, String> beanProperties = new LinkedHashMap<>();
515+
beanProperties.put("type", "Colliding\u0001Name");
516+
List<String> attrKeys = new ArrayList<>();
517+
attrKeys.add("key1");
518+
519+
stdoutWriter.recordBean(
520+
"test.domain",
521+
beanProperties,
522+
Collections.emptyMap(),
523+
attrKeys,
524+
"AttrName",
525+
"java.lang.String",
526+
"desc",
527+
"hello");
528+
529+
String output = baos.toString();
530+
assertThat(output).contains("Colliding\\x01Name");
531+
assertThat(output).doesNotContain("Colliding\u0001Name");
532+
} finally {
533+
System.setOut(originalOut);
534+
}
535+
}
505536
}
506537

507538
@Nested

jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/tools/CustomServiceTransformer.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.HashMap;
2626
import java.util.List;
2727
import java.util.Map;
28+
import java.util.Objects;
2829
import java.util.jar.JarEntry;
2930
import java.util.jar.JarOutputStream;
3031
import org.apache.maven.plugins.shade.relocation.Relocator;
@@ -37,22 +38,32 @@
3738
* and entries. This is used during shading to prevent conflicts between the shaded and unshaded
3839
* versions of the same library.
3940
*
40-
* <p>The prefix {@code e1723a08afd7bca35570fd31a7656f59.} is added to:
41+
* <p>A unique shading prefix is added to:
4142
*
4243
* <ul>
4344
* <li>Service file names (e.g., META-INF/services/MyService becomes
44-
* META-INF/services/e1723a08afd7bca35570fd31a7656f59.MyService)
45+
* META-INF/services/{@code <prefix>}MyService)
4546
* <li>Service implementation class names within the files
4647
* </ul>
4748
*
49+
* <p>The prefix is configured by the Maven Shade plugin via the {@code <prefix>}
50+
* element; it is generated at build time so that each build uses a fresh
51+
* namespace. The default prefix is retained for backwards compatibility when
52+
* no prefix is configured.
53+
*
4854
* <p>This class is used during the Maven build process and is not used at runtime.
4955
*/
5056
public class CustomServiceTransformer implements ResourceTransformer {
5157

58+
/**
59+
* Default prefix to add to service names and entries when none is configured.
60+
*/
61+
private static final String DEFAULT_PREFIX = "e1723a08afd7bca35570fd31a7656f59.";
62+
5263
/**
5364
* Prefix to add to service names and entries.
5465
*/
55-
private static final String PREFIX = "e1723a08afd7bca35570fd31a7656f59.";
66+
private String prefix = DEFAULT_PREFIX;
5667

5768
/**
5869
* META-INF services directory path.
@@ -73,6 +84,15 @@ public CustomServiceTransformer() {
7384
// Intentionally empty
7485
}
7586

87+
/**
88+
* Sets the prefix to add to service names and entries.
89+
*
90+
* @param prefix the prefix to add, must not be {@code null}
91+
*/
92+
public void setPrefix(String prefix) {
93+
this.prefix = Objects.requireNonNull(prefix);
94+
}
95+
7696
/**
7797
* Determines whether the resource is a META-INF/services file that should be transformed.
7898
*
@@ -105,7 +125,7 @@ public void processResource(String resource, InputStream is, List<Relocator> rel
105125
.filter(line -> !line.isEmpty())
106126
.map(line -> {
107127
// Avoid double prefixing
108-
return line.startsWith(PREFIX) ? line : PREFIX + line;
128+
return line.startsWith(prefix) ? line : prefix + line;
109129
})
110130
.forEach(line -> {
111131
if (!entries.contains(line)) {
@@ -129,7 +149,7 @@ public boolean hasTransformedResource() {
129149
* Writes the collected service entries to the JAR with prefixed filenames and entries.
130150
*
131151
* <p>Each service file is written to a new path where the service name is prefixed with
132-
* {@value #PREFIX}, and all implementation class entries within the file are also prefixed.
152+
* {@link #prefix}, and all implementation class entries within the file are also prefixed.
133153
*
134154
* @param jos the JAR output stream to write to
135155
* @throws IOException if writing to the JAR fails
@@ -140,7 +160,7 @@ public void modifyOutputStream(JarOutputStream jos) throws IOException {
140160
String originalPath = entry.getKey();
141161
List<String> lines = entry.getValue();
142162

143-
String newPath = SERVICES_DIR + PREFIX + originalPath.substring(SERVICES_DIR.length());
163+
String newPath = SERVICES_DIR + prefix + originalPath.substring(SERVICES_DIR.length());
144164

145165
jos.putNextEntry(new JarEntry(newPath));
146166
for (String line : lines) {

jmx_prometheus_common/src/test/java/io/prometheus/jmx/common/tools/CustomServiceTransformerTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
import java.nio.charset.StandardCharsets;
2525
import java.util.ArrayList;
2626
import java.util.Collections;
27+
import java.util.List;
28+
import java.util.jar.JarEntry;
29+
import java.util.jar.JarInputStream;
2730
import java.util.jar.JarOutputStream;
2831
import org.junit.jupiter.api.Nested;
2932
import org.junit.jupiter.api.Test;
@@ -165,6 +168,31 @@ void modifyOutputStreamWritesPrefixedServiceName() throws IOException {
165168
assertThat(baos.toByteArray().length).isGreaterThan(0);
166169
}
167170

171+
@Test
172+
void modifyOutputStreamUsesConfiguredPrefix() throws IOException {
173+
CustomServiceTransformer transformer = new CustomServiceTransformer();
174+
transformer.setPrefix("ab12cd34.");
175+
byte[] content = "com.example.Impl1\n".getBytes(StandardCharsets.UTF_8);
176+
transformer.processResource(
177+
"META-INF/services/com.example.MyService",
178+
new ByteArrayInputStream(content),
179+
Collections.emptyList());
180+
181+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
182+
try (JarOutputStream jos = new JarOutputStream(baos)) {
183+
transformer.modifyOutputStream(jos);
184+
}
185+
186+
try (JarInputStream jis = new JarInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
187+
List<String> names = new ArrayList<>();
188+
JarEntry jarEntry;
189+
while ((jarEntry = jis.getNextJarEntry()) != null) {
190+
names.add(jarEntry.getName());
191+
}
192+
assertThat(names).containsExactly("META-INF/services/ab12cd34.com.example.MyService");
193+
}
194+
}
195+
168196
@Test
169197
void modifyOutputStreamWritesMultipleServices() throws IOException {
170198
CustomServiceTransformer transformer = new CustomServiceTransformer();

jmx_prometheus_isolator_javaagent/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@
6060
<exclude>org.vafer:jdependency</exclude>
6161
</excludes>
6262
</artifactSet>
63+
<relocations>
64+
<relocation>
65+
<shadedPattern>${isolator.shade.prefix}.</shadedPattern>
66+
<includes>
67+
<include>io.prometheus.jmx.**</include>
68+
</includes>
69+
<excludes>
70+
<exclude>io.prometheus.jmx.IsolatorJavaAgent</exclude>
71+
<exclude>io.prometheus.jmx.JavaAgent</exclude>
72+
</excludes>
73+
</relocation>
74+
</relocations>
6375
<transformers>
6476
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
6577
<manifestEntries>

jmx_prometheus_isolator_javaagent/src/main/java/io/prometheus/jmx/JarClassLoader.java

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import java.io.IOException;
2121
import java.io.InputStream;
2222
import java.util.HashMap;
23+
import java.util.HashSet;
2324
import java.util.Map;
25+
import java.util.Set;
2426
import java.util.jar.Attributes;
2527
import java.util.jar.JarEntry;
2628
import java.util.jar.JarFile;
@@ -51,11 +53,11 @@
5153
* <li>Fall back to the standard parent-first chain ({@code super.loadClass()})
5254
* </ol>
5355
*
54-
* <p>Each exporter JAR is fully shaded under the
55-
* {@code e1723a08afd7bca35570fd31a7656f59.} prefix, so all dependency classes are
56-
* namespace-scoped to that JAR. The self-first strategy ensures the exact version packaged
57-
* inside each JAR is used, preventing {@link ClassCastException} and other version conflicts
58-
* when multiple exporter instances run in the same JVM.
56+
* <p>Each exporter JAR is fully shaded under a unique build-time prefix, so all
57+
* dependency classes are namespace-scoped to that JAR. The self-first strategy
58+
* ensures the exact version packaged inside each JAR is used, preventing
59+
* {@link ClassCastException} and other version conflicts when multiple exporter
60+
* instances run in the same JVM.
5961
*
6062
* <p>Thread-safety: This class is thread-safe. Class loading operations are synchronized by
6163
* the parent ClassLoader.
@@ -87,6 +89,11 @@ public class JarClassLoader extends ClassLoader {
8789
*/
8890
private final Map<String, byte[]> classBytes = new HashMap<>();
8991

92+
/**
93+
* Package names already defined by this classloader.
94+
*/
95+
private final Set<String> definedPackages = new HashSet<>();
96+
9097
/**
9198
* Constructs a classloader that loads classes from the specified JAR file.
9299
*
@@ -222,25 +229,28 @@ private byte[] readAllBytes(InputStream inputStream) throws IOException {
222229
/**
223230
* Ensures that the package for a class is defined before loading.
224231
*
225-
* <p>For classes with the shading prefix, the package is defined with implementation
226-
* title and version from the JAR manifest.
232+
* <p>Packages are defined with the implementation title and version from the JAR
233+
* manifest. Only packages defined by this classloader are considered, so packages
234+
* defined by a parent classloader (including packages of the same name defined for
235+
* classes loaded from the system classpath) do not prevent the package metadata from
236+
* being attached here.
227237
*
228238
* @param className the fully qualified class name
229239
*/
230240
private void ensurePackageDefined(String className) {
231241
int index = className.lastIndexOf('.');
232242
if (index != -1) {
233243
String packageName = className.substring(0, index);
234-
if (getPackage(packageName) == null) {
235-
String title = null;
236-
String version = null;
237-
238-
if (className.startsWith("e1723a08afd7bca35570fd31a7656f59")) {
239-
title = manifestMap.get(IMPLEMENTATION_TITLE);
240-
version = manifestMap.get(IMPLEMENTATION_VERSION);
241-
}
242-
243-
definePackage(packageName, null, null, null, title, version, null, null);
244+
if (definedPackages.add(packageName)) {
245+
definePackage(
246+
packageName,
247+
null,
248+
null,
249+
null,
250+
manifestMap.get(IMPLEMENTATION_TITLE),
251+
manifestMap.get(IMPLEMENTATION_VERSION),
252+
null,
253+
null);
244254
}
245255
}
246256
}

jmx_prometheus_javaagent/pom.xml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
</filters>
107107
<relocations>
108108
<relocation>
109-
<shadedPattern>e1723a08afd7bca35570fd31a7656f59.</shadedPattern>
109+
<shadedPattern>${exporter.shade.prefix}.</shadedPattern>
110110
<includes>
111111
<include>com.**</include>
112112
<include>google.**</include>
@@ -125,7 +125,7 @@
125125
</relocation>
126126
<relocation>
127127
<pattern>io.prometheus.metrics.shaded</pattern>
128-
<shadedPattern>e1723a08afd7bca35570fd31a7656f59.io.prometheus.metrics.shaded</shadedPattern>
128+
<shadedPattern>${exporter.shade.prefix}.io.prometheus.metrics.shaded</shadedPattern>
129129
</relocation>
130130
</relocations>
131131
<transformers>
@@ -137,7 +137,9 @@
137137
<Implementation-Title>${project.artifactId}</Implementation-Title>
138138
</manifestEntries>
139139
</transformer>
140-
<transformer implementation="io.prometheus.jmx.common.tools.CustomServiceTransformer"/>
140+
<transformer implementation="io.prometheus.jmx.common.tools.CustomServiceTransformer">
141+
<prefix>${exporter.shade.prefix}.</prefix>
142+
</transformer>
141143
</transformers>
142144
</configuration>
143145
</execution>

jmx_prometheus_standalone/pom.xml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
</filters>
112112
<relocations>
113113
<relocation>
114-
<shadedPattern>e1723a08afd7bca35570fd31a7656f59.</shadedPattern>
114+
<shadedPattern>${exporter.shade.prefix}.</shadedPattern>
115115
<includes>
116116
<include>com.**</include>
117117
<include>google.**</include>
@@ -129,7 +129,7 @@
129129
</relocation>
130130
<relocation>
131131
<pattern>io.prometheus.metrics.shaded</pattern>
132-
<shadedPattern>e1723a08afd7bca35570fd31a7656f59.io.prometheus.metrics.shaded</shadedPattern>
132+
<shadedPattern>${exporter.shade.prefix}.io.prometheus.metrics.shaded</shadedPattern>
133133
</relocation>
134134
</relocations>
135135
<transformers>
@@ -140,7 +140,9 @@
140140
<Implementation-Title>${project.artifactId}</Implementation-Title>
141141
</manifestEntries>
142142
</transformer>
143-
<transformer implementation="io.prometheus.jmx.common.tools.CustomServiceTransformer"/>
143+
<transformer implementation="io.prometheus.jmx.common.tools.CustomServiceTransformer">
144+
<prefix>${exporter.shade.prefix}.</prefix>
145+
</transformer>
144146
</transformers>
145147
</configuration>
146148
</execution>

0 commit comments

Comments
 (0)