Add OTLP signal specific certificate autoconfiguration (#3439)

* Add setTrustedCertificates support to OtlpGrpcMetricExporterBuilder

* Add signal specific certificate configuration to autoconfiguration

* Improve test coverage.

* Try to meet code coverage requirements.

* Qualify SslUtil.setTrustedCertificatesPem
This commit is contained in:
jack-berg 2021-08-05 03:10:48 -05:00 committed by GitHub
parent 956912dd5f
commit 20f872d8dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 657 additions and 150 deletions

View File

@ -17,6 +17,10 @@ dependencies {
implementation("com.google.protobuf:protobuf-java")
compileOnly("io.grpc:grpc-netty")
compileOnly("io.grpc:grpc-netty-shaded")
compileOnly("io.grpc:grpc-okhttp")
testImplementation(project(":sdk:testing"))
testImplementation("io.grpc:grpc-testing")

View File

@ -0,0 +1,102 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.exporter.otlp.internal;
import static java.util.Objects.requireNonNull;
import io.grpc.ManagedChannelBuilder;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NettyChannelBuilder;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManagerFactory;
public final class SslUtil {
/**
* Configure the channel builder to trust the certificates. The {@code byte[]} should contain an
* X.509 certificate collection in PEM format.
*
* @throws SSLException if error occur processing the certificates
*/
public static void setTrustedCertificatesPem(
ManagedChannelBuilder<?> managedChannelBuilder, byte[] trustedCertificatesPem)
throws SSLException {
requireNonNull(managedChannelBuilder, "managedChannelBuilder");
requireNonNull(trustedCertificatesPem, "trustedCertificatesPem");
TrustManagerFactory tmf = trustManagerFactory(trustedCertificatesPem);
// gRPC does not abstract TLS configuration so we need to check the implementation and act
// accordingly.
if (managedChannelBuilder.getClass().getName().equals("io.grpc.netty.NettyChannelBuilder")) {
NettyChannelBuilder nettyBuilder = (NettyChannelBuilder) managedChannelBuilder;
nettyBuilder.sslContext(GrpcSslContexts.forClient().trustManager(tmf).build());
} else if (managedChannelBuilder
.getClass()
.getName()
.equals("io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder")) {
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder nettyBuilder =
(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder) managedChannelBuilder;
nettyBuilder.sslContext(
io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts.forClient().trustManager(tmf).build());
} else if (managedChannelBuilder
.getClass()
.getName()
.equals("io.grpc.okhttp.OkHttpChannelBuilder")) {
io.grpc.okhttp.OkHttpChannelBuilder okHttpBuilder =
(io.grpc.okhttp.OkHttpChannelBuilder) managedChannelBuilder;
SSLContext sslContext;
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new SSLException("Could not build SSLContext.", e);
}
okHttpBuilder.sslSocketFactory(sslContext.getSocketFactory());
} else {
throw new SSLException(
"TLS certificate configuration not supported for unrecognized ManagedChannelBuilder "
+ managedChannelBuilder.getClass().getName());
}
}
private static TrustManagerFactory trustManagerFactory(byte[] trustedCertificatesPem)
throws SSLException {
requireNonNull(trustedCertificatesPem, "trustedCertificatesPem");
try {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null);
ByteArrayInputStream is = new ByteArrayInputStream(trustedCertificatesPem);
CertificateFactory factory = CertificateFactory.getInstance("X.509");
int i = 0;
while (is.available() > 0) {
X509Certificate cert = (X509Certificate) factory.generateCertificate(is);
ks.setCertificateEntry("cert_" + i, cert);
i++;
}
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
return tmf;
} catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException e) {
throw new SSLException("Could not build TrustManagerFactory from trustedCertificatesPem.", e);
}
}
private SslUtil() {}
}

View File

