From 3023fc346e0deb7a1a88d95f23c79595d8cfd7cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Guilherme=20Vanz?= Date: Mon, 23 Dec 2024 13:47:33 -0300 Subject: [PATCH 1/4] feat: configure TLS with environment variables. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the opentelemetry-otlp crate to allow users to configure TLS using environment variables. Removing the need to crating the TLS config object and defining it with the `with_tls_config` method. In the same way other OTLP libraries does (e.g. go lang). Signed-off-by: José Guilherme Vanz --- Cargo.toml | 5 +- opentelemetry-otlp/CHANGELOG.md | 73 +++--- opentelemetry-otlp/Cargo.toml | 70 +++++- opentelemetry-otlp/src/exporter/mod.rs | 61 +++++ opentelemetry-otlp/src/exporter/tonic/mod.rs | 236 ++++++++++++++++--- opentelemetry-otlp/src/lib.rs | 126 ++++++++++ opentelemetry-otlp/src/logs.rs | 14 ++ opentelemetry-otlp/src/metric.rs | 14 ++ opentelemetry-otlp/src/span.rs | 14 ++ opentelemetry-otlp/tests/smoke.rs | 170 ++++++++++++- 10 files changed, 699 insertions(+), 84 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b19195b09e..81539b0056 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ tokio-stream = "0.1" # required for OpenTelemetry's internal logging macros. tracing = { version = ">=0.1.40", default-features = false } # `tracing-core >=0.1.33` is required for compatibility with `tracing >=0.1.40`. -tracing-core = { version = ">=0.1.33", 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 } anyhow = "1.0.94" @@ -71,12 +71,13 @@ percent-encoding = "2.0" rstest = "0.23.0" schemars = "0.8" sysinfo = "0.32" -tempfile = "3.3.0" testcontainers = "0.23.1" tracing-log = "0.2" tracing-opentelemetry = "0.30" typed-builder = "0.20" uuid = "1.3" +rcgen = { version = "0.13", features = ["crypto"] } +tempfile = "3.14" # Aviod use of crates.io version of these crates through the tracing-opentelemetry dependencies [patch.crates-io] diff --git a/opentelemetry-otlp/CHANGELOG.md b/opentelemetry-otlp/CHANGELOG.md index 4ad96907a7..0a0cf94067 100644 --- a/opentelemetry-otlp/CHANGELOG.md +++ b/opentelemetry-otlp/CHANGELOG.md @@ -3,6 +3,7 @@ ## vNext - Update `tonic` dependency version to 0.13 +- TLS configuration via environment variables for GRPc exporters. ## 0.29.0 @@ -19,16 +20,16 @@ Released 2025-Mar-21 [#2770](https://github.com/open-telemetry/opentelemetry-rust/issues/2770) partially to properly handle `shutdown()` when using `http`. (`tonic` still does not do proper shutdown) -- *Breaking* - ExporterBuilder's build() method now Result with `ExporterBuildError` being the - Error variant. Previously it returned signal specific errors like `LogError` - from the `opentelemetry_sdk`, which are no longer part of the sdk. No changes - required if you were using unwrap/expect. If you were matching on the returning - Error enum, replace with the enum `ExporterBuildError`. Unlike the previous - `Error` which contained many variants unrelated to building an exporter, the - new one returns specific variants applicable to building an exporter. Some - variants might be applicable only on select features. - Also, now unused `Error` enum is removed. +- _Breaking_ + ExporterBuilder's build() method now Result with `ExporterBuildError` being the + Error variant. Previously it returned signal specific errors like `LogError` + from the `opentelemetry_sdk`, which are no longer part of the sdk. No changes + required if you were using unwrap/expect. If you were matching on the returning + Error enum, replace with the enum `ExporterBuildError`. Unlike the previous + `Error` which contained many variants unrelated to building an exporter, the + new one returns specific variants applicable to building an exporter. Some + variants might be applicable only on select features. + Also, now unused `Error` enum is removed. - **Breaking** `ExportConfig`'s `timeout` field is now optional(`Option`) - **Breaking** Export configuration done via code is final. ENV variables cannot be used to override the code config. Do not use code based config, if there is desire to control the settings via ENV variables. @@ -58,10 +59,10 @@ Released 2025-Feb-10 - The HTTP clients (reqwest, reqwest-blocking, hyper) now support the export timeout interval configured in below order - Signal specific env variable `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT`, - `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` or `OTEL_EXPORTER_OTLP_TIMEOUT`. + `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` or `OTEL_EXPORTER_OTLP_TIMEOUT`. - `OTEL_EXPORTER_OTLP_TIMEOUT` env variable. - `with_http().with_timeout()` API method of -`LogExporterBuilder` and `SpanExporterBuilder` and `MetricsExporterBuilder`. + `LogExporterBuilder` and `SpanExporterBuilder` and `MetricsExporterBuilder`. - The default interval of 10 seconds is used if none is configured. ## 0.27.0 @@ -74,6 +75,7 @@ Released 2024-Nov-11 - Update `opentelemetry-proto` dependency version to 0.27 - **BREAKING**: + - ([#2217](https://github.com/open-telemetry/opentelemetry-rust/pull/2217)) **Replaced**: The `MetricsExporterBuilder` interface is modified from `with_temporality_selector` to `with_temporality` example can be seen below: Previous Signature: ```rust @@ -84,6 +86,7 @@ Released 2024-Nov-11 MetricsExporterBuilder::default().with_temporality(opentelemetry_sdk::metrics::Temporality::Delta) ``` - ([#2221](https://github.com/open-telemetry/opentelemetry-rust/pull/2221)) **Replaced**: + - The `opentelemetry_otlp::new_pipeline().{trace,logging,metrics}()` interface is now replaced with `{TracerProvider,SdkMeterProvider,LoggerProvider}::builder()`. - The `opentelemetry_otlp::new_exporter()` interface is now replaced with `{SpanExporter,MetricsExporter,LogExporter}::builder()`. @@ -91,6 +94,7 @@ Released 2024-Nov-11 and [basic-otlp](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp/src/main.rs) for more details: Previous Signature: + ```rust let logger_provider: LoggerProvider = opentelemetry_otlp::new_pipeline() .logging() @@ -102,7 +106,9 @@ Released 2024-Nov-11 ) .install_batch(runtime::Tokio)?; ``` + Updated Signature: + ```rust let exporter = LogExporter::builder() .with_tonic() @@ -114,16 +120,19 @@ Released 2024-Nov-11 .with_batch_exporter(exporter, runtime::Tokio) .build()) ``` + - **Renamed** + - ([#2255](https://github.com/open-telemetry/opentelemetry-rust/pull/2255)): de-pluralize Metric types. - `MetricsExporter` -> `MetricExporter` - `MetricsExporterBuilder` -> `MetricExporterBuilder` - [#2263](https://github.com/open-telemetry/opentelemetry-rust/pull/2263) - Support `hyper` client for opentelemetry-otlp. This can be enabled using flag `hyper-client`. - Refer example: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-otlp/examples/basic-otlp-http + Support `hyper` client for opentelemetry-otlp. This can be enabled using flag `hyper-client`. + Refer example: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-otlp/examples/basic-otlp-http ## v0.26.0 + Released 2024-Sep-30 - Update `opentelemetry` dependency version to 0.26 @@ -141,11 +150,11 @@ Released 2024-Sep-30 - Starting with this version, this crate will align with `opentelemetry` crate on major,minor versions. - **Breaking** -The logrecord event-name is added as an attribute only if the feature flag -`populate-logs-event-name` is enabled. The name of the attribute is changed from -"name" to "event.name". -[1994](https://github.com/open-telemetry/opentelemetry-rust/pull/1994), -[2050](https://github.com/open-telemetry/opentelemetry-rust/pull/2050) + The logrecord event-name is added as an attribute only if the feature flag + `populate-logs-event-name` is enabled. The name of the attribute is changed from + "name" to "event.name". + [1994](https://github.com/open-telemetry/opentelemetry-rust/pull/1994), + [2050](https://github.com/open-telemetry/opentelemetry-rust/pull/2050) ## v0.17.0 @@ -155,10 +164,10 @@ The logrecord event-name is added as an attribute only if the feature flag `global::set_meter_provider`. User who setup the pipeline must do it themselves using `global::set_meter_provider(meter_provider.clone());`. - Add `with_resource` on `OtlpLogPipeline`, replacing the `with_config` method. -Instead of using -`.with_config(Config::default().with_resource(RESOURCE::default()))` users must -now use `.with_resource(RESOURCE::default())` to configure Resource when using -`OtlpLogPipeline`. + Instead of using + `.with_config(Config::default().with_resource(RESOURCE::default()))` users must + now use `.with_resource(RESOURCE::default())` to configure Resource when using + `OtlpLogPipeline`. - **Breaking** The methods `OtlpTracePipeline::install_simple()` and `OtlpTracePipeline::install_batch()` would now return `TracerProvider` instead of `Tracer`. These methods would also no longer set the global tracer provider. It would now be the responsibility of users to set it by calling `global::set_tracer_provider(tracer_provider.clone());`. Refer to the [basic-otlp](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp/src/main.rs) and [basic-otlp-http](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp-http/src/main.rs) examples on how to initialize OTLP Trace Exporter. - **Breaking** Correct the misspelling of "webkpi" to "webpki" in features [#1842](https://github.com/open-telemetry/opentelemetry-rust/pull/1842) @@ -190,9 +199,10 @@ now use `.with_resource(RESOURCE::default())` to configure Resource when using [#1568]: https://github.com/open-telemetry/opentelemetry-rust/pull/1568 ### Changed - - **Breaking** Remove global provider for Logs [#1691](https://github.com/open-telemetry/opentelemetry-rust/pull/1691/) - - The method OtlpLogPipeline::install_simple() and OtlpLogPipeline::install_batch() now return `LoggerProvider` instead of - `Logger`. Refer to the [basic-otlp](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp/src/main.rs) and [basic-otlp-http](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp-http/src/main.rs) examples for how to initialize OTLP Log Exporter to use with OpenTelemetryLogBridge and OpenTelemetryTracingBridge respectively. + +- **Breaking** Remove global provider for Logs [#1691](https://github.com/open-telemetry/opentelemetry-rust/pull/1691/) + - The method OtlpLogPipeline::install_simple() and OtlpLogPipeline::install_batch() now return `LoggerProvider` instead of + `Logger`. Refer to the [basic-otlp](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp/src/main.rs) and [basic-otlp-http](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp-http/src/main.rs) examples for how to initialize OTLP Log Exporter to use with OpenTelemetryLogBridge and OpenTelemetryTracingBridge respectively. - Update `opentelemetry` dependency version to 0.23 - Update `opentelemetry_sdk` dependency version to 0.23 - Update `opentelemetry-http` dependency version to 0.12 @@ -202,16 +212,19 @@ now use `.with_resource(RESOURCE::default())` to configure Resource when using ### Added -- Support custom channels in topic exporters [#1335](https://github.com/open-telemetry/opentelemetry-rust/pull/1335) +- Support custom channels in topic exporters [#1335](https://github.com/open-telemetry/opentelemetry-rust/pull/1335) - Allow specifying OTLP Tonic metadata from env variable [#1377](https://github.com/open-telemetry/opentelemetry-rust/pull/1377) ### Changed + - Update to tonic 0.11 and prost 0.12 [#1536](https://github.com/open-telemetry/opentelemetry-rust/pull/1536) ### Fixed + - Fix `tonic()` to the use correct port. [#1556](https://github.com/open-telemetry/opentelemetry-rust/pull/1556) ### Removed + - **Breaking** Remove support for surf HTTP client [#1537](https://github.com/open-telemetry/opentelemetry-rust/pull/1537) - **Breaking** Remove support for grpcio transport [#1534](https://github.com/open-telemetry/opentelemetry-rust/pull/1534) @@ -268,7 +281,6 @@ now use `.with_resource(RESOURCE::default())` to configure Resource when using - Change to export using v0.19.0 protobuf definitions. [#989](https://github.com/open-telemetry/opentelemetry-rust/pull/989). - Update dependencies and bump MSRV to 1.60 [#969](https://github.com/open-telemetry/opentelemetry-rust/pull/969). - ## v0.11.0 ### Changed @@ -321,13 +333,14 @@ now use `.with_resource(RESOURCE::default())` to configure Resource when using ### Changed -- Allow users to bring their own tonic channel #515 +- Allow users to bring their own tonic channel #515 - Remove default surf features #546 - Update to opentelemetry v0.14.0 ### v0.6.0 ### Added + - Examples on how to connect to an external otlp using tonic, tls and tokio #449 - Examples on how to connect to an external otlp using grpcio and tls #450 - `with_env` method for `OtlpPipelineBuilder` to use environment variables to config otlp pipeline #451 @@ -335,12 +348,14 @@ now use `.with_resource(RESOURCE::default())` to configure Resource when using - Mentioned `service.name` resource in README #476 ### Changed + - Update to opentelemetry v0.13.0 - Update `tonic-build` dependency to 0.4 #463 - Update the opentelemetry pipeline to use API to choose grpc layer instead of feature #467 - Rename trace config with_default_sampler to with_sampler #482 ### Removed + - Removed `from_env` and use environment variables to initialize the configurations by default #459 - Removed support for running tonic without tokio runtime #483 diff --git a/opentelemetry-otlp/Cargo.toml b/opentelemetry-otlp/Cargo.toml index 4b9be0c4e6..7c229a00a1 100644 --- a/opentelemetry-otlp/Cargo.toml +++ b/opentelemetry-otlp/Cargo.toml @@ -31,7 +31,7 @@ opentelemetry = { version = "0.29", default-features = false, path = "../opentel opentelemetry_sdk = { version = "0.29", default-features = false, path = "../opentelemetry-sdk" } opentelemetry-http = { version = "0.29", path = "../opentelemetry-http", optional = true } opentelemetry-proto = { version = "0.29", path = "../opentelemetry-proto", default-features = false } -tracing = {workspace = true, optional = true} +tracing = { workspace = true, optional = true } prost = { workspace = true, optional = true } tonic = { workspace = true, optional = true } @@ -46,26 +46,57 @@ serde_json = { workspace = true, optional = true } [dev-dependencies] tokio-stream = { workspace = true, features = ["net"] } # need tokio runtime to run smoke tests. -opentelemetry_sdk = { features = ["trace", "rt-tokio", "testing"], path = "../opentelemetry-sdk" } +opentelemetry_sdk = { features = [ + "trace", + "rt-tokio", + "testing", +], path = "../opentelemetry-sdk" } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } futures-util = { workspace = true } temp-env = { workspace = true } tonic = { workspace = true, features = ["router", "server"] } +rcgen = { workspace = true } +tempfile = { workspace = true } [features] # telemetry pillars and functions -trace = ["opentelemetry/trace", "opentelemetry_sdk/trace", "opentelemetry-proto/trace"] -metrics = ["opentelemetry/metrics", "opentelemetry_sdk/metrics", "opentelemetry-proto/metrics"] -logs = ["opentelemetry/logs", "opentelemetry_sdk/logs", "opentelemetry-proto/logs"] +trace = [ + "opentelemetry/trace", + "opentelemetry_sdk/trace", + "opentelemetry-proto/trace", +] +metrics = [ + "opentelemetry/metrics", + "opentelemetry_sdk/metrics", + "opentelemetry-proto/metrics", +] +logs = [ + "opentelemetry/logs", + "opentelemetry_sdk/logs", + "opentelemetry-proto/logs", +] internal-logs = ["tracing", "opentelemetry/internal-logs"] # add ons serialize = ["serde", "serde_json"] -default = ["http-proto", "reqwest-blocking-client", "trace", "metrics", "logs", "internal-logs"] +default = [ + "http-proto", + "reqwest-blocking-client", + "trace", + "metrics", + "logs", + "internal-logs", +] # grpc using tonic -grpc-tonic = ["tonic", "prost", "http", "tokio", "opentelemetry-proto/gen-tonic"] +grpc-tonic = [ + "tonic", + "prost", + "http", + "tokio", + "opentelemetry-proto/gen-tonic", +] gzip-tonic = ["tonic/gzip"] zstd-tonic = ["tonic/zstd"] tls = ["tonic/tls-ring"] @@ -73,12 +104,31 @@ tls-roots = ["tls", "tonic/tls-native-roots"] tls-webpki-roots = ["tls", "tonic/tls-webpki-roots"] # http binary -http-proto = ["prost", "opentelemetry-http", "opentelemetry-proto/gen-tonic-messages", "http", "trace", "metrics"] -http-json = ["serde_json", "prost", "opentelemetry-http", "opentelemetry-proto/gen-tonic-messages", "opentelemetry-proto/with-serde", "http", "trace", "metrics"] +http-proto = [ + "prost", + "opentelemetry-http", + "opentelemetry-proto/gen-tonic-messages", + "http", + "trace", + "metrics", +] +http-json = [ + "serde_json", + "prost", + "opentelemetry-http", + "opentelemetry-proto/gen-tonic-messages", + "opentelemetry-proto/with-serde", + "http", + "trace", + "metrics", +] reqwest-blocking-client = ["reqwest/blocking", "opentelemetry-http/reqwest"] reqwest-client = ["reqwest", "opentelemetry-http/reqwest"] reqwest-rustls = ["reqwest", "opentelemetry-http/reqwest-rustls"] -reqwest-rustls-webpki-roots = ["reqwest", "opentelemetry-http/reqwest-rustls-webpki-roots"] +reqwest-rustls-webpki-roots = [ + "reqwest", + "opentelemetry-http/reqwest-rustls-webpki-roots", +] hyper-client = ["opentelemetry-http/hyper"] # test diff --git a/opentelemetry-otlp/src/exporter/mod.rs b/opentelemetry-otlp/src/exporter/mod.rs index 28676e4566..92de33212d 100644 --- a/opentelemetry-otlp/src/exporter/mod.rs +++ b/opentelemetry-otlp/src/exporter/mod.rs @@ -29,6 +29,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; @@ -82,6 +95,18 @@ pub struct ExportConfig { /// /// Note: Programmatically setting this will override any value set via the environment variable. pub timeout: Option, + + /// 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 { @@ -94,6 +119,10 @@ impl Default for ExportConfig { // won't know if user provided a value protocol, timeout: None, + insecure: None, + certificate: None, + client_certificate: None, + client_key: None, } } } @@ -247,6 +276,17 @@ pub trait WithExportConfig { /// /// Note: Programmatically setting this will override any value set via environment variables. 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 { @@ -269,6 +309,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 948b605b17..f1e7c0c3ed 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 http::{HeaderMap, HeaderName, HeaderValue}; @@ -9,7 +11,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 super::{resolve_timeout, ExporterBuildError}; @@ -19,6 +21,12 @@ use crate::{ OTEL_EXPORTER_OTLP_HEADERS, }; +#[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; @@ -153,7 +161,11 @@ impl TonicExporterBuilder { signal_timeout_var: &str, signal_compression_var: &str, signal_headers_var: &str, - ) -> Result<(Channel, BoxInterceptor, Option), ExporterBuildError> { + #[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)?; let (headers_from_env, headers_for_logging) = parse_headers_from_env(signal_headers_var); @@ -201,20 +213,107 @@ impl TonicExporterBuilder { let timeout = resolve_timeout(signal_timeout_var, config.timeout.as_ref()); #[cfg(feature = "tls")] - let channel = match self.tonic_config.tls_config { - Some(tls_config) => endpoint - .tls_config(tls_config) - .map_err(|er| ExporterBuildError::InternalFailure(er.to_string()))?, - None => endpoint, + { + let insecure = config.insecure.unwrap_or_else(|| { + env::var(signal_insecure_var) + .or_else(|_| env::var(OTEL_EXPORTER_OTLP_INSECURE)) + .is_ok_and(|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)) + } + } + + #[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)); + } - 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)) + 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 { @@ -257,17 +356,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 @@ -281,16 +398,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::from_tonic(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 @@ -300,16 +437,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::from_tonic(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 05c84744cc..30a50a4c82 100644 --- a/opentelemetry-otlp/src/lib.rs +++ b/opentelemetry-otlp/src/lib.rs @@ -335,12 +335,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"))] @@ -348,6 +359,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}; @@ -362,6 +378,14 @@ 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::ExportError; + /// Type to indicate the builder does not have a client set. #[derive(Debug, Default, Clone)] pub struct NoExporterBuilderSet; @@ -391,6 +415,108 @@ pub use crate::exporter::tonic::{TonicConfig, TonicExporterBuilder}; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; +/// Wrap type for errors from this crate. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Wrap error from [`tonic::transport::Error`] + #[cfg(feature = "grpc-tonic")] + #[error("transport error {0}")] + Transport(#[from] tonic::transport::Error), + + /// Wrap the [`tonic::codegen::http::uri::InvalidUri`] error + #[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] + #[error("invalid URI {0}")] + InvalidUri(#[from] http::uri::InvalidUri), + + /// Wrap type for [`tonic::Status`] + #[cfg(feature = "grpc-tonic")] + #[error("the grpc server returns error ({code}): {message}")] + Status { + /// grpc status code + code: tonic::Code, + /// error message + message: String, + }, + + /// Http requests failed because no http client is provided. + #[cfg(any(feature = "http-proto", feature = "http-json"))] + #[error( + "no http client, you must select one from features or provide your own implementation" + )] + NoHttpClient, + + /// Http requests failed. + #[cfg(any(feature = "http-proto", feature = "http-json"))] + #[error("http request failed with {0}")] + RequestFailed(#[from] opentelemetry_http::HttpError), + + /// The provided value is invalid in HTTP headers. + #[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] + #[error("http header value error {0}")] + InvalidHeaderValue(#[from] http::header::InvalidHeaderValue), + + /// The provided name is invalid in HTTP headers. + #[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] + #[error("http header name error {0}")] + InvalidHeaderName(#[from] http::header::InvalidHeaderName), + + /// Prost encode failed + #[cfg(any( + feature = "http-proto", + all(feature = "http-json", not(feature = "trace")) + ))] + #[error("prost encoding error {0}")] + EncodeError(#[from] prost::EncodeError), + + /// The lock in exporters has been poisoned. + #[cfg(feature = "metrics")] + #[error("the lock of the {0} has been poisoned")] + PoisonedLock(&'static str), + + /// Unsupported compression algorithm. + #[error("unsupported compression algorithm '{0}'")] + UnsupportedCompressionAlgorithm(String), + + /// Feature required to use the specified compression algorithm. + #[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")] +impl From for Error { + fn from(status: tonic::Status) -> Error { + Error::Status { + code: status.code(), + message: { + if !status.message().is_empty() { + let mut result = ", detailed error message: ".to_string() + status.message(); + if status.code() == tonic::Code::Unknown { + let source = (&status as &dyn std::error::Error) + .source() + .map(|e| format!("{:?}", e)); + result.push(' '); + result.push_str(source.unwrap_or_default().as_ref()); + } + result + } else { + String::new() + } + }, + } + } +} + +impl ExportError for Error { + fn exporter_name(&self) -> &'static str { + "otlp" + } +} + /// The communication protocol to use when exporting data. #[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))] #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/opentelemetry-otlp/src/logs.rs b/opentelemetry-otlp/src/logs.rs index 7a8e66f6f8..51471ff649 100644 --- a/opentelemetry-otlp/src/logs.rs +++ b/opentelemetry-otlp/src/logs.rs @@ -31,6 +31,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 bdb383b47b..46da69c9a9 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 81c2cedd67..b77a32a84e 100644 --- a/opentelemetry-otlp/src/span.rs +++ b/opentelemetry-otlp/src/span.rs @@ -36,6 +36,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 910bb281ac..a38fdb4308 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,125 @@ 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"), + #[cfg(not(feature = "gzip-tonic"))] + 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"), + ) + .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); +} + +#[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"), @@ -97,6 +229,14 @@ async fn smoke_tracer() { 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"), @@ -136,3 +276,25 @@ async fn smoke_tracer() { 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) +} From b0ed39db877416acd1840c59e1a9abcf95b9faac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Guilherme=20Vanz?= Date: Wed, 22 Jan 2025 17:06:41 -0300 Subject: [PATCH 2/4] fix: add missing TLS configuration directives. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add missing TLS configuration directives. Signed-off-by: José Guilherme Vanz --- opentelemetry-otlp/src/exporter/mod.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/opentelemetry-otlp/src/exporter/mod.rs b/opentelemetry-otlp/src/exporter/mod.rs index 92de33212d..b14e6883ce 100644 --- a/opentelemetry-otlp/src/exporter/mod.rs +++ b/opentelemetry-otlp/src/exporter/mod.rs @@ -97,15 +97,19 @@ pub struct ExportConfig { pub timeout: Option, /// Disable TLS + #[cfg(feature = "tls")] pub insecure: Option, /// The certificate file to validate the OTLP server connection + #[cfg(feature = "tls")] pub certificate: Option, /// The path to the certificate file to use for client authentication (mTLS). + #[cfg(feature = "tls")] pub client_certificate: Option, /// The path to the key file to use for client authentication (mTLS). + #[cfg(feature = "tls")] pub client_key: Option, } @@ -119,9 +123,13 @@ impl Default for ExportConfig { // won't know if user provided a value protocol, timeout: None, + #[cfg(feature = "tls")] insecure: None, + #[cfg(feature = "tls")] certificate: None, + #[cfg(feature = "tls")] client_certificate: None, + #[cfg(feature = "tls")] client_key: None, } } @@ -277,15 +285,19 @@ pub trait WithExportConfig { /// Note: Programmatically setting this will override any value set via environment variables. fn with_export_config(self, export_config: ExportConfig) -> Self; /// Set insecure connection. Disable TLS + #[cfg(feature = "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. + #[cfg(feature = "tls")] 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. + #[cfg(feature = "tls")] 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. + #[cfg(feature = "tls")] fn with_client_key>(self, client_key: T) -> Self; } @@ -309,25 +321,32 @@ 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); + #[cfg(feature = "tls")] + { + self.export_config().insecure = Some(true); + } self } + #[cfg(feature = "tls")] fn with_insecure(mut self) -> Self { self.export_config().insecure = Some(true); self } + #[cfg(feature = "tls")] fn with_certificate>(mut self, certificate: T) -> Self { self.export_config().certificate = Some(certificate.into()); self } + #[cfg(feature = "tls")] fn with_client_certificate>(mut self, client_certificate: T) -> Self { self.export_config().client_certificate = Some(client_certificate.into()); self } + #[cfg(feature = "tls")] fn with_client_key>(mut self, client_key: T) -> Self { self.export_config().client_key = Some(client_key.into()); self From ec3389eda40d0dc678b187e5033205d868f1c82c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Guilherme=20Vanz?= Date: Mon, 27 Jan 2025 15:10:07 -0300 Subject: [PATCH 3/4] fix: add missing periods at the end of comments. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comments formatting adding missing periods at the end. Signed-off-by: José Guilherme Vanz --- opentelemetry-otlp/src/exporter/mod.rs | 5 ++--- opentelemetry-otlp/src/logs.rs | 5 ++--- opentelemetry-otlp/src/metric.rs | 5 ++--- opentelemetry-otlp/src/span.rs | 5 ++--- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/opentelemetry-otlp/src/exporter/mod.rs b/opentelemetry-otlp/src/exporter/mod.rs index b14e6883ce..63d4d6c0f0 100644 --- a/opentelemetry-otlp/src/exporter/mod.rs +++ b/opentelemetry-otlp/src/exporter/mod.rs @@ -28,8 +28,7 @@ pub const OTEL_EXPORTER_OTLP_HEADERS: &str = "OTEL_EXPORTER_OTLP_HEADERS"; 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 +/// 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). @@ -38,7 +37,7 @@ pub const OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: &str = "OTEL_EXPORTER_OTLP_CLIE /// 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 +/// Use insecure connection. Disable TLS. #[cfg(feature = "tls")] pub const OTEL_EXPORTER_OTLP_INSECURE: &str = "OTEL_EXPORTER_OTLP_INSECURE"; diff --git a/opentelemetry-otlp/src/logs.rs b/opentelemetry-otlp/src/logs.rs index 51471ff649..114a7f159c 100644 --- a/opentelemetry-otlp/src/logs.rs +++ b/opentelemetry-otlp/src/logs.rs @@ -30,8 +30,7 @@ pub const OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_LOGS_TIMEO /// Example: `k1=v1,k2=v2` /// 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 +/// 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. @@ -41,7 +40,7 @@ pub const OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE: &str = /// 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 +/// Use insecure connection when sending metrics. Disable TLS. #[cfg(feature = "tls")] pub const OTEL_EXPORTER_OTLP_LOGS_INSECURE: &str = "OTEL_EXPORTER_OTLP_LOGS_INSECURE"; diff --git a/opentelemetry-otlp/src/metric.rs b/opentelemetry-otlp/src/metric.rs index 46da69c9a9..3da9594c1d 100644 --- a/opentelemetry-otlp/src/metric.rs +++ b/opentelemetry-otlp/src/metric.rs @@ -36,8 +36,7 @@ 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 +/// 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. @@ -47,7 +46,7 @@ pub const OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: &str = /// 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 +/// Use insecure connection when sending metrics. Disable TLS. #[cfg(feature = "tls")] pub const OTEL_EXPORTER_OTLP_METRICS_INSECURE: &str = "OTEL_EXPORTER_OTLP_METRICS_INSECURE"; diff --git a/opentelemetry-otlp/src/span.rs b/opentelemetry-otlp/src/span.rs index b77a32a84e..e4c5127c3e 100644 --- a/opentelemetry-otlp/src/span.rs +++ b/opentelemetry-otlp/src/span.rs @@ -35,8 +35,7 @@ pub const OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_TRAC /// Example: `k1=v1,k2=v2` /// 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 +/// 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. @@ -46,7 +45,7 @@ pub const OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: &str = /// 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 +/// Use insecure connection when sending trace. Disable TLS. #[cfg(feature = "tls")] pub const OTEL_EXPORTER_OTLP_TRACES_INSECURE: &str = "OTEL_EXPORTER_OTLP_TRACES_INSECURE"; From d034890cab335257ed0b3ddbc7a6b75ee3ae1669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Guilherme=20Vanz?= Date: Wed, 9 Apr 2025 14:52:16 -0300 Subject: [PATCH 4/4] fix: typo in CHANGELOG.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix a typo in the CHANGELOG.md Signed-off-by: José Guilherme Vanz --- opentelemetry-otlp/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-otlp/CHANGELOG.md b/opentelemetry-otlp/CHANGELOG.md index 0a0cf94067..f5d0fe2953 100644 --- a/opentelemetry-otlp/CHANGELOG.md +++ b/opentelemetry-otlp/CHANGELOG.md @@ -3,7 +3,7 @@ ## vNext - Update `tonic` dependency version to 0.13 -- TLS configuration via environment variables for GRPc exporters. +- TLS configuration via environment variables for GRPC exporters. ## 0.29.0