diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java index c47745b1351..04dbaeac3d6 100644 --- a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java @@ -10,11 +10,18 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.exporter.internal.TlsUtil; +import io.opentelemetry.exporter.internal.retry.RetryInterceptor; +import io.opentelemetry.exporter.internal.retry.RetryPolicy; +import io.opentelemetry.exporter.internal.retry.RetryUtil; import java.net.InetAddress; import java.time.Duration; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import javax.annotation.Nullable; +import javax.net.ssl.SSLException; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; import zipkin2.Span; import zipkin2.codec.BytesEncoder; import zipkin2.codec.SpanBytesEncoder; @@ -27,11 +34,21 @@ public final class ZipkinSpanExporterBuilder { private Supplier localIpAddressSupplier = LocalInetAddressSupplier.getInstance(); @Nullable private Sender sender; private String endpoint = ZipkinSpanExporter.DEFAULT_ENDPOINT; - // compression is enabled by default, because this is the default of OkHttpSender, + // compression is enabled by default, because this is the default of + // OkHttpSender, // which is created when no custom sender is set (see OkHttpSender.Builder) private boolean compressionEnabled = true; private long readTimeoutMillis = TimeUnit.SECONDS.toMillis(10); private Supplier meterProviderSupplier = GlobalOpenTelemetry::getMeterProvider; + private final OkHttpSender.Builder okHttpSenderBuilder; + @Nullable private byte[] trustedCertificatesPem; + @Nullable private byte[] privateKeyPem; + @Nullable private byte[] certificatePem; + @Nullable private RetryPolicy retryPolicy; + + public ZipkinSpanExporterBuilder() { + this.okHttpSenderBuilder = OkHttpSender.newBuilder(); + } /** * Sets the Zipkin sender. Implements the client side of the span transport. An {@link @@ -151,6 +168,41 @@ public ZipkinSpanExporterBuilder setMeterProvider(MeterProvider meterProvider) { return this; } + /** + * Sets the certificate chain to use for verifying servers when TLS is enabled. The {@code byte[]} + * should contain an X.509 certificate collection in PEM format. If not set, TLS connections will + * use the system default trusted certificates. + */ + public ZipkinSpanExporterBuilder setTrustedCertificates(byte[] trustedCertificatesPem) { + requireNonNull(trustedCertificatesPem, "trustedCertificatesPem"); + this.trustedCertificatesPem = trustedCertificatesPem; + return this; + } + + /** + * Sets ths client key and the certificate chain to use for verifying client when TLS is enabled. + * The key must be PKCS8, and both must be in PEM format. + */ + public ZipkinSpanExporterBuilder setClientTls(byte[] privateKeyPem, byte[] certificatePem) { + requireNonNull(privateKeyPem, "privateKeyPem"); + requireNonNull(certificatePem, "certificatePem"); + this.privateKeyPem = privateKeyPem; + this.certificatePem = certificatePem; + return this; + } + + /** + * Set the retry policy to use by the client + * + * @param retryPolicy The retry policy. + * @return this. + */ + public ZipkinSpanExporterBuilder setRetryPolicy(RetryPolicy retryPolicy) { + requireNonNull(retryPolicy, "retryPolicy"); + this.retryPolicy = retryPolicy; + return this; + } + /** * Builds a {@link ZipkinSpanExporter}. * @@ -159,8 +211,35 @@ public ZipkinSpanExporterBuilder setMeterProvider(MeterProvider meterProvider) { public ZipkinSpanExporter build() { Sender sender = this.sender; if (sender == null) { + if (trustedCertificatesPem != null) { + try { + X509TrustManager trustManager = TlsUtil.trustManager(trustedCertificatesPem); + X509KeyManager keyManager = null; + if (privateKeyPem != null && certificatePem != null) { + keyManager = TlsUtil.keyManager(privateKeyPem, certificatePem); + } + this.okHttpSenderBuilder + .clientBuilder() + .sslSocketFactory(TlsUtil.sslSocketFactory(keyManager, trustManager), trustManager); + } catch (SSLException e) { + throw new IllegalStateException( + "Could not set trusted certificate for Zipkin HTTP connection, are they valid X.509 in PEM format?", + e); + } + } + + if (retryPolicy != null) { + this.okHttpSenderBuilder + .clientBuilder() + .addInterceptor( + new RetryInterceptor( + retryPolicy, + (response) -> + RetryUtil.retryableHttpResponseCodes().contains(response.code()))); + } + sender = - OkHttpSender.newBuilder() + this.okHttpSenderBuilder .endpoint(endpoint) .compressionEnabled(compressionEnabled) .readTimeout((int) readTimeoutMillis) diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProvider.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProvider.java index cbd55d386dc..194c40b821f 100644 --- a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProvider.java +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProvider.java @@ -5,12 +5,18 @@ package io.opentelemetry.exporter.zipkin.internal; +import io.opentelemetry.exporter.internal.retry.RetryPolicy; import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; import java.time.Duration; +import javax.annotation.Nullable; /** * {@link SpanExporter} SPI implementation for {@link ZipkinSpanExporter}. @@ -38,6 +44,53 @@ public SpanExporter createExporter(ConfigProperties config) { builder.setReadTimeout(timeout); } + String certificatePath = config.getString("otel.exporter.zipkin.certificate"); + String clientKeyPath = config.getString("otel.exporter.zipkin.client.key"); + String clientKeyChainPath = config.getString("otel.exporter.zipkin.client.certificate"); + + if (clientKeyPath != null && clientKeyChainPath == null) { + throw new ConfigurationException("Client key provided but certification chain is missing"); + } else if (clientKeyPath == null && clientKeyChainPath != null) { + throw new ConfigurationException("Client key chain provided but key is missing"); + } + + byte[] certificateBytes = readFileBytes(certificatePath); + if (certificateBytes != null) { + builder.setTrustedCertificates(certificateBytes); + } + + byte[] clientKeyBytes = readFileBytes(clientKeyPath); + byte[] clientKeyChainBytes = readFileBytes(clientKeyChainPath); + + if (clientKeyBytes != null && clientKeyChainBytes != null) { + builder.setClientTls(clientKeyBytes, clientKeyChainBytes); + } + + boolean retryEnabled = + config.getBoolean("otel.experimental.exporter.zipkin.retry.enabled", false); + if (retryEnabled) { + builder.setRetryPolicy(RetryPolicy.getDefault()); + } + return builder.build(); } + + @Nullable + private static byte[] readFileBytes(@Nullable String filePath) { + if (filePath == null) { + return null; + } + File file = new File(filePath); + if (!file.exists()) { + throw new ConfigurationException("Invalid Zipkin certificate/key path: " + filePath); + } + try { + RandomAccessFile raf = new RandomAccessFile(file, "r"); + byte[] bytes = new byte[(int) raf.length()]; + raf.readFully(bytes); + return bytes; + } catch (IOException e) { + throw new ConfigurationException("Error reading content of file (" + filePath + ")", e); + } + } }