Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions opentelemetry-otlp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
110 changes: 109 additions & 1 deletion opentelemetry-otlp/src/exporter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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);
},
);
}
}
148 changes: 145 additions & 3 deletions opentelemetry-otlp/src/exporter/tonic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ impl TonicExporterBuilder {
signal_timeout_var: &str,
signal_compression_var: &str,
signal_headers_var: &str,
signal_insecure_var: &str,
) -> Result<
(
Channel,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading