Skip to content

Commit a7315c6

Browse files
authored
7181: Loading Compressor using ClassLoader configured through setServiceClassLoader (#7428)
1 parent edd51d4 commit a7315c6

File tree

13 files changed

+301
-55
lines changed

13 files changed

+301
-55
lines changed

exporters/common/src/main/java/io/opentelemetry/exporter/internal/compression/CompressorUtil.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
*/
2525
public final class CompressorUtil {
2626

27-
private static final Map<String, Compressor> compressorRegistry = buildCompressorRegistry();
27+
private static final Map<String, Compressor> compressorRegistry =
28+
buildCompressorRegistry(CompressorUtil.class.getClassLoader());
2829

2930
private CompressorUtil() {}
3031

@@ -36,19 +37,36 @@ private CompressorUtil() {}
3637
*/
3738
@Nullable
3839
public static Compressor validateAndResolveCompressor(String compressionMethod) {
39-
Set<String> supportedEncodings = compressorRegistry.keySet();
40-
Compressor compressor = compressorRegistry.get(compressionMethod);
40+
return validateAndResolveCompressor(compressionMethod, null);
41+
}
42+
43+
/**
44+
* Validate that the {@code compressionMethod} is "none" or matches a registered compressor.
45+
*
46+
* @param compressionMethod the compression method to validate and resolve
47+
* @param classLoader the class loader to use for loading SPI implementations, or null to use the
48+
* default
49+
* @return {@code null} if {@code compressionMethod} is "none" or the registered compressor
50+
* @throws IllegalArgumentException if no match is found
51+
*/
52+
@Nullable
53+
public static Compressor validateAndResolveCompressor(
54+
String compressionMethod, @Nullable ClassLoader classLoader) {
55+
Map<String, Compressor> registry =
56+
classLoader == null ? compressorRegistry : buildCompressorRegistry(classLoader);
57+
58+
Set<String> supportedEncodings = registry.keySet();
59+
Compressor compressor = registry.get(compressionMethod);
4160
checkArgument(
4261
"none".equals(compressionMethod) || compressor != null,
4362
"Unsupported compressionMethod. Compression method must be \"none\" or one of: "
4463
+ supportedEncodings.stream().collect(joining(",", "[", "]")));
4564
return compressor;
4665
}
4766

48-
private static Map<String, Compressor> buildCompressorRegistry() {
67+
private static Map<String, Compressor> buildCompressorRegistry(ClassLoader classLoader) {
4968
Map<String, Compressor> compressors = new HashMap<>();
50-
for (CompressorProvider spi :
51-
ServiceLoader.load(CompressorProvider.class, CompressorUtil.class.getClassLoader())) {
69+
for (CompressorProvider spi : ServiceLoader.load(CompressorProvider.class, classLoader)) {
5270
Compressor compressor = spi.getInstance();
5371
compressors.put(compressor.getEncoding(), compressor);
5472
}

exporters/common/src/main/java/io/opentelemetry/exporter/internal/grpc/GrpcExporterBuilder.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package io.opentelemetry.exporter.internal.grpc;
77

8+
import static java.util.Objects.requireNonNull;
9+
810
import io.grpc.Channel;
911
import io.grpc.ManagedChannel;
1012
import io.opentelemetry.api.GlobalOpenTelemetry;
@@ -13,6 +15,7 @@
1315
import io.opentelemetry.exporter.internal.ExporterBuilderUtil;
1416
import io.opentelemetry.exporter.internal.TlsConfigHelper;
1517
import io.opentelemetry.exporter.internal.compression.Compressor;
18+
import io.opentelemetry.exporter.internal.compression.CompressorUtil;
1619
import io.opentelemetry.exporter.internal.marshal.Marshaler;
1720
import io.opentelemetry.sdk.common.InternalTelemetryVersion;
1821
import io.opentelemetry.sdk.common.export.RetryPolicy;
@@ -115,6 +118,18 @@ public GrpcExporterBuilder<T> setCompression(@Nullable Compressor compressor) {
115118
return this;
116119
}
117120

121+
/**
122+
* Sets the method used to compress payloads. If unset, compression is disabled. Compression
123+
* method "gzip" and "none" are supported out of the box. Support for additional compression
124+
* methods is available by implementing {@link Compressor} and {@link CompressorProvider}.
125+
*/
126+
public GrpcExporterBuilder<T> setCompression(String compressionMethod) {
127+
requireNonNull(compressionMethod, "compressionMethod");
128+
Compressor compressor =
129+
CompressorUtil.validateAndResolveCompressor(compressionMethod, serviceClassLoader);
130+
return setCompression(compressor);
131+
}
132+
118133
public GrpcExporterBuilder<T> setTrustManagerFromCerts(byte[] trustedCertificatesPem) {
119134
tlsConfigHelper.setTrustManagerFromCerts(trustedCertificatesPem);
120135
return this;
@@ -158,8 +173,8 @@ public GrpcExporterBuilder<T> setInternalTelemetryVersion(
158173
return this;
159174
}
160175

161-
public GrpcExporterBuilder<T> setServiceClassLoader(ClassLoader servieClassLoader) {
162-
this.serviceClassLoader = servieClassLoader;
176+
public GrpcExporterBuilder<T> setServiceClassLoader(ClassLoader serviceClassLoader) {
177+
this.serviceClassLoader = serviceClassLoader;
163178
return this;
164179
}
165180

exporters/common/src/main/java/io/opentelemetry/exporter/internal/http/HttpExporterBuilder.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55

66
package io.opentelemetry.exporter.internal.http;
77

8+
import static java.util.Objects.requireNonNull;
9+
810
import io.opentelemetry.api.GlobalOpenTelemetry;
911
import io.opentelemetry.api.internal.ConfigUtil;
1012
import io.opentelemetry.api.metrics.MeterProvider;
1113
import io.opentelemetry.exporter.internal.ExporterBuilderUtil;
1214
import io.opentelemetry.exporter.internal.TlsConfigHelper;
1315
import io.opentelemetry.exporter.internal.compression.Compressor;
16+
import io.opentelemetry.exporter.internal.compression.CompressorUtil;
1417
import io.opentelemetry.exporter.internal.marshal.Marshaler;
1518
import io.opentelemetry.sdk.common.InternalTelemetryVersion;
1619
import io.opentelemetry.sdk.common.export.ProxyOptions;
@@ -95,6 +98,18 @@ public HttpExporterBuilder<T> setCompression(@Nullable Compressor compressor) {
9598
return this;
9699
}
97100

101+
/**
102+
* Sets the method used to compress payloads. If unset, compression is disabled. Compression
103+
* method "gzip" and "none" are supported out of the box. Support for additional compression
104+
* methods is available by implementing {@link Compressor} and {@link CompressorProvider}.
105+
*/
106+
public HttpExporterBuilder<T> setCompression(String compressionMethod) {
107+
requireNonNull(compressionMethod, "compressionMethod");
108+
Compressor compressor =
109+
CompressorUtil.validateAndResolveCompressor(compressionMethod, serviceClassLoader);
110+
return setCompression(compressor);
111+
}
112+
98113
public HttpExporterBuilder<T> addConstantHeaders(String key, String value) {
99114
constantHeaders.put(key, value);
100115
return this;
@@ -143,8 +158,8 @@ public HttpExporterBuilder<T> setProxyOptions(ProxyOptions proxyOptions) {
143158
return this;
144159
}
145160

146-
public HttpExporterBuilder<T> setServiceClassLoader(ClassLoader servieClassLoader) {
147-
this.serviceClassLoader = servieClassLoader;
161+
public HttpExporterBuilder<T> setServiceClassLoader(ClassLoader serviceClassLoader) {
162+
this.serviceClassLoader = serviceClassLoader;
148163
return this;
149164
}
150165

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.exporter.internal.compression;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
11+
import java.net.URL;
12+
import java.net.URLClassLoader;
13+
import org.junit.jupiter.api.Test;
14+
15+
class CompressorUtilTest {
16+
17+
@Test
18+
void validateAndResolveCompressor_none() {
19+
assertThat(CompressorUtil.validateAndResolveCompressor("none")).isNull();
20+
}
21+
22+
@Test
23+
void validateAndResolveCompressor_gzip() {
24+
assertThat(CompressorUtil.validateAndResolveCompressor("gzip"))
25+
.isEqualTo(GzipCompressor.getInstance());
26+
}
27+
28+
@Test
29+
void validateAndResolveCompressor_invalid() {
30+
assertThatThrownBy(() -> CompressorUtil.validateAndResolveCompressor("invalid"))
31+
.isInstanceOf(IllegalArgumentException.class)
32+
.hasMessageContaining("Unsupported compressionMethod");
33+
}
34+
35+
@Test
36+
void validateAndResolveCompressor_withClassLoader_none() {
37+
ClassLoader classLoader = CompressorUtilTest.class.getClassLoader();
38+
assertThat(CompressorUtil.validateAndResolveCompressor("none", classLoader)).isNull();
39+
}
40+
41+
@Test
42+
void validateAndResolveCompressor_withClassLoader_gzip() {
43+
ClassLoader classLoader = CompressorUtilTest.class.getClassLoader();
44+
assertThat(CompressorUtil.validateAndResolveCompressor("gzip", classLoader))
45+
.isEqualTo(GzipCompressor.getInstance());
46+
}
47+
48+
@Test
49+
void validateAndResolveCompressor_withClassLoader_invalid() {
50+
ClassLoader classLoader = CompressorUtilTest.class.getClassLoader();
51+
assertThatThrownBy(() -> CompressorUtil.validateAndResolveCompressor("invalid", classLoader))
52+
.isInstanceOf(IllegalArgumentException.class)
53+
.hasMessageContaining("Unsupported compressionMethod");
54+
}
55+
56+
@Test
57+
void validateAndResolveCompressor_emptyClassLoader() {
58+
// Create a class loader that cannot load CompressorProvider services
59+
ClassLoader emptyClassLoader = new URLClassLoader(new URL[0], null);
60+
61+
// Gzip should still work because it's hardcoded
62+
assertThat(CompressorUtil.validateAndResolveCompressor("gzip", emptyClassLoader))
63+
.isEqualTo(GzipCompressor.getInstance());
64+
65+
// None should still work because it doesn't require loading services
66+
assertThat(CompressorUtil.validateAndResolveCompressor("none", emptyClassLoader)).isNull();
67+
68+
// Any SPI-based compressor should not be available
69+
assertThatThrownBy(
70+
() -> CompressorUtil.validateAndResolveCompressor("base64", emptyClassLoader))
71+
.isInstanceOf(IllegalArgumentException.class)
72+
.hasMessageContaining("Unsupported compressionMethod");
73+
}
74+
75+
@Test
76+
void validateAndResolveCompressor_delegatesCorrectly() {
77+
// Test that single-parameter method delegates to two-parameter method
78+
assertThat(CompressorUtil.validateAndResolveCompressor("gzip"))
79+
.isEqualTo(
80+
CompressorUtil.validateAndResolveCompressor(
81+
"gzip", CompressorUtil.class.getClassLoader()));
82+
83+
assertThat(CompressorUtil.validateAndResolveCompressor("none"))
84+
.isEqualTo(
85+
CompressorUtil.validateAndResolveCompressor(
86+
"none", CompressorUtil.class.getClassLoader()));
87+
}
88+
}

exporters/common/src/test/java/io/opentelemetry/exporter/internal/grpc/GrpcExporterBuilderTest.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
package io.opentelemetry.exporter.internal.grpc;
77

88
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
910

11+
import io.opentelemetry.exporter.internal.compression.Compressor;
1012
import io.opentelemetry.exporter.internal.compression.GzipCompressor;
1113
import io.opentelemetry.exporter.internal.marshal.Marshaler;
1214
import io.opentelemetry.sdk.internal.StandardComponentId;
1315
import java.net.URI;
16+
import java.net.URL;
17+
import java.net.URLClassLoader;
1418
import org.junit.jupiter.api.BeforeEach;
1519
import org.junit.jupiter.api.Test;
1620

@@ -36,7 +40,7 @@ void compressionDefault() {
3640

3741
@Test
3842
void compressionNone() {
39-
builder.setCompression(null);
43+
builder.setCompression((Compressor) null);
4044

4145
assertThat(builder).extracting("compressor").isNull();
4246
}
@@ -50,8 +54,44 @@ void compressionGzip() {
5054

5155
@Test
5256
void compressionEnabledAndDisabled() {
53-
builder.setCompression(GzipCompressor.getInstance()).setCompression(null);
57+
builder.setCompression(GzipCompressor.getInstance()).setCompression((Compressor) null);
5458

5559
assertThat(builder).extracting("compressor").isNull();
5660
}
61+
62+
@Test
63+
void compressionString_none() {
64+
builder.setCompression("none");
65+
66+
assertThat(builder).extracting("compressor").isNull();
67+
}
68+
69+
@Test
70+
void compressionString_gzip() {
71+
builder.setCompression("gzip");
72+
73+
assertThat(builder).extracting("compressor").isEqualTo(GzipCompressor.getInstance());
74+
}
75+
76+
@Test
77+
void compressionString_invalid() {
78+
assertThatThrownBy(() -> builder.setCompression("invalid-compression"))
79+
.isInstanceOf(IllegalArgumentException.class)
80+
.hasMessageContaining("Unsupported compressionMethod");
81+
}
82+
83+
@Test
84+
void compressionString_usesServiceClassLoader() {
85+
// Create a class loader that cannot load CompressorProvider services
86+
ClassLoader emptyClassLoader = new URLClassLoader(new URL[0], null);
87+
builder.setServiceClassLoader(emptyClassLoader);
88+
89+
// This should still work because gzip compressor is hardcoded
90+
builder.setCompression("gzip");
91+
assertThat(builder).extracting("compressor").isEqualTo(GzipCompressor.getInstance());
92+
93+
// This should still work because "none" doesn't require loading services
94+
builder.setCompression("none");
95+
assertThat(builder).extracting("compressor").isNull();
96+
}
5797
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.exporter.internal.http;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
11+
import io.opentelemetry.exporter.internal.compression.Compressor;
12+
import io.opentelemetry.exporter.internal.compression.GzipCompressor;
13+
import io.opentelemetry.exporter.internal.marshal.Marshaler;
14+
import io.opentelemetry.sdk.internal.StandardComponentId;
15+
import java.net.URL;
16+
import java.net.URLClassLoader;
17+
import org.junit.jupiter.api.BeforeEach;
18+
import org.junit.jupiter.api.Test;
19+
20+
class HttpExporterBuilderTest {
21+
22+
private HttpExporterBuilder<Marshaler> builder;
23+
24+
@BeforeEach
25+
void setUp() {
26+
builder =
27+
new HttpExporterBuilder<>(
28+
StandardComponentId.ExporterType.OTLP_HTTP_SPAN_EXPORTER, "http://localhost:4318");
29+
}
30+
31+
@Test
32+
void compressionDefault() {
33+
assertThat(builder).extracting("compressor").isNull();
34+
}
35+
36+
@Test
37+
void compressionNone() {
38+
builder.setCompression((Compressor) null);
39+
40+
assertThat(builder).extracting("compressor").isNull();
41+
}
42+
43+
@Test
44+
void compressionGzip() {
45+
builder.setCompression(GzipCompressor.getInstance());
46+
47+
assertThat(builder).extracting("compressor").isEqualTo(GzipCompressor.getInstance());
48+
}
49+
50+
@Test
51+
void compressionEnabledAndDisabled() {
52+
builder.setCompression(GzipCompressor.getInstance()).setCompression((Compressor) null);
53+
54+
assertThat(builder).extracting("compressor").isNull();
55+
}
56+
57+
@Test
58+
void compressionString_none() {
59+
builder.setCompression("none");
60+
61+
assertThat(builder).extracting("compressor").isNull();
62+
}
63+
64+
@Test
65+
void compressionString_gzip() {
66+
builder.setCompression("gzip");
67+
68+
assertThat(builder).extracting("compressor").isEqualTo(GzipCompressor.getInstance());
69+
}
70+
71+
@Test
72+
void compressionString_invalid() {
73+
assertThatThrownBy(() -> builder.setCompression("invalid-compression"))
74+
.isInstanceOf(IllegalArgumentException.class)
75+
.hasMessageContaining("Unsupported compressionMethod");
76+
}
77+
78+
@Test
79+
void compressionString_usesServiceClassLoader() {
80+
// Create a class loader that cannot load CompressorProvider services
81+
ClassLoader emptyClassLoader = new URLClassLoader(new URL[0], null);
82+
builder.setServiceClassLoader(emptyClassLoader);
83+
84+
// This should still work because gzip compressor is hardcoded
85+
builder.setCompression("gzip");
86+
assertThat(builder).extracting("compressor").isEqualTo(GzipCompressor.getInstance());
87+
88+
// This should still work because "none" doesn't require loading services
89+
builder.setCompression("none");
90+
assertThat(builder).extracting("compressor").isNull();
91+
}
92+
}

0 commit comments

Comments
 (0)