diff --git a/opentelemetry-otlp/CHANGELOG.md b/opentelemetry-otlp/CHANGELOG.md index 541a98219f..bcac2d50ca 100644 --- a/opentelemetry-otlp/CHANGELOG.md +++ b/opentelemetry-otlp/CHANGELOG.md @@ -20,6 +20,16 @@ processors contain only the gRPC status code or HTTP status code. Full details are logged at DEBUG level only. [#3021](https://github.com/open-telemetry/opentelemetry-rust/issues/3021) +- Add support for INSECURE environment variables for gRPC (env-var-only, no builder method, per spec): + `OTEL_EXPORTER_OTLP_INSECURE` (generic), `OTEL_EXPORTER_OTLP_TRACES_INSECURE`, + `OTEL_EXPORTER_OTLP_METRICS_INSECURE`, `OTEL_EXPORTER_OTLP_LOGS_INSECURE`. + Per the spec, these only apply to gRPC connections. When an endpoint has no explicit scheme, + `INSECURE=true` uses `http://`, `INSECURE=false` (default) uses `https://` with auto-TLS. + **Note:** Schemeless endpoints (e.g., `collector.example.com:4317`) now default to `https://` + instead of being passed as-is. Set `OTEL_EXPORTER_OTLP_INSECURE=true` for plaintext connections. + Endpoints with an explicit `http://` or `https://` scheme are unaffected. + [#774](https://github.com/open-telemetry/opentelemetry-rust/issues/774) + [#984](https://github.com/open-telemetry/opentelemetry-rust/issues/984) - Add support for `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` environment variable to configure metrics temporality. Accepted values: `cumulative` (default), `delta`, `lowmemory` (case-insensitive). Programmatic `.with_temporality()` overrides the env var. diff --git a/opentelemetry-otlp/src/exporter/mod.rs b/opentelemetry-otlp/src/exporter/mod.rs index 2c6bde80e8..5610219769 100644 --- a/opentelemetry-otlp/src/exporter/mod.rs +++ b/opentelemetry-otlp/src/exporter/mod.rs @@ -38,6 +38,13 @@ pub const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON: &str = "http/json"; /// Max waiting time for the backend to process each signal batch, defaults to 10 seconds. pub const OTEL_EXPORTER_OTLP_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_TIMEOUT"; + +/// Whether to enable client transport security for the exporter's gRPC connection. +/// Per the spec, this only applies to gRPC. For HTTP, security is determined by the URL scheme. +/// There is intentionally no programmatic builder method — this is env-var-only per the +/// [OTLP exporter spec](https://opentelemetry.io/docs/specs/otel/protocol/exporter/). +/// Default: `false` (TLS is used). +pub const OTEL_EXPORTER_OTLP_INSECURE: &str = "OTEL_EXPORTER_OTLP_INSECURE"; /// Default max waiting time for the backend to process each signal batch. pub const OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT: Duration = Duration::from_millis(10000); @@ -186,6 +193,26 @@ fn resolve_compression_from_env( } } +/// Resolve whether the connection should be insecure (no TLS). +/// +/// Priority: +/// 1. Signal-specific env var (e.g., `OTEL_EXPORTER_OTLP_TRACES_INSECURE`) +/// 2. Generic `OTEL_EXPORTER_OTLP_INSECURE` +/// 3. Default: `false` (secure/TLS) +/// +/// Values: `"true"` (case-insensitive) = insecure, everything else = secure. +/// Per the spec, this only applies to gRPC connections. +#[cfg(feature = "grpc-tonic")] +pub(crate) fn resolve_insecure(signal_insecure_var: &str) -> bool { + let value = std::env::var(signal_insecure_var) + .ok() + .or_else(|| std::env::var(OTEL_EXPORTER_OTLP_INSECURE).ok()); + match value { + Some(val) => val.eq_ignore_ascii_case("true"), + None => false, + } +} + /// Returns the default protocol based on environment variable or enabled features. /// /// Priority order (first available wins): @@ -422,9 +449,11 @@ mod tests { async fn export_builder_error_invalid_grpc_endpoint() { use crate::{LogExporter, WithExportConfig}; + // Use a URI with an explicit scheme but malformed host to ensure it + // fails URI parsing regardless of INSECURE scheme-prepending logic let exporter_result = LogExporter::builder() .with_tonic() - .with_endpoint("invalid_uri/something") + .with_endpoint("http://[invalid") .with_timeout(std::time::Duration::from_secs(10)) .build(); @@ -626,4 +655,83 @@ mod tests { assert_eq!(timeout.as_millis(), 10_000); }); } + + #[cfg(feature = "grpc-tonic")] + #[test] + fn test_resolve_insecure_signal_overrides_generic() { + run_env_test( + vec![ + (crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "true"), + (super::OTEL_EXPORTER_OTLP_INSECURE, "false"), + ], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(insecure); + }, + ); + } + + #[cfg(feature = "grpc-tonic")] + #[test] + fn test_resolve_insecure_default_is_false() { + temp_env::with_vars_unset( + [ + crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, + super::OTEL_EXPORTER_OTLP_INSECURE, + ], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(!insecure); + }, + ); + } + + #[cfg(feature = "grpc-tonic")] + #[test] + fn test_resolve_insecure_case_insensitive() { + run_env_test( + vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "True")], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(insecure); + }, + ); + run_env_test( + vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "TRUE")], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(insecure); + }, + ); + } + + #[cfg(feature = "grpc-tonic")] + #[test] + fn test_resolve_insecure_falls_back_to_generic() { + temp_env::with_var_unset(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, || { + run_env_test(vec![(super::OTEL_EXPORTER_OTLP_INSECURE, "true")], || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(insecure); + }); + }); + } + + #[cfg(feature = "grpc-tonic")] + #[test] + fn test_resolve_insecure_non_true_is_false() { + run_env_test( + vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "false")], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(!insecure); + }, + ); + run_env_test( + vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "invalid")], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(!insecure); + }, + ); + } } diff --git a/opentelemetry-otlp/src/exporter/tonic/mod.rs b/opentelemetry-otlp/src/exporter/tonic/mod.rs index 60336eaf85..8a07bafdd6 100644 --- a/opentelemetry-otlp/src/exporter/tonic/mod.rs +++ b/opentelemetry-otlp/src/exporter/tonic/mod.rs @@ -188,6 +188,7 @@ impl TonicExporterBuilder { signal_timeout_var: &str, signal_compression_var: &str, signal_headers_var: &str, + signal_insecure_var: &str, ) -> Result< ( Channel, @@ -246,12 +247,29 @@ impl TonicExporterBuilder { let config = self.exporter_config; - let endpoint = Self::resolve_endpoint(signal_endpoint_var, config.endpoint); + let mut endpoint_str = Self::resolve_endpoint(signal_endpoint_var, config.endpoint); + let insecure = super::resolve_insecure(signal_insecure_var); + + // Per OTLP spec, INSECURE only applies to schemeless endpoints. + // Endpoints with explicit http:// or https:// are used as-is (case-insensitive). + let has_scheme = endpoint_str + .get(..8) + .is_some_and(|p| p.eq_ignore_ascii_case("https://")) + || endpoint_str + .get(..7) + .is_some_and(|p| p.eq_ignore_ascii_case("http://")); + if !has_scheme { + if insecure { + endpoint_str = format!("http://{endpoint_str}"); + } else { + endpoint_str = format!("https://{endpoint_str}"); + } + } // Used for logging the endpoint - let endpoint_clone = endpoint.clone(); + let endpoint_clone = endpoint_str.clone(); - let endpoint = tonic::transport::Endpoint::from_shared(endpoint) + let endpoint = tonic::transport::Endpoint::from_shared(endpoint_str) .map_err(|op| ExporterBuildError::InvalidUri(endpoint_clone.clone(), op.to_string()))?; let is_https = endpoint @@ -355,6 +373,7 @@ impl TonicExporterBuilder { 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, )?; let client = TonicLogsClient::new(channel, interceptor, compression, retry_policy); @@ -378,6 +397,7 @@ impl TonicExporterBuilder { 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, )?; let client = TonicMetricsClient::new(channel, interceptor, compression, retry_policy); @@ -397,6 +417,7 @@ impl TonicExporterBuilder { 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, )?; let client = TonicTracesClient::new(channel, interceptor, compression, retry_policy); @@ -1009,6 +1030,127 @@ mod tests { assert!(builder.tonic_config.retry_policy.is_none()); } + #[test] + #[cfg(not(any( + feature = "tls", + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + )))] + fn test_schemeless_endpoint_insecure_false_errors_without_tls() { + use crate::exporter::tests::run_env_test; + use crate::exporter::ExporterBuildError; + use crate::SpanExporter; + use crate::WithExportConfig; + + // INSECURE=false (default) + schemeless endpoint → https:// prepended → error (no TLS) + // Unset signal-specific var to ensure generic takes effect + temp_env::with_var_unset(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, || { + run_env_test(vec![(crate::OTEL_EXPORTER_OTLP_INSECURE, "false")], || { + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("collector.example.com:4317") + .build(); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, ExporterBuildError::InvalidConfig { .. }), + "expected InvalidConfig error for schemeless+secure without TLS, got: {err:?}" + ); + }); + }); + } + + #[test] + #[cfg(not(any( + feature = "tls", + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + )))] + fn test_schemeless_endpoint_insecure_true_succeeds_without_tls() { + use crate::exporter::tests::run_env_test; + use crate::SpanExporter; + use crate::WithExportConfig; + + // INSECURE=true + schemeless endpoint → http:// prepended → no TLS needed + // Unset signal-specific var to ensure generic takes effect + temp_env::with_var_unset(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, || { + run_env_test(vec![(crate::OTEL_EXPORTER_OTLP_INSECURE, "true")], || { + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("collector.example.com:4317") + .build(); + + assert!( + result.is_ok(), + "schemeless + INSECURE=true should succeed without TLS, got: {:?}", + result.unwrap_err() + ); + }); + }); + } + + #[test] + #[cfg(not(any( + feature = "tls", + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + )))] + fn test_schemeless_endpoint_defaults_to_https() { + use crate::exporter::ExporterBuildError; + use crate::SpanExporter; + use crate::WithExportConfig; + + // No INSECURE env var set → defaults to secure (https://) → error without TLS + // This verifies the behavioral change: schemeless endpoints now get https:// prepended + temp_env::with_vars_unset( + [ + crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, + crate::OTEL_EXPORTER_OTLP_INSECURE, + ], + || { + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("collector.example.com:4317") + .build(); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, ExporterBuildError::InvalidConfig { .. }), + "schemeless endpoint should default to https:// and fail without TLS, got: {err:?}" + ); + }, + ); + } + + #[tokio::test] + async fn test_explicit_http_scheme_ignores_insecure_env() { + use crate::exporter::tests::run_env_test; + use crate::SpanExporter; + use crate::WithExportConfig; + + // Explicit http:// scheme should succeed regardless of INSECURE value + // Unset signal-specific var to ensure generic takes effect + temp_env::with_var_unset(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, || { + run_env_test(vec![(crate::OTEL_EXPORTER_OTLP_INSECURE, "false")], || { + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("http://collector.example.com:4317") + .build(); + + assert!( + result.is_ok(), + "explicit http:// should succeed even with INSECURE=false, got: {:?}", + result.unwrap_err() + ); + }); + }); + } + #[test] #[cfg(not(any( feature = "tls", diff --git a/opentelemetry-otlp/src/lib.rs b/opentelemetry-otlp/src/lib.rs index 1ebfedf79d..766109659e 100644 --- a/opentelemetry-otlp/src/lib.rs +++ b/opentelemetry-otlp/src/lib.rs @@ -224,6 +224,7 @@ //! | `OTEL_EXPORTER_OTLP_TIMEOUT` | Maximum wait time (in milliseconds) for the backend to process each batch. | `10000` | //! | `OTEL_EXPORTER_OTLP_HEADERS` | Key-value pairs for request headers. Format: `key1=value1,key2=value2`. Values are URL-decoded. | (none) | //! | `OTEL_EXPORTER_OTLP_COMPRESSION` | Compression algorithm. Valid values: `gzip`, `zstd`. | (none) | +//! | `OTEL_EXPORTER_OTLP_INSECURE` | Whether to disable TLS for gRPC connections. Only applies to gRPC; HTTP security is determined by URL scheme. Valid values: `true`, `false` (case-insensitive). | `false` | //! //! ## Traces //! @@ -233,6 +234,7 @@ //! | `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` | Signal-specific timeout (in milliseconds) for trace exports. | //! | `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | Signal-specific headers for trace exports. | //! | `OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` | Signal-specific compression for trace exports. | +//! | `OTEL_EXPORTER_OTLP_TRACES_INSECURE` | Signal-specific insecure flag for gRPC trace exports. | //! //! ## Metrics //! @@ -242,6 +244,7 @@ //! | `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` | Signal-specific timeout (in milliseconds) for metrics exports. | //! | `OTEL_EXPORTER_OTLP_METRICS_HEADERS` | Signal-specific headers for metrics exports. | //! | `OTEL_EXPORTER_OTLP_METRICS_COMPRESSION` | Signal-specific compression for metrics exports. | +//! | `OTEL_EXPORTER_OTLP_METRICS_INSECURE` | Signal-specific insecure flag for gRPC metrics exports. | //! | `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` | Temporality preference for metrics. Valid values: `cumulative`, `delta`, `lowmemory` (case-insensitive). | `cumulative` | //! //! ## Logs @@ -252,6 +255,7 @@ //! | `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` | Signal-specific timeout (in milliseconds) for log exports. | //! | `OTEL_EXPORTER_OTLP_LOGS_HEADERS` | Signal-specific headers for log exports. | //! | `OTEL_EXPORTER_OTLP_LOGS_COMPRESSION` | Signal-specific compression for log exports. | +//! | `OTEL_EXPORTER_OTLP_LOGS_INSECURE` | Signal-specific insecure flag for gRPC log exports. | //! //! # Feature Flags //! The following feature flags can enable exporters for different telemetry signals: @@ -652,7 +656,7 @@ pub use crate::exporter::ExporterBuildError; pub use crate::span::{ SpanExporter, SpanExporterBuilder, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + OTEL_EXPORTER_OTLP_TRACES_INSECURE, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, }; #[cfg(feature = "metrics")] @@ -660,7 +664,8 @@ pub use crate::span::{ pub use crate::metric::{ MetricExporter, MetricExporterBuilder, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + OTEL_EXPORTER_OTLP_METRICS_INSECURE, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, }; #[cfg(feature = "logs")] @@ -668,7 +673,7 @@ pub use crate::metric::{ pub use crate::logs::{ LogExporter, LogExporterBuilder, OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + OTEL_EXPORTER_OTLP_LOGS_INSECURE, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, }; #[cfg(any(feature = "http-proto", feature = "http-json"))] @@ -679,10 +684,10 @@ pub use crate::exporter::tonic::WithTonicConfig; pub use crate::exporter::{ WithExportConfig, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_PROTOCOL, - OTEL_EXPORTER_OTLP_PROTOCOL_GRPC, OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON, - OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF, OTEL_EXPORTER_OTLP_TIMEOUT, - OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT, + OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_INSECURE, + OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_PROTOCOL_GRPC, + OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON, OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF, + OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT, }; #[cfg(any( diff --git a/opentelemetry-otlp/src/logs.rs b/opentelemetry-otlp/src/logs.rs index f94478214a..64dbbbd884 100644 --- a/opentelemetry-otlp/src/logs.rs +++ b/opentelemetry-otlp/src/logs.rs @@ -30,6 +30,9 @@ 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"; +/// Whether to enable client transport security for gRPC log exports. +/// Only applies to gRPC; HTTP security is determined by URL scheme. +pub const OTEL_EXPORTER_OTLP_LOGS_INSECURE: &str = "OTEL_EXPORTER_OTLP_LOGS_INSECURE"; /// Builder for creating a new [LogExporter]. #[derive(Debug, Default, Clone)] diff --git a/opentelemetry-otlp/src/metric.rs b/opentelemetry-otlp/src/metric.rs index 483bbc32d2..44f6177d96 100644 --- a/opentelemetry-otlp/src/metric.rs +++ b/opentelemetry-otlp/src/metric.rs @@ -42,6 +42,9 @@ 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"; +/// Whether to enable client transport security for gRPC metrics exports. +/// Only applies to gRPC; HTTP security is determined by URL scheme. +pub const OTEL_EXPORTER_OTLP_METRICS_INSECURE: &str = "OTEL_EXPORTER_OTLP_METRICS_INSECURE"; /// Temporality preference for metrics, defaults to cumulative. pub const OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: &str = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"; diff --git a/opentelemetry-otlp/src/span.rs b/opentelemetry-otlp/src/span.rs index a6b07993d7..55fdff74dc 100644 --- a/opentelemetry-otlp/src/span.rs +++ b/opentelemetry-otlp/src/span.rs @@ -35,6 +35,9 @@ 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"; +/// Whether to enable client transport security for gRPC trace exports. +/// Only applies to gRPC; HTTP security is determined by URL scheme. +pub const OTEL_EXPORTER_OTLP_TRACES_INSECURE: &str = "OTEL_EXPORTER_OTLP_TRACES_INSECURE"; /// OTLP span exporter builder #[derive(Debug, Default, Clone)]