@ -2,12 +2,19 @@ plugins {
id("otel.java-conventions")
id("otel.publish-conventions")
id("org.unbroken-dome.test-sets")
id("otel.animalsniffer-conventions")
}
description = "OpenTelemetry Protocol Metrics Exporter"
otelJava.moduleName.set("io.opentelemetry.exporter.otlp.metrics")
testSets {
create("testGrpcNetty")
create("testGrpcNettyShaded")
create("testGrpcOkhttp")
}
dependencies {
api(project(":sdk:metrics"))
@ -22,4 +29,25 @@ dependencies {
testImplementation("io.grpc:grpc-testing")
testRuntimeOnly("io.grpc:grpc-netty-shaded")
add("testGrpcNettyImplementation", "com.linecorp.armeria:armeria-grpc")
add("testGrpcNettyImplementation", "com.linecorp.armeria:armeria-junit5")
add("testGrpcNettyRuntimeOnly", "io.grpc:grpc-netty")
add("testGrpcNettyRuntimeOnly", "org.bouncycastle:bcpkix-jdk15on")
add("testGrpcNettyShadedImplementation", "com.linecorp.armeria:armeria-grpc")
add("testGrpcNettyShadedImplementation", "com.linecorp.armeria:armeria-junit5")
add("testGrpcNettyShadedRuntimeOnly", "io.grpc:grpc-netty-shaded")
add("testGrpcNettyShadedRuntimeOnly", "org.bouncycastle:bcpkix-jdk15on")
add("testGrpcOkhttpImplementation", "com.linecorp.armeria:armeria-grpc")
add("testGrpcOkhttpImplementation", "com.linecorp.armeria:armeria-junit5")
add("testGrpcOkhttpRuntimeOnly", "io.grpc:grpc-okhttp")
add("testGrpcOkhttpRuntimeOnly", "org.bouncycastle:bcpkix-jdk15on")
}
tasks {
named("check") {
dependsOn("testGrpcNetty", "testGrpcNettyShaded", "testGrpcOkhttp")
}
}

View File

@ -13,11 +13,13 @@ import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;
import io.opentelemetry.exporter.otlp.internal.SslUtil;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.net.ssl.SSLException;
/** Builder utility for this exporter. */
public final class OtlpGrpcMetricExporterBuilder {
@ -31,6 +33,7 @@ public final class OtlpGrpcMetricExporterBuilder {
private URI endpoint = DEFAULT_ENDPOINT;
@Nullable private Metadata metadata;
@Nullable private byte[] trustedCertificatesPem;
/**
* Sets the managed chanel to use when communicating with the backend. Takes precedence over
@ -88,6 +91,16 @@ public final class OtlpGrpcMetricExporterBuilder {
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 OtlpGrpcMetricExporterBuilder setTrustedCertificates(byte[] trustedCertificatesPem) {
this.trustedCertificatesPem = trustedCertificatesPem;
return this;
}
/**
* Add header to request. Optional. Applicable only if {@link
* OtlpGrpcMetricExporterBuilder#endpoint} is set to build channel.
@ -124,6 +137,17 @@ public final class OtlpGrpcMetricExporterBuilder {
managedChannelBuilder.intercept(MetadataUtils.newAttachHeadersInterceptor(metadata));
}
if (trustedCertificatesPem != null) {
try {
SslUtil.setTrustedCertificatesPem(managedChannelBuilder, trustedCertificatesPem);
} catch (SSLException e) {
throw new IllegalStateException(
"Could not set trusted certificates for gRPC TLS connection, are they valid "
+ "X.509 in PEM format?",
e);
}
}
channel = managedChannelBuilder.build();
}
return new OtlpGrpcMetricExporter(channel, timeoutNanos);

View File

@ -0,0 +1,134 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.exporter.otlp.metrics;
import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.grpc.GrpcService;
import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
import io.grpc.stub.StreamObserver;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse;
import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc;
import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
import io.opentelemetry.sdk.metrics.data.LongPointData;
import io.opentelemetry.sdk.metrics.data.LongSumData;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.resources.Resource;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class ExportTest {
private static final long START_NS = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis());
private static final List<MetricData> METRICS =
Collections.singletonList(
MetricData.createLongSum(
Resource.empty(),
InstrumentationLibraryInfo.empty(),
"name",
"description",
"1",
LongSumData.create(
/* isMonotonic= */ true,
AggregationTemporality.CUMULATIVE,
Collections.singletonList(
LongPointData.create(
START_NS,
START_NS + TimeUnit.MILLISECONDS.toNanos(900),
Attributes.of(stringKey("k"), "v"),
5)))));
@RegisterExtension
@Order(1)
public static SelfSignedCertificateExtension certificate = new SelfSignedCertificateExtension();
@RegisterExtension
@Order(2)
public static ServerExtension server =
new ServerExtension() {
@Override
protected void configure(ServerBuilder sb) {
sb.service(
GrpcService.builder()
.addService(
new MetricsServiceGrpc.MetricsServiceImplBase() {
@Override
public void export(
ExportMetricsServiceRequest request,
StreamObserver<ExportMetricsServiceResponse> responseObserver) {
responseObserver.onNext(
ExportMetricsServiceResponse.getDefaultInstance());
responseObserver.onCompleted();
}
})
.build());
sb.http(0);
sb.https(0);
sb.tls(certificate.certificateFile(), certificate.privateKeyFile());
}
};
@Test
void plainTextExport() {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("http://localhost:" + server.httpPort())
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void authorityWithAuth() {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("http://foo:bar@localhost:" + server.httpPort())
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void testTlsExport() throws Exception {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("https://localhost:" + server.httpsPort())
.setTrustedCertificates(Files.readAllBytes(certificate.certificateFile().toPath()))
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void testTlsExport_untrusted() {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("https://localhost:" + server.httpsPort())
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isFalse();
}
@Test
void tlsBadCert() {
assertThatThrownBy(
() ->
OtlpGrpcMetricExporter.builder()
.setTrustedCertificates("foobar".getBytes(StandardCharsets.UTF_8))
.build())
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Could not set trusted certificates");
}
}

View File

@ -0,0 +1,134 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.exporter.otlp.metrics;
import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.grpc.GrpcService;
import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
import io.grpc.stub.StreamObserver;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse;
import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc;
import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
import io.opentelemetry.sdk.metrics.data.LongPointData;
import io.opentelemetry.sdk.metrics.data.LongSumData;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.resources.Resource;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class ExportTest {
private static final long START_NS = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis());
private static final List<MetricData> METRICS =
Collections.singletonList(
MetricData.createLongSum(
Resource.empty(),
InstrumentationLibraryInfo.empty(),
"name",
"description",
"1",
LongSumData.create(
/* isMonotonic= */ true,
AggregationTemporality.CUMULATIVE,
Collections.singletonList(
LongPointData.create(
START_NS,
START_NS + TimeUnit.MILLISECONDS.toNanos(900),
Attributes.of(stringKey("k"), "v"),
5)))));
@RegisterExtension
@Order(1)
public static SelfSignedCertificateExtension certificate = new SelfSignedCertificateExtension();
@RegisterExtension
@Order(2)
public static ServerExtension server =
new ServerExtension() {
@Override
protected void configure(ServerBuilder sb) {
sb.service(
GrpcService.builder()
.addService(
new MetricsServiceGrpc.MetricsServiceImplBase() {
@Override
public void export(
ExportMetricsServiceRequest request,
StreamObserver<ExportMetricsServiceResponse> responseObserver) {
responseObserver.onNext(
ExportMetricsServiceResponse.getDefaultInstance());
responseObserver.onCompleted();
}
})
.build());
sb.http(0);
sb.https(0);
sb.tls(certificate.certificateFile(), certificate.privateKeyFile());
}
};
@Test
void plainTextExport() {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("http://localhost:" + server.httpPort())
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void authorityWithAuth() {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("http://foo:bar@localhost:" + server.httpPort())
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void testTlsExport() throws Exception {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("https://localhost:" + server.httpsPort())
.setTrustedCertificates(Files.readAllBytes(certificate.certificateFile().toPath()))
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void testTlsExport_untrusted() {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("https://localhost:" + server.httpsPort())
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isFalse();
}
@Test
void tlsBadCert() {
assertThatThrownBy(
() ->
OtlpGrpcMetricExporter.builder()
.setTrustedCertificates("foobar".getBytes(StandardCharsets.UTF_8))
.build())
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Could not set trusted certificates");
}
}

View File

@ -0,0 +1,134 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.exporter.otlp.metrics;
import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.grpc.GrpcService;
import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
import io.grpc.stub.StreamObserver;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse;
import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc;
import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
import io.opentelemetry.sdk.metrics.data.LongPointData;
import io.opentelemetry.sdk.metrics.data.LongSumData;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.resources.Resource;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class ExportTest {
private static final long START_NS = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis());
private static final List<MetricData> METRICS =
Collections.singletonList(
MetricData.createLongSum(
Resource.empty(),
InstrumentationLibraryInfo.empty(),
"name",
"description",
"1",
LongSumData.create(
/* isMonotonic= */ true,
AggregationTemporality.CUMULATIVE,
Collections.singletonList(
LongPointData.create(
START_NS,
START_NS + TimeUnit.MILLISECONDS.toNanos(900),
Attributes.of(stringKey("k"), "v"),
5)))));
@RegisterExtension
@Order(1)
public static SelfSignedCertificateExtension certificate = new SelfSignedCertificateExtension();
@RegisterExtension
@Order(2)
public static ServerExtension server =
new ServerExtension() {
@Override
protected void configure(ServerBuilder sb) {
sb.service(
GrpcService.builder()
.addService(
new MetricsServiceGrpc.MetricsServiceImplBase() {
@Override
public void export(
ExportMetricsServiceRequest request,
StreamObserver<ExportMetricsServiceResponse> responseObserver) {
responseObserver.onNext(
ExportMetricsServiceResponse.getDefaultInstance());
responseObserver.onCompleted();
}
})
.build());
sb.http(0);
sb.https(0);
sb.tls(certificate.certificateFile(), certificate.privateKeyFile());
}
};
@Test
void plainTextExport() {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("http://localhost:" + server.httpPort())
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void authorityWithAuth() {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("http://foo:bar@localhost:" + server.httpPort())
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void testTlsExport() throws Exception {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("https://localhost:" + server.httpsPort())
.setTrustedCertificates(Files.readAllBytes(certificate.certificateFile().toPath()))
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void testTlsExport_untrusted() {
OtlpGrpcMetricExporter exporter =
OtlpGrpcMetricExporter.builder()
.setEndpoint("https://localhost:" + server.httpsPort())
.build();
assertThat(exporter.export(METRICS).join(10, TimeUnit.SECONDS).isSuccess()).isFalse();
}
@Test
void tlsBadCert() {
assertThatThrownBy(
() ->
OtlpGrpcMetricExporter.builder()
.setTrustedCertificates("foobar".getBytes(StandardCharsets.UTF_8))
.build())
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Could not set trusted certificates");
}
}

View File

@ -12,26 +12,14 @@ import static java.util.Objects.requireNonNull;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NettyChannelBuilder;
import io.grpc.stub.MetadataUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import io.opentelemetry.exporter.otlp.internal.SslUtil;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManagerFactory;
/** Builder utility for this exporter. */
public final class OtlpGrpcSpanExporterBuilder {
@ -149,60 +137,13 @@ public final class OtlpGrpcSpanExporterBuilder {
}
if (trustedCertificatesPem != null) {
// gRPC does not abstract TLS configuration so we need to check the implementation and act
// accordingly.
if (managedChannelBuilder
.getClass()
.getName()
.equals("io.grpc.netty.NettyChannelBuilder")) {
NettyChannelBuilder nettyBuilder = (NettyChannelBuilder) managedChannelBuilder;
try {
nettyBuilder.sslContext(
GrpcSslContexts.forClient().trustManager(trustManagerFactory()).build());
} catch (IllegalArgumentException | SSLException e) {
throw new IllegalStateException(
"Could not set trusted certificates for gRPC TLS connection, are they valid "
+ "X.509 in PEM format?",
e);
}
} else if (managedChannelBuilder
.getClass()
.getName()
.equals("io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder")) {
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder nettyBuilder =
(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder) managedChannelBuilder;
try {
nettyBuilder.sslContext(
io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts.forClient()
.trustManager(trustManagerFactory())
.build());
} catch (IllegalArgumentException | SSLException e) {
throw new IllegalStateException(
"Could not set trusted certificates for gRPC TLS connection, are they valid "
+ "X.509 in PEM format?",
e);
}
} else if (managedChannelBuilder
.getClass()
.getName()
.equals("io.grpc.okhttp.OkHttpChannelBuilder")) {
io.grpc.okhttp.OkHttpChannelBuilder okHttpBuilder =
(io.grpc.okhttp.OkHttpChannelBuilder) managedChannelBuilder;
SSLContext sslContext;
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory().getTrustManagers(), null);
} catch (NoSuchAlgorithmException | SSLException | KeyManagementException e) {
throw new IllegalStateException(
"Could not set trusted certificates for gRPC TLS connection, are they valid "
+ "X.509 in PEM format?",
e);
}
okHttpBuilder.sslSocketFactory(sslContext.getSocketFactory());
} else {
try {
SslUtil.setTrustedCertificatesPem(managedChannelBuilder, trustedCertificatesPem);
} catch (SSLException e) {
throw new IllegalStateException(
"TLS certificate configuration not supported for unrecognized ManagedChannelBuilder "
+ managedChannelBuilder.getClass().getName());
"Could not set trusted certificates for gRPC TLS connection, are they valid "
+ "X.509 in PEM format?",
e);
}
}
@ -211,29 +152,5 @@ public final class OtlpGrpcSpanExporterBuilder {
return new OtlpGrpcSpanExporter(channel, timeoutNanos);
}
private TrustManagerFactory trustManagerFactory() throws SSLException {
requireNonNull(trustedCertificatesPem, "trustedCertificatesPem");
try {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null);
ByteArrayInputStream is = new ByteArrayInputStream(trustedCertificatesPem);
CertificateFactory factory = CertificateFactory.getInstance("X.509");
int i = 0;
while (is.available() > 0) {
X509Certificate cert = (X509Certificate) factory.generateCertificate(is);
ks.setCertificateEntry("cert_" + i, cert);
i++;
}
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
return tmf;
} catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException e) {
throw new SSLException("Could not build TrustManagerFactory from trustedCertificatesPem.", e);
}
}
OtlpGrpcSpanExporterBuilder() {}
}

View File

@ -18,16 +18,29 @@ import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse;
import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc;
import io.opentelemetry.sdk.testing.trace.TestSpanData;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.data.StatusData;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class TlsExportTest {
class ExportTest {
private static final List<SpanData> SPANS =
Collections.singletonList(
TestSpanData.builder()
.setName("name")
.setKind(SpanKind.CLIENT)
.setStartEpochNanos(1)
.setEndEpochNanos(2)
.setStatus(StatusData.ok())
.setHasEnded(true)
.build());
@RegisterExtension
@Order(1)
@ -52,10 +65,28 @@ class TlsExportTest {
}
})
.build());
sb.http(0);
sb.https(0);
sb.tls(certificate.certificateFile(), certificate.privateKeyFile());
}
};
@Test
void plainTextExport() {
OtlpGrpcSpanExporter exporter =
OtlpGrpcSpanExporter.builder().setEndpoint("http://localhost:" + server.httpPort()).build();
assertThat(exporter.export(SPANS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void authorityWithAuth() {
OtlpGrpcSpanExporter exporter =
OtlpGrpcSpanExporter.builder()
.setEndpoint("http://foo:bar@localhost:" + server.httpPort())
.build();
assertThat(exporter.export(SPANS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void testTlsExport() throws Exception {
OtlpGrpcSpanExporter exporter =
@ -63,44 +94,16 @@ class TlsExportTest {
.setEndpoint("https://localhost:" + server.httpsPort())
.setTrustedCertificates(Files.readAllBytes(certificate.certificateFile().toPath()))
.build();
assertThat(
exporter
.export(
Arrays.asList(
TestSpanData.builder()
.setName("name")
.setKind(SpanKind.CLIENT)
.setStartEpochNanos(1)
.setEndEpochNanos(2)
.setStatus(StatusData.ok())
.setHasEnded(true)
.build()))
.join(10, TimeUnit.SECONDS)
.isSuccess())
.isTrue();
assertThat(exporter.export(SPANS).join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
}
@Test
void testTlsExport_untrusted() throws Exception {
void testTlsExport_untrusted() {
OtlpGrpcSpanExporter exporter =
OtlpGrpcSpanExporter.builder()
.setEndpoint("https://localhost:" + server.httpsPort())
.build();
assertThat(
exporter
.export(
Arrays.asList(
TestSpanData.builder()
.setName("name")
.setKind(SpanKind.CLIENT)
.setStartEpochNanos(1)
.setEndEpochNanos(2)
.setStatus(StatusData.ok())
.setHasEnded(true)
.build()))
.join(10, TimeUnit.SECONDS)
.isSuccess())
.isFalse();
assertThat(exporter.export(SPANS).join(10, TimeUnit.SECONDS).isSuccess()).isFalse();
}
@Test

View File

@ -51,6 +51,9 @@ The [OpenTelemetry Protocol (OTLP)](https://github.com/open-telemetry/openteleme
| otel.exporter.otlp.endpoint | OTEL_EXPORTER_OTLP_ENDPOINT | The OTLP traces and metrics endpoint to connect to. Must be a URL with a scheme of either `http` or `https` based on the use of TLS. Default is `http://localhost:4317`. |
| otel.exporter.otlp.traces.endpoint | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | The OTLP traces endpoint to connect to. Must be a URL with a scheme of either `http` or `https` based on the use of TLS. Default is `http://localhost:4317`. |
| otel.exporter.otlp.metrics.endpoint | OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | The OTLP metrics endpoint to connect to. Must be a URL with a scheme of either `http` or `https` based on the use of TLS. Default is `http://localhost:4317`. |
| otel.exporter.otlp.certificate | OTEL_EXPORTER_OTLP_CERTIFICATE | The path to the file containing trusted certificates to use when verifying an OTLP trace or metric server's TLS credentials. The file should contain one or more X.509 certificates in PEM format. By default the host platform's trusted root certificates are used. |
| otel.exporter.otlp.traces.certificate | OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE | The path to the file containing trusted certificates to use when verifying an OTLP trace server's TLS credentials. The file should contain one or more X.509 certificates in PEM format. By default the host platform's trusted root certificates are used. |
| otel.exporter.otlp.metrics.certificate | OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE | The path to the file containing trusted certificates to use when verifying an OTLP metric server's TLS credentials. The file should contain one or more X.509 certificates in PEM format. By default the host platform's trusted root certificates are used. |
| otel.exporter.otlp.headers | OTEL_EXPORTER_OTLP_HEADERS | Key-value pairs separated by commas to pass as request headers on OTLP trace and metrics requests. |
| otel.exporter.otlp.traces.headers | OTEL_EXPORTER_OTLP_TRACES_HEADERS | Key-value pairs separated by commas to pass as request headers on OTLP trace requests. |
| otel.exporter.otlp.metrics.headers | OTEL_EXPORTER_OTLP_METRICS_HEADERS | Key-value pairs separated by commas to pass as request headers on OTLP metrics requests. |

View File

@ -15,6 +15,9 @@ import io.opentelemetry.sdk.metrics.export.IntervalMetricReaderBuilder;
import io.opentelemetry.sdk.metrics.export.MetricExporter;
import io.prometheus.client.exporter.HTTPServer;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
@ -89,6 +92,24 @@ final class MetricExporterConfiguration {
builder.setTimeout(timeout);
}
String certificate = config.getString("otel.exporter.otlp.metrics.certificate");
if (certificate == null) {
certificate = config.getString("otel.exporter.otlp.certificate");
}
if (certificate != null) {
Path path = Paths.get(certificate);
if (!Files.exists(path)) {
throw new ConfigurationException("Invalid OTLP certificate path: " + path);
}
final byte[] certificateBytes;
try {
certificateBytes = Files.readAllBytes(path);
} catch (IOException e) {
throw new ConfigurationException("Error reading OTLP certificate.", e);
}
builder.setTrustedCertificates(certificateBytes);
}
OtlpGrpcMetricExporter exporter = builder.build();
configureIntervalMetricReader(config, meterProvider, exporter);

View File

@ -92,7 +92,10 @@ final class SpanExporterConfiguration {
builder.setTimeout(timeout);
}
String certificate = config.getString("otel.exporter.otlp.certificate");
String certificate = config.getString("otel.exporter.otlp.traces.certificate");
if (certificate == null) {
certificate = config.getString("otel.exporter.otlp.certificate");
}
if (certificate != null) {
Path path = Paths.get(certificate);
if (!Files.exists(path)) {

View File

@ -108,8 +108,6 @@ class OtlpConfigTest {
requestHeaders.add(req.headers());
return delegate.serve(ctx, req);
});
sb.http(0);
sb.https(0);
sb.tls(certificate.certificateFile(), certificate.privateKeyFile());
}
};
@ -132,7 +130,8 @@ class OtlpConfigTest {
@Test
void configureExportersGeneral() {
Map<String, String> props = new HashMap<>();
props.put("otel.exporter.otlp.endpoint", "http://localhost:" + server.httpPort());
props.put("otel.exporter.otlp.endpoint", "https://localhost:" + server.httpsPort());
props.put("otel.exporter.otlp.certificate", certificate.certificateFile().getAbsolutePath());
props.put("otel.exporter.otlp.headers", "header-key=header-value");
props.put("otel.exporter.otlp.timeout", "5s");
ConfigProperties properties = ConfigProperties.createForTest(props);
@ -178,9 +177,12 @@ class OtlpConfigTest {
// general.
Map<String, String> props = new HashMap<>();
props.put("otel.exporter.otlp.endpoint", "http://foo.bar");
props.put("otel.exporter.otlp.certificate", Paths.get("foo", "bar", "baz").toString());
props.put("otel.exporter.otlp.headers", "header-key=dummy-value");
props.put("otel.exporter.otlp.timeout", "10s");
props.put("otel.exporter.otlp.traces.endpoint", "http://localhost:" + server.httpPort());
props.put("otel.exporter.otlp.traces.endpoint", "https://localhost:" + server.httpsPort());
props.put(
"otel.exporter.otlp.traces.certificate", certificate.certificateFile().getAbsolutePath());
props.put("otel.exporter.otlp.traces.headers", "header-key=header-value");
props.put("otel.exporter.otlp.traces.timeout", "5s");
SpanExporter spanExporter =
@ -208,9 +210,12 @@ class OtlpConfigTest {
// general.
Map<String, String> props = new HashMap<>();
props.put("otel.exporter.otlp.endpoint", "http://foo.bar");
props.put("otel.exporter.otlp.certificate", Paths.get("foo", "bar", "baz").toString());
props.put("otel.exporter.otlp.headers", "header-key=dummy-value");
props.put("otel.exporter.otlp.timeout", "10s");
props.put("otel.exporter.otlp.metrics.endpoint", "http://localhost:" + server.httpPort());
props.put("otel.exporter.otlp.metrics.endpoint", "https://localhost:" + server.httpsPort());
props.put(
"otel.exporter.otlp.metrics.certificate", certificate.certificateFile().getAbsolutePath());
props.put("otel.exporter.otlp.metrics.headers", "header-key=header-value");
props.put("otel.exporter.otlp.metrics.timeout", "5s");
MetricExporter metricExporter =
@ -233,29 +238,22 @@ class OtlpConfigTest {
&& headers.contains("header-key", "header-value"));
}
@Test
void configureTls() {
Map<String, String> props = new HashMap<>();
props.put("otel.exporter.otlp.endpoint", "https://localhost:" + server.httpsPort());
props.put("otel.exporter.otlp.certificate", certificate.certificateFile().getAbsolutePath());
SpanExporter spanExporter =
SpanExporterConfiguration.configureExporter("otlp", ConfigProperties.createForTest(props));
assertThat(
spanExporter
.export(Lists.newArrayList(generateFakeSpan()))
.join(10, TimeUnit.SECONDS)
.isSuccess())
.isTrue();
assertThat(otlpTraceRequests).hasSize(1);
}
@Test
void configureTlsInvalidCertificatePath() {
System.setProperty("otel.exporter.otlp.certificate", Paths.get("foo", "bar", "baz").toString());
Map<String, String> props = new HashMap<>();
props.put("otel.exporter.otlp.certificate", Paths.get("foo", "bar", "baz").toString());
ConfigProperties properties = ConfigProperties.createForTest(props);
assertThatThrownBy(OpenTelemetrySdkAutoConfiguration::initialize)
.isInstanceOf(ConfigurationException.class);
assertThatThrownBy(() -> SpanExporterConfiguration.configureExporter("otlp", properties))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Invalid OTLP certificate path:");
assertThatThrownBy(
() ->
MetricExporterConfiguration.configureOtlpMetrics(
properties, SdkMeterProvider.builder().build()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Invalid OTLP certificate path:");
}
private static SpanData generateFakeSpan() {
@ -291,7 +289,9 @@ class OtlpConfigTest {
@Test
void configuresGlobal() {
System.setProperty("otel.exporter.otlp.endpoint", "http://localhost:" + server.httpPort());
System.setProperty("otel.exporter.otlp.endpoint", "https://localhost:" + server.httpsPort());
System.setProperty(
"otel.exporter.otlp.certificate", certificate.certificateFile().getAbsolutePath());
System.setProperty("otel.imr.export.interval", "1s");
GlobalOpenTelemetry.get().getTracer("test").spanBuilder("test").startSpan().end();