From 27af04d9ac42136ad0cc6f73568acbbdaf451bf7 Mon Sep 17 00:00:00 2001 From: Zapolyarny Date: Sat, 18 Jul 2026 01:29:55 +0300 Subject: [PATCH] Add TLS protocol configuration for OTLP HTTP exporters --- .../OtlpHttpLogRecordExporterBuilder.java | 17 +++++++++++ .../OtlpHttpMetricExporterBuilder.java | 17 +++++++++++ .../trace/OtlpHttpSpanExporterBuilder.java | 17 +++++++++++ .../otlp/internal/HttpExporterBuilder.java | 11 +++++++ .../internal/ImmutableHttpSenderConfig.java | 6 ++++ .../sender/jdk/internal/JdkHttpSender.java | 12 ++++++-- .../jdk/internal/JdkHttpSenderProvider.java | 1 + .../jdk/internal/JdkHttpSenderTest.java | 2 ++ .../okhttp/internal/OkHttpHttpSender.java | 30 +++++++++++++------ .../internal/OkHttpHttpSenderProvider.java | 1 + .../okhttp/internal/OkHttpHttpSenderTest.java | 4 +++ .../internal/OkHttpHttpSuppressionTest.java | 1 + .../sdk/common/export/HttpSenderConfig.java | 5 ++++ 13 files changed, 113 insertions(+), 11 deletions(-) diff --git a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/logs/OtlpHttpLogRecordExporterBuilder.java b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/logs/OtlpHttpLogRecordExporterBuilder.java index 9918de27b85..f27a9d26bd1 100644 --- a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/logs/OtlpHttpLogRecordExporterBuilder.java +++ b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/logs/OtlpHttpLogRecordExporterBuilder.java @@ -20,6 +20,7 @@ import io.opentelemetry.sdk.common.export.RetryPolicy; import io.opentelemetry.sdk.common.internal.StandardComponentId; import java.time.Duration; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -274,6 +275,22 @@ public OtlpHttpLogRecordExporterBuilder setExecutorService(ExecutorService execu return this; } + /** + * Sets the TLS protocol versions enabled for HTTPS connections. If unset, sender defaults are + * used. + * + *

Protocol names use the JSSE convention, for example {@code TLSv1.2} and {@code TLSv1.3}. The + * JVM security policy may prevent use of configured protocols. + * + * @param enabledProtocols the non-empty list of TLS protocol versions to enable + * @return this builder + */ + public OtlpHttpLogRecordExporterBuilder setEnabledProtocols(List enabledProtocols) { + requireNonNull(enabledProtocols, "enabledProtocols"); + delegate.setEnabledProtocols(enabledProtocols); + return this; + } + /** * Constructs a new instance of the exporter based on the builder's values. * diff --git a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/metrics/OtlpHttpMetricExporterBuilder.java b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/metrics/OtlpHttpMetricExporterBuilder.java index d408c5543f5..2021210bc0c 100644 --- a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/metrics/OtlpHttpMetricExporterBuilder.java +++ b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/metrics/OtlpHttpMetricExporterBuilder.java @@ -26,6 +26,7 @@ import io.opentelemetry.sdk.metrics.export.MetricExporter; import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -333,6 +334,22 @@ public OtlpHttpMetricExporterBuilder setExecutorService(ExecutorService executor return this; } + /** + * Sets the TLS protocol versions enabled for HTTPS connections. If unset, sender defaults are + * used. + * + *

Protocol names use the JSSE convention, for example {@code TLSv1.2} and {@code TLSv1.3}. The + * JVM security policy may prevent use of configured protocols. + * + * @param enabledProtocols the non-empty list of TLS protocol versions to enable + * @return this builder + */ + public OtlpHttpMetricExporterBuilder setEnabledProtocols(List enabledProtocols) { + requireNonNull(enabledProtocols, "enabledProtocols"); + delegate.setEnabledProtocols(enabledProtocols); + return this; + } + OtlpHttpMetricExporterBuilder exportAsJson() { delegate.exportAsJson(); return this; diff --git a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/trace/OtlpHttpSpanExporterBuilder.java b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/trace/OtlpHttpSpanExporterBuilder.java index 11f3e2d63a9..8f08aa85053 100644 --- a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/trace/OtlpHttpSpanExporterBuilder.java +++ b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/http/trace/OtlpHttpSpanExporterBuilder.java @@ -20,6 +20,7 @@ import io.opentelemetry.sdk.common.export.RetryPolicy; import io.opentelemetry.sdk.common.internal.StandardComponentId; import java.time.Duration; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -275,6 +276,22 @@ public OtlpHttpSpanExporterBuilder setExecutorService(ExecutorService executorSe return this; } + /** + * Sets the TLS protocol versions enabled for HTTPS connections. If unset, sender defaults are + * used. + * + *

Protocol names use the JSSE convention, for example {@code TLSv1.2} and {@code TLSv1.3}. The + * JVM security policy may prevent use of configured protocols. + * + * @param enabledProtocols the non-empty list of TLS protocol versions to enable + * @return this builder + */ + public OtlpHttpSpanExporterBuilder setEnabledProtocols(List enabledProtocols) { + requireNonNull(enabledProtocols, "enabledProtocols"); + delegate.setEnabledProtocols(enabledProtocols); + return this; + } + /** * Constructs a new instance of the exporter based on the builder's values. * diff --git a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/HttpExporterBuilder.java b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/HttpExporterBuilder.java index 98a5a2e2b85..6ddf1e3d8d1 100644 --- a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/HttpExporterBuilder.java +++ b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/HttpExporterBuilder.java @@ -69,6 +69,7 @@ public final class HttpExporterBuilder { private ComponentLoader componentLoader = ComponentLoader.forClassLoader(HttpExporterBuilder.class.getClassLoader()); @Nullable private ExecutorService executorService; + @Nullable private List enabledProtocols; public HttpExporterBuilder( StandardComponentId.ExporterType exporterType, String defaultEndpoint) { @@ -167,6 +168,14 @@ public HttpExporterBuilder setExecutorService(ExecutorService executorService) { return this; } + public HttpExporterBuilder setEnabledProtocols(List enabledProtocols) { + if (enabledProtocols.isEmpty()) { + throw new IllegalArgumentException("enabledProtocols must not be empty"); + } + this.enabledProtocols = Collections.unmodifiableList(new ArrayList<>(enabledProtocols)); + return this; + } + public HttpExporterBuilder exportAsJson() { this.exportAsJson = true; exporterType = mapToJsonTypeIfPossible(exporterType); @@ -204,6 +213,7 @@ public HttpExporterBuilder copy() { copy.internalTelemetryVersion = internalTelemetryVersion; copy.proxyOptions = proxyOptions; copy.componentLoader = componentLoader; + copy.enabledProtocols = enabledProtocols; return copy; } @@ -245,6 +255,7 @@ public HttpExporter build() { isPlainHttp ? null : tlsConfigHelper.getSslContext(), isPlainHttp ? null : tlsConfigHelper.getTrustManager(), executorService, + enabledProtocols, // 4mb to align with spec guidance - even though we don't do anything with the // response today, we will so better to have future-looking memory profile 4 * 1024L * 1024L)); diff --git a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/ImmutableHttpSenderConfig.java b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/ImmutableHttpSenderConfig.java index a6d04ae6aa4..bf24415ab28 100644 --- a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/ImmutableHttpSenderConfig.java +++ b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/ImmutableHttpSenderConfig.java @@ -23,6 +23,10 @@ @AutoValue abstract class ImmutableHttpSenderConfig implements HttpSenderConfig { + @Override + @Nullable + public abstract List getEnabledProtocols(); + @SuppressWarnings("TooManyParameters") static HttpSenderConfig create( URI endpoint, @@ -36,6 +40,7 @@ static HttpSenderConfig create( @Nullable SSLContext sslContext, @Nullable X509TrustManager trustManager, @Nullable ExecutorService executorService, + @Nullable List enabledProtocols, long maxResponseBodySize) { return new AutoValue_ImmutableHttpSenderConfig( endpoint, @@ -49,6 +54,7 @@ static HttpSenderConfig create( sslContext, trustManager, executorService, + enabledProtocols, maxResponseBodySize); } diff --git a/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSender.java b/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSender.java index 6c3610e7345..7f1339c2a0c 100644 --- a/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSender.java +++ b/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSender.java @@ -45,6 +45,7 @@ import javax.annotation.Nullable; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; /** * {@link HttpSender} which is backed by JDK {@link HttpClient}. @@ -118,9 +119,10 @@ public final class JdkHttpSender implements HttpSender { @Nullable ProxyOptions proxyOptions, @Nullable SSLContext sslContext, @Nullable ExecutorService executorService, + @Nullable List enabledProtocols, long maxResponseBodySize) { this( - configureClient(sslContext, connectTimeout, proxyOptions), + configureClient(sslContext, connectTimeout, proxyOptions, enabledProtocols), endpoint, contentType, compressor, @@ -144,7 +146,8 @@ private static ExecutorService newExecutor() { private static HttpClient configureClient( @Nullable SSLContext sslContext, Duration connectTimeout, - @Nullable ProxyOptions proxyOptions) { + @Nullable ProxyOptions proxyOptions, + @Nullable List enabledProtocols) { HttpClient.Builder builder = HttpClient.newBuilder().connectTimeout(connectTimeout); if (sslContext != null) { builder.sslContext(sslContext); @@ -152,6 +155,11 @@ private static HttpClient configureClient( if (proxyOptions != null) { builder.proxy(proxyOptions.getProxySelector()); } + if (enabledProtocols != null) { + SSLParameters sslParameters = new SSLParameters(); + sslParameters.setProtocols(enabledProtocols.toArray(new String[0])); + builder.sslParameters(sslParameters); + } return builder.build(); } diff --git a/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProvider.java b/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProvider.java index fcd9d64e0fe..1f968f62f12 100644 --- a/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProvider.java +++ b/exporters/sender/jdk/src/main/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderProvider.java @@ -30,6 +30,7 @@ public HttpSender createSender(HttpSenderConfig httpSenderConfig) { httpSenderConfig.getProxyOptions(), httpSenderConfig.getSslContext(), httpSenderConfig.getExecutorService(), + httpSenderConfig.getEnabledProtocols(), httpSenderConfig.getMaxResponseBodySize()); } } diff --git a/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderTest.java b/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderTest.java index e329266b80d..530dec08e8e 100644 --- a/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderTest.java +++ b/exporters/sender/jdk/src/test/java/io/opentelemetry/exporter/sender/jdk/internal/JdkHttpSenderTest.java @@ -191,6 +191,7 @@ void defaultExecutor_isBounded() { null, null, null, + null, Long.MAX_VALUE); try { @@ -223,6 +224,7 @@ void connectTimeout() { null, null, null, + null, Long.MAX_VALUE); assertThat(sender) diff --git a/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java b/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java index 266b7080686..df33c1283a4 100644 --- a/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java +++ b/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java @@ -81,6 +81,7 @@ public OkHttpHttpSender( @Nullable SSLContext sslContext, @Nullable X509TrustManager trustManager, @Nullable ExecutorService executorService, + @Nullable List enabledProtocols, long maxResponseBodySize) { int callTimeoutMillis = (int) Math.min(timeout.toMillis(), Integer.MAX_VALUE); int connectTimeoutMillis = (int) Math.min(connectTimeout.toMillis(), Integer.MAX_VALUE); @@ -111,17 +112,28 @@ public OkHttpHttpSender( boolean isPlainHttp = endpoint.getScheme().equals("http"); if (isPlainHttp) { builder.connectionSpecs(Collections.singletonList(ConnectionSpec.CLEARTEXT)); - } else if (sslContext != null) { - X509TrustManager effectiveTrustManager = trustManager; - - if (effectiveTrustManager == null) { - try { - effectiveTrustManager = TlsUtil.defaultTrustManager(); - } catch (SSLException e) { - throw new IllegalStateException("Unable to initialize default trust manager", e); + } else { + if (enabledProtocols != null) { + ConnectionSpec spec = + new ConnectionSpec.Builder(ConnectionSpec.COMPATIBLE_TLS) + .tlsVersions(enabledProtocols.toArray(new String[0])) + .build(); + + builder.connectionSpecs(Collections.singletonList(spec)); + } + + if (sslContext != null) { + X509TrustManager effectiveTrustManager = trustManager; + + if (effectiveTrustManager == null) { + try { + effectiveTrustManager = TlsUtil.defaultTrustManager(); + } catch (SSLException e) { + throw new IllegalStateException("Unable to initialize default trust manager", e); + } } + builder.sslSocketFactory(sslContext.getSocketFactory(), effectiveTrustManager); } - builder.sslSocketFactory(sslContext.getSocketFactory(), effectiveTrustManager); } this.client = builder.build(); diff --git a/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderProvider.java b/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderProvider.java index 581cf9bb549..fe644baa95a 100644 --- a/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderProvider.java +++ b/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderProvider.java @@ -31,6 +31,7 @@ public HttpSender createSender(HttpSenderConfig httpSenderConfig) { httpSenderConfig.getSslContext(), httpSenderConfig.getTrustManager(), httpSenderConfig.getExecutorService(), + httpSenderConfig.getEnabledProtocols(), httpSenderConfig.getMaxResponseBodySize()); } } diff --git a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java index bc62e524762..17b4ce6e024 100644 --- a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java +++ b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java @@ -53,6 +53,7 @@ void send_rejectedExecution_callsOnError() { null, null, executor, + null, Long.MAX_VALUE); AtomicReference responseRef = new AtomicReference<>(); @@ -216,6 +217,7 @@ private static OkHttpHttpSender newSender( null, null, executorService, + null, Long.MAX_VALUE); } @@ -248,6 +250,7 @@ void constructor_usesDefaultTrustManagerWhenTrustManagerIsNull() throws Exceptio sslContext, null, null, + null, Long.MAX_VALUE)) .doesNotThrowAnyException(); } @@ -276,6 +279,7 @@ void constructor_wrapsDefaultTrustManagerFailure() throws Exception { sslContext, null, null, + null, Long.MAX_VALUE)) .isInstanceOf(IllegalStateException.class) .hasMessage("Unable to initialize default trust manager") diff --git a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSuppressionTest.java b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSuppressionTest.java index c64247a9b36..be1a0e504ea 100644 --- a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSuppressionTest.java +++ b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSuppressionTest.java @@ -47,6 +47,7 @@ OkHttpHttpSender createSender(String endpoint) { null, null, null, + null, Long.MAX_VALUE); } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSenderConfig.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSenderConfig.java index be40289dd9c..3583b572e73 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSenderConfig.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSenderConfig.java @@ -87,6 +87,11 @@ public interface HttpSenderConfig { @Nullable ExecutorService getExecutorService(); + @Nullable + default List getEnabledProtocols() { + return null; + } + /** * The maximum number of bytes to read from a response body. Defaults to 4 MiB. *