diff --git a/Cargo.toml b/Cargo.toml index 1445047825..1c5f56f0e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,3 +50,5 @@ tracing = { version = ">=0.1.40", default-features = false } tracing-core = { version = ">=0.1.33", default-features = false } tracing-subscriber = { version = "0.3", default-features = false } url = { version = "2.5", default-features = false } +rcgen = { version = "0.13", features = ["crypto"] } +tempfile = "3.14" diff --git a/opentelemetry-otlp/CHANGELOG.md b/opentelemetry-otlp/CHANGELOG.md index ce4b760618..c6a5136b9c 100644 --- a/opentelemetry-otlp/CHANGELOG.md +++ b/opentelemetry-otlp/CHANGELOG.md @@ -6,6 +6,7 @@ - Feature flag "populate-logs-event-name" is removed as no longer relevant. LogRecord's `event_name()` is now automatically populated on the newly added "event_name" field in LogRecord proto definition. +- TLS configuration via environment variables for GRPc exporters. ## 0.27.0 diff --git a/opentelemetry-otlp/Cargo.toml b/opentelemetry-otlp/Cargo.toml index cebe075082..e67fd30ede 100644 --- a/opentelemetry-otlp/Cargo.toml +++ b/opentelemetry-otlp/Cargo.toml @@ -51,6 +51,8 @@ opentelemetry_sdk = { features = ["trace", "rt-tokio", "testing"], path = "../op tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } futures-util = { workspace = true } temp-env = { workspace = true } +rcgen = { workspace = true } +tempfile = { workspace = true } [features] # telemetry pillars and functions diff --git a/opentelemetry-otlp/src/exporter/mod.rs b/opentelemetry-otlp/src/exporter/mod.rs index bec1c809bb..f7e0c9affc 100644 --- a/opentelemetry-otlp/src/exporter/mod.rs +++ b/opentelemetry-otlp/src/exporter/mod.rs @@ -28,6 +28,19 @@ pub const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// Compression algorithm to use, defaults to none. pub const OTEL_EXPORTER_OTLP_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_COMPRESSION"; +/// Certificate file to validate the OTLP server connection +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_CERTIFICATE: &str = "OTEL_EXPORTER_OTLP_CERTIFICATE"; +/// Path to the certificate file to use for client authentication (mTLS). +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: &str = "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE"; +/// Path to the key file to use for client authentication (mTLS). +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_CLIENT_KEY: &str = "OTEL_EXPORTER_OTLP_CLIENT_KEY"; +/// Use insecure connection. Disable TLS +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_INSECURE: &str = "OTEL_EXPORTER_OTLP_INSECURE"; + #[cfg(feature = "http-json")] /// Default protocol, using http-json. pub const OTEL_EXPORTER_OTLP_PROTOCOL_DEFAULT: &str = OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON; @@ -76,6 +89,18 @@ pub struct ExportConfig { /// The timeout to the collector. pub timeout: Duration, + + /// Disable TLS + pub insecure: Option, + + /// The certificate file to validate the OTLP server connection + pub certificate: Option, + + /// The path to the certificate file to use for client authentication (mTLS). + pub client_certificate: Option, + + /// The path to the key file to use for client authentication (mTLS). + pub client_key: Option, } impl Default for ExportConfig { @@ -88,6 +113,10 @@ impl Default for ExportConfig { // won't know if user provided a value protocol, timeout: Duration::from_secs(OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT), + insecure: None, + certificate: None, + client_certificate: None, + client_key: None, } } } @@ -195,6 +224,17 @@ pub trait WithExportConfig { fn with_timeout(self, timeout: Duration) -> Self; /// Set export config. This will override all previous configuration. fn with_export_config(self, export_config: ExportConfig) -> Self; + /// Set insecure connection. Disable TLS + fn with_insecure(self) -> Self; + /// Set the certificate file to validate the OTLP server connection + /// This is only available when the `tls` feature is enabled. + fn with_certificate>(self, certificate: T) -> Self; + /// Set the path to the certificate file to use for client authentication (mTLS). + /// This is only available when the `tls` feature is enabled. + fn with_client_certificate>(self, client_certificate: T) -> Self; + /// Set the path to the key file to use for client authentication (mTLS). + /// This is only available when the `tls` feature is enabled. + fn with_client_key>(self, client_key: T) -> Self; } impl WithExportConfig for B { @@ -217,6 +257,27 @@ impl WithExportConfig for B { self.export_config().endpoint = exporter_config.endpoint; self.export_config().protocol = exporter_config.protocol; self.export_config().timeout = exporter_config.timeout; + self.export_config().insecure = Some(true); + self + } + + fn with_insecure(mut self) -> Self { + self.export_config().insecure = Some(true); + self + } + + fn with_certificate>(mut self, certificate: T) -> Self { + self.export_config().certificate = Some(certificate.into()); + self + } + + fn with_client_certificate>(mut self, client_certificate: T) -> Self { + self.export_config().client_certificate = Some(client_certificate.into()); + self + } + + fn with_client_key>(mut self, client_key: T) -> Self { + self.export_config().client_key = Some(client_key.into()); self } } diff --git a/opentelemetry-otlp/src/exporter/tonic/mod.rs b/opentelemetry-otlp/src/exporter/tonic/mod.rs index 9e2b54c631..e8c2432cd9 100644 --- a/opentelemetry-otlp/src/exporter/tonic/mod.rs +++ b/opentelemetry-otlp/src/exporter/tonic/mod.rs @@ -1,5 +1,7 @@ use std::env; use std::fmt::{Debug, Formatter}; +#[cfg(feature = "tls")] +use std::fs; use std::str::FromStr; use std::time::Duration; @@ -10,7 +12,7 @@ use tonic::metadata::{KeyAndValueRef, MetadataMap}; use tonic::service::Interceptor; use tonic::transport::Channel; #[cfg(feature = "tls")] -use tonic::transport::ClientTlsConfig; +use tonic::transport::{Certificate, ClientTlsConfig, Identity}; use super::{default_headers, parse_header_string, OTEL_EXPORTER_OTLP_GRPC_ENDPOINT_DEFAULT}; use crate::exporter::Compression; @@ -19,6 +21,12 @@ use crate::{ OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, }; +#[cfg(feature = "tls")] +use crate::{ + OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, OTEL_EXPORTER_OTLP_INSECURE, +}; + #[cfg(feature = "logs")] pub(crate) mod logs; @@ -145,12 +153,17 @@ impl Default for TonicExporterBuilder { } impl TonicExporterBuilder { + #[allow(clippy::too_many_arguments)] fn build_channel( self, signal_endpoint_var: &str, signal_timeout_var: &str, signal_compression_var: &str, signal_headers_var: &str, + #[cfg(feature = "tls")] signal_insecure_var: &str, + #[cfg(feature = "tls")] signal_certificate_var: &str, + #[cfg(feature = "tls")] signal_client_cert_var: &str, + #[cfg(feature = "tls")] signal_client_key_var: &str, ) -> Result<(Channel, BoxInterceptor, Option), crate::Error> { let compression = self.resolve_compression(signal_compression_var)?; @@ -207,20 +220,107 @@ impl TonicExporterBuilder { }; #[cfg(feature = "tls")] - let channel = match self.tonic_config.tls_config { - Some(tls_config) => endpoint - .tls_config(tls_config) - .map_err(crate::Error::from)?, - None => endpoint, + { + let insecure = config.insecure.unwrap_or_else(|| { + env::var(signal_insecure_var) + .or_else(|_| env::var(OTEL_EXPORTER_OTLP_INSECURE)) + .map_or(false, |x| { + if x == "1" { + true + } else if x == "0" || x != "true" || x != "false" { + false + } else { + bool::from_str(&x).unwrap_or(false) + } + }) + }); + + let channel = match self.tonic_config.tls_config { + Some(tls_config) => endpoint + .tls_config(tls_config) + .map_err(crate::Error::from)?, + None => { + if !insecure { + let tls_config = Self::resolve_tls_config( + signal_certificate_var, + signal_client_cert_var, + signal_client_key_var, + self.tonic_config.tls_config, + config.certificate, + config.client_certificate, + config.client_key, + )?; + endpoint + .tls_config(tls_config) + .map_err(crate::Error::from)? + } else { + endpoint + } + } + } + .timeout(timeout) + .connect_lazy(); + otel_debug!(name: "TonicChannelBuilt", endpoint = endpoint_clone, timeout_in_millisecs = timeout.as_millis(), compression = format!("{:?}", compression), headers = format!("{:?}", headers_for_logging)); + Ok((channel, interceptor, compression)) } - .timeout(timeout) - .connect_lazy(); #[cfg(not(feature = "tls"))] - let channel = endpoint.timeout(timeout).connect_lazy(); + { + otel_debug!(name: "TonicChannelBuilt", endpoint = endpoint_clone, timeout_in_millisecs = timeout.as_millis(), compression = format!("{:?}", compression), headers = format!("{:?}", headers_for_logging)); + let channel = endpoint.timeout(timeout).connect_lazy(); + Ok((channel, interceptor, compression)) + } + } - otel_debug!(name: "TonicChannelBuilt", endpoint = endpoint_clone, timeout_in_millisecs = timeout.as_millis(), compression = format!("{:?}", compression), headers = format!("{:?}", headers_for_logging)); - Ok((channel, interceptor, compression)) + #[cfg(feature = "tls")] + fn resolve_tls_config( + signal_certificate_var: &str, + signal_client_cert_var: &str, + signal_client_key_var: &str, + provided_tls_config: Option, + provided_certificate: Option, + provided_client_cert: Option, + provided_client_key: Option, + ) -> Result { + // User provided tls config. Use it. + if let Some(tls_config) = provided_tls_config { + return Ok(tls_config); + } + + // No user provided tls config. Try to build one from env vars. + let mut client_tls_config = ClientTlsConfig::new(); + + let ca_file = provided_certificate.or_else(|| { + env::var(signal_certificate_var) + .or_else(|_| env::var(OTEL_EXPORTER_OTLP_CERTIFICATE)) + .ok() + }); + let client_cert_file = provided_client_cert.or_else(|| { + env::var(signal_client_cert_var) + .or_else(|_| env::var(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE)) + .ok() + }); + let client_key_file = provided_client_key.or_else(|| { + env::var(signal_client_key_var) + .or_else(|_| env::var(OTEL_EXPORTER_OTLP_CLIENT_KEY)) + .ok() + }); + if let Some(ca_path) = ca_file { + let ca_cert = + std::fs::read(ca_path).map_err(|x| crate::Error::TLSConfigError(x.to_string()))?; + client_tls_config = client_tls_config.ca_certificate(Certificate::from_pem(ca_cert)); + } + + if let (Some(cert_path), Some(key_path)) = (client_cert_file, client_key_file) { + let cert = + fs::read(cert_path).map_err(|x| crate::Error::TLSConfigError(x.to_string()))?; + let key = + fs::read(key_path).map_err(|x| crate::Error::TLSConfigError(x.to_string()))?; + + let identity = Identity::from_pem(cert, key); + client_tls_config = client_tls_config.identity(identity); + } + Ok(client_tls_config) } fn resolve_endpoint(default_endpoint_var: &str, provided_endpoint: Option) -> String { @@ -263,17 +363,35 @@ impl TonicExporterBuilder { use crate::exporter::tonic::logs::TonicLogsClient; otel_debug!(name: "LogsTonicChannelBuilding"); + #[cfg(not(feature = "tls"))] + { + let (channel, interceptor, compression) = self.build_channel( + crate::logs::OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_HEADERS, + )?; + let client = TonicLogsClient::new(channel, interceptor, compression); + + Ok(crate::logs::LogExporter::from_tonic(client)) + } - let (channel, interceptor, compression) = self.build_channel( - crate::logs::OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, - crate::logs::OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, - crate::logs::OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, - crate::logs::OTEL_EXPORTER_OTLP_LOGS_HEADERS, - )?; - - let client = TonicLogsClient::new(channel, interceptor, compression); - - Ok(crate::logs::LogExporter::from_tonic(client)) + #[cfg(feature = "tls")] + { + let (channel, interceptor, compression) = self.build_channel( + crate::logs::OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_HEADERS, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_INSECURE, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + )?; + + let client = TonicLogsClient::new(channel, interceptor, compression); + Ok(crate::logs::LogExporter::from_tonic(client)) + } } /// Build a new tonic metrics exporter @@ -287,16 +405,36 @@ impl TonicExporterBuilder { otel_debug!(name: "MetricsTonicChannelBuilding"); - let (channel, interceptor, compression) = self.build_channel( - crate::metric::OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, - crate::metric::OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, - crate::metric::OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, - crate::metric::OTEL_EXPORTER_OTLP_METRICS_HEADERS, - )?; - - let client = TonicMetricsClient::new(channel, interceptor, compression); + #[cfg(not(feature = "tls"))] + { + let (channel, interceptor, compression) = self.build_channel( + crate::metric::OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_HEADERS, + )?; + + let client = TonicMetricsClient::new(channel, interceptor, compression); + Ok(MetricExporter::new(client, temporality)) + } - Ok(MetricExporter::new(client, temporality)) + #[cfg(feature = "tls")] + { + let (channel, interceptor, compression) = self.build_channel( + crate::metric::OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_HEADERS, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_INSECURE, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, + )?; + + let client = TonicMetricsClient::new(channel, interceptor, compression); + + Ok(MetricExporter::new(client, temporality)) + } } /// Build a new tonic span exporter @@ -308,16 +446,37 @@ impl TonicExporterBuilder { otel_debug!(name: "TracesTonicChannelBuilding"); - let (channel, interceptor, compression) = self.build_channel( - crate::span::OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, - crate::span::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, - crate::span::OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, - crate::span::OTEL_EXPORTER_OTLP_TRACES_HEADERS, - )?; + #[cfg(not(feature = "tls"))] + { + let (channel, interceptor, compression) = self.build_channel( + crate::span::OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + crate::span::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + crate::span::OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + crate::span::OTEL_EXPORTER_OTLP_TRACES_HEADERS, + )?; - let client = TonicTracesClient::new(channel, interceptor, compression); + let client = TonicTracesClient::new(channel, interceptor, compression); - Ok(crate::SpanExporter::new(client)) + Ok(crate::SpanExporter::new(client)) + } + + #[cfg(feature = "tls")] + { + let (channel, interceptor, compression) = self.build_channel( + crate::span::OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + crate::span::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + crate::span::OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + crate::span::OTEL_EXPORTER_OTLP_TRACES_HEADERS, + crate::span::OTEL_EXPORTER_OTLP_TRACES_INSECURE, + crate::span::OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + crate::span::OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, + crate::span::OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, + )?; + + let client = TonicTracesClient::new(channel, interceptor, compression); + + Ok(crate::SpanExporter::new(client)) + } } } diff --git a/opentelemetry-otlp/src/lib.rs b/opentelemetry-otlp/src/lib.rs index 3e02ef16a8..cafe83ca6c 100644 --- a/opentelemetry-otlp/src/lib.rs +++ b/opentelemetry-otlp/src/lib.rs @@ -235,12 +235,23 @@ pub use crate::span::{ OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, }; +#[cfg(all(feature = "trace", feature = "tls"))] +pub use crate::span::{ + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_INSECURE, +}; + #[cfg(feature = "metrics")] #[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] pub use crate::metric::{ MetricExporter, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, }; +#[cfg(all(feature = "metrics", feature = "tls"))] +pub use crate::metric::{ + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_INSECURE, +}; #[cfg(feature = "logs")] #[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] @@ -248,6 +259,11 @@ pub use crate::logs::{ LogExporter, OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, }; +#[cfg(all(feature = "metrics", feature = "tls"))] +pub use crate::logs::{ + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_INSECURE, +}; #[cfg(any(feature = "http-proto", feature = "http-json"))] pub use crate::exporter::http::{HasHttpConfig, WithHttpConfig}; @@ -262,6 +278,12 @@ pub use crate::exporter::{ OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT, }; +#[cfg(feature = "tls")] +pub use crate::exporter::{ + OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, OTEL_EXPORTER_OTLP_INSECURE, +}; + use opentelemetry_sdk::export::ExportError; /// Type to indicate the builder does not have a client set. @@ -357,6 +379,10 @@ pub enum Error { #[cfg(any(not(feature = "gzip-tonic"), not(feature = "zstd-tonic")))] #[error("feature '{0}' is required to use the compression algorithm '{1}'")] FeatureRequiredForCompressionAlgorithm(&'static str, Compression), + + /// TLS configuration error. + #[error("TLS configuration error: {0}")] + TLSConfigError(String), } #[cfg(feature = "grpc-tonic")] diff --git a/opentelemetry-otlp/src/logs.rs b/opentelemetry-otlp/src/logs.rs index aa4ea8fa07..edb448db0f 100644 --- a/opentelemetry-otlp/src/logs.rs +++ b/opentelemetry-otlp/src/logs.rs @@ -33,6 +33,20 @@ pub const OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_LOGS_TIMEO /// Note: this is only supported for HTTP. pub const OTEL_EXPORTER_OTLP_LOGS_HEADERS: &str = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"; +/// Certificate file to validate the OTLP server connection when sending metrics +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: &str = "OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE"; +/// Path to the certificate file to use for client authentication (mTLS) when sending metrics. +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE: &str = + "OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE"; +/// Path to the key file to use for client authentication (mTLS) when sending metrics. +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY: &str = "OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY"; +/// Use insecure connection when sending metrics. Disable TLS +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_LOGS_INSECURE: &str = "OTEL_EXPORTER_OTLP_LOGS_INSECURE"; + #[derive(Debug, Default, Clone)] pub struct LogExporterBuilder { client: C, diff --git a/opentelemetry-otlp/src/metric.rs b/opentelemetry-otlp/src/metric.rs index c8bdb38d9e..3b560580da 100644 --- a/opentelemetry-otlp/src/metric.rs +++ b/opentelemetry-otlp/src/metric.rs @@ -36,6 +36,20 @@ pub const OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_MET /// Example: `k1=v1,k2=v2` /// Note: this is only supported for HTTP. pub const OTEL_EXPORTER_OTLP_METRICS_HEADERS: &str = "OTEL_EXPORTER_OTLP_METRICS_HEADERS"; +/// +/// Certificate file to validate the OTLP server connection when sending metrics +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: &str = "OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE"; +/// Path to the certificate file to use for client authentication (mTLS) when sending metrics. +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: &str = + "OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE"; +/// Path to the key file to use for client authentication (mTLS) when sending metrics. +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: &str = "OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY"; +/// Use insecure connection when sending metrics. Disable TLS +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_METRICS_INSECURE: &str = "OTEL_EXPORTER_OTLP_METRICS_INSECURE"; #[derive(Debug, Default, Clone)] pub struct MetricExporterBuilder { diff --git a/opentelemetry-otlp/src/span.rs b/opentelemetry-otlp/src/span.rs index 190e3fdfce..f8b5dad3a9 100644 --- a/opentelemetry-otlp/src/span.rs +++ b/opentelemetry-otlp/src/span.rs @@ -35,6 +35,20 @@ pub const OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_TRAC /// Note: this is only supported for HTTP. pub const OTEL_EXPORTER_OTLP_TRACES_HEADERS: &str = "OTEL_EXPORTER_OTLP_TRACES_HEADERS"; +/// Certificate file to validate the OTLP server connection when sending traces +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: &str = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE"; +/// Path to the certificate file to use for client authentication (mTLS) when sending traces. +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: &str = + "OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE"; +/// Path to the key file to use for client authentication (mTLS) when sending traces. +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: &str = "OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY"; +/// Use insecure connection when sending trace. Disable TLS +#[cfg(feature = "tls")] +pub const OTEL_EXPORTER_OTLP_TRACES_INSECURE: &str = "OTEL_EXPORTER_OTLP_TRACES_INSECURE"; + #[derive(Debug, Default, Clone)] pub struct SpanExporterBuilder { client: C, diff --git a/opentelemetry-otlp/tests/smoke.rs b/opentelemetry-otlp/tests/smoke.rs index e9cd0da165..e5d38e0e40 100644 --- a/opentelemetry-otlp/tests/smoke.rs +++ b/opentelemetry-otlp/tests/smoke.rs @@ -6,11 +6,16 @@ use opentelemetry_proto::tonic::collector::trace::v1::{ trace_service_server::{TraceService, TraceServiceServer}, ExportTraceServiceRequest, ExportTraceServiceResponse, }; +use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, KeyPair}; +use std::fs::{self, set_permissions, Permissions}; +use std::os::unix::fs::PermissionsExt; use std::{net::SocketAddr, sync::Mutex}; +use tempfile::NamedTempFile; use tokio::sync::mpsc; use tokio_stream::wrappers::TcpListenerStream; #[cfg(feature = "gzip-tonic")] use tonic::codec::CompressionEncoding; +use tonic::transport::{Identity, ServerTlsConfig}; struct MockServer { tx: Mutex>, @@ -45,7 +50,9 @@ impl TraceService for MockServer { } } -async fn setup() -> (SocketAddr, mpsc::Receiver) { +async fn setup( + tls_config: Option, +) -> (SocketAddr, mpsc::Receiver) { let addr: SocketAddr = "[::1]:0".parse().unwrap(); let listener = tokio::net::TcpListener::bind(addr) .await @@ -65,11 +72,17 @@ async fn setup() -> (SocketAddr, mpsc::Receiver) { #[cfg(not(feature = "gzip-tonic"))] let service = TraceServiceServer::new(MockServer::new(req_tx)); tokio::task::spawn(async move { - tonic::transport::Server::builder() + let mut server = tonic::transport::Server::builder(); + if let Some(tls_config) = tls_config { + server = server + .tls_config(tls_config) + .expect("failed to set tls config"); + } + server .add_service(service) .serve_with_incoming(stream) .await - .expect("Server failed"); + .expect("Server failed") }); (addr, req_rx) } @@ -77,7 +90,7 @@ async fn setup() -> (SocketAddr, mpsc::Receiver) { #[tokio::test(flavor = "multi_thread")] async fn smoke_tracer() { println!("Starting server setup..."); - let (addr, mut req_rx) = setup().await; + let (addr, mut req_rx) = setup(None).await; { println!("Installing tracer provider..."); @@ -90,6 +103,7 @@ async fn smoke_tracer() { .with_tonic() .with_compression(opentelemetry_otlp::Compression::Gzip) .with_endpoint(format!("http://{}", addr)) + .with_insecure() .with_metadata(metadata) .build() .expect("gzip-tonic SpanExporter failed to build"), @@ -97,6 +111,7 @@ async fn smoke_tracer() { opentelemetry_otlp::SpanExporter::builder() .with_tonic() .with_endpoint(format!("http://{}", addr)) + .with_insecure() .with_metadata(metadata) .build() .expect("NON gzip-tonic SpanExporter failed to build"), @@ -136,3 +151,150 @@ async fn smoke_tracer() { let first_event = first_span.events.first().unwrap(); assert_eq!("my-test-event", first_event.name); } + +#[tokio::test(flavor = "multi_thread")] +async fn smoke_tls_tracer() { + let (server_ca, server_cert, server_key) = generate_tls_certs(); + let (client_ca, client_cert, client_key) = generate_tls_certs(); + + let server_ca_file = NamedTempFile::new().unwrap(); + let server_cert_file = NamedTempFile::new().unwrap(); + let server_key_file = NamedTempFile::new().unwrap(); + + let client_ca_file = NamedTempFile::new().unwrap(); + let client_cert_file = NamedTempFile::new().unwrap(); + let client_key_file = NamedTempFile::new().unwrap(); + + let files_and_contents = [ + (server_ca_file.path(), &server_ca), + (server_cert_file.path(), &server_cert), + (server_key_file.path(), &server_key), + (client_ca_file.path(), &client_ca), + (client_cert_file.path(), &client_cert), + (client_key_file.path(), &client_key), + ]; + + for (file_path, content) in &files_and_contents { + fs::write(file_path, content).unwrap(); + } + + let permissions = Permissions::from_mode(0o666); + let files_to_set_permissions = [ + server_ca_file.path(), + server_cert_file.path(), + server_key_file.path(), + client_ca_file.path(), + client_cert_file.path(), + client_key_file.path(), + ]; + + for file_path in &files_to_set_permissions { + set_permissions(file_path, permissions.clone()).unwrap(); + } + + println!("Starting server setup..."); + let tls_config = ServerTlsConfig::new() + .identity(Identity::from_pem(server_cert, server_key)) + .client_ca_root(tonic::transport::Certificate::from_pem(client_ca)) + .client_auth_optional(false); + let (addr, mut req_rx) = setup(Some(tls_config)).await; + + { + println!("Installing tracer provider..."); + let mut metadata = tonic::metadata::MetadataMap::new(); + metadata.insert("x-header-key", "header-value".parse().unwrap()); + let tracer_provider = opentelemetry_sdk::trace::TracerProvider::builder() + .with_batch_exporter( + #[cfg(feature = "gzip-tonic")] + opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_compression(opentelemetry_otlp::Compression::Gzip) + // Due a limitation in rustls, it's not possible to use the + // addr directely. It's not possible to use IP address. Domain + // name is required. + // https://github.com/hyperium/tonic/issues/279 + .with_endpoint(format!("https://localhost:{}", addr.port())) + .with_certificate(server_ca_file.path().to_str().expect("Missing server CA")) + .with_client_certificate( + client_cert_file + .path() + .to_str() + .expect("Missing client certificate"), + ) + .with_client_key(client_key_file.path().to_str().expect("Missing client key")) + .with_metadata(metadata) + .build() + .expect("gzip-tonic SpanExporter failed to build"), + #[cfg(not(feature = "gzip-tonic"))] + opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(format!("http://{}", addr)) + .with_certificate(server_ca_file.path().to_str().expect("Missing server CA")) + .with_client_certificate( + client_cert_file + .path() + .to_str() + .expect("Missing client certificate"), + ) + .with_client_key(client_key_file.path().to_str().expect("Missing client key")) + .with_metadata(metadata) + .build() + .expect("NON gzip-tonic SpanExporter failed to build"), + ) + .build(); + + global::set_tracer_provider(tracer_provider.clone()); + + let tracer = global::tracer("smoke"); + + println!("Sending span..."); + let mut span = tracer + .span_builder("my-test-span") + .with_kind(SpanKind::Server) + .start(&tracer); + span.add_event("my-test-event", vec![]); + span.end(); + + tracer_provider + .shutdown() + .expect("tracer_provider should shutdown successfully"); + } + + println!("Waiting for request..."); + let req = req_rx.recv().await.expect("missing export request"); + let first_span = req + .resource_spans + .first() + .unwrap() + .scope_spans + .first() + .unwrap() + .spans + .first() + .unwrap(); + assert_eq!("my-test-span", first_span.name); + let first_event = first_span.events.first().unwrap(); + assert_eq!("my-test-event", first_event.name); +} + +fn generate_tls_certs() -> (String, String, String) { + let ca_key = KeyPair::generate().unwrap(); + let mut params = CertificateParams::new(vec!["My Test CA".to_string()]).unwrap(); + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + let ca_cert = params.self_signed(&ca_key).unwrap(); + let ca_cert_pem = ca_cert.pem(); + + let mut params = CertificateParams::new(vec!["localhost".to_string()]).unwrap(); + params + .distinguished_name + .push(DnType::OrganizationName, "OpenTelemetry"); + params + .distinguished_name + .push(DnType::CommonName, "opentelemetry.io"); + + let cert_key = KeyPair::generate().unwrap(); + let cert = params.signed_by(&cert_key, &ca_cert, &ca_key).unwrap(); + let key = cert_key.serialize_pem(); + + (ca_cert_pem, cert.pem(), key) +}