Skip to content

Commit a0a9305

Browse files
bryantbiggsscottgerringcijothomas
authored
feat(otlp): support INSECURE environment variables for gRPC (#3365)
Co-authored-by: Scott Gerring <scottgerring@users.noreply.github.com> Co-authored-by: Cijo Thomas <cijo.thomas@gmail.com>
1 parent 3e12c94 commit a0a9305

12 files changed

Lines changed: 421 additions & 31 deletions

File tree

opentelemetry-otlp/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
## vNext
44

5+
- Add support for INSECURE environment variables for gRPC (env-var-only, no builder method, per spec):
6+
`OTEL_EXPORTER_OTLP_INSECURE` (generic), `OTEL_EXPORTER_OTLP_TRACES_INSECURE`,
7+
`OTEL_EXPORTER_OTLP_METRICS_INSECURE`, `OTEL_EXPORTER_OTLP_LOGS_INSECURE`.
8+
Per the spec, these only apply to gRPC connections. When an endpoint has no explicit scheme,
9+
`INSECURE=true` uses `http://`, `INSECURE=false` (default) uses `https://` with auto-TLS.
10+
**Breaking:** Schemeless endpoints (e.g., `collector.example.com:4317`) now default to `https://`
11+
instead of being passed as-is. Set `OTEL_EXPORTER_OTLP_INSECURE=true` for plaintext connections.
12+
Endpoints with an explicit scheme (e.g., `http://`, `https://`, `unix://`) are unaffected.
13+
[#774](https://github.com/open-telemetry/opentelemetry-rust/issues/774)
14+
[#984](https://github.com/open-telemetry/opentelemetry-rust/issues/984)
15+
516
## 0.32.0
617

718
Released 2026-May-08

opentelemetry-otlp/src/exporter/http/logs.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ fn handle_partial_success(response_body: &[u8], protocol: Protocol) {
8181
mod tests {
8282
use super::*;
8383

84+
#[cfg(feature = "http-proto")]
8485
#[test]
8586
fn test_handle_invalid_protobuf() {
8687
// Corrupted/invalid protobuf data
@@ -90,6 +91,7 @@ mod tests {
9091
handle_partial_success(&invalid, Protocol::HttpBinary);
9192
}
9293

94+
#[cfg(feature = "http-proto")]
9395
#[test]
9496
fn test_handle_empty_response() {
9597
let empty = vec![];

opentelemetry-otlp/src/exporter/http/metrics.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ fn handle_partial_success(response_body: &[u8], protocol: Protocol) {
7777
mod tests {
7878
use super::*;
7979

80+
#[cfg(feature = "http-proto")]
8081
#[test]
8182
fn test_handle_invalid_protobuf() {
8283
// Corrupted/invalid protobuf data
@@ -86,6 +87,7 @@ mod tests {
8687
handle_partial_success(&invalid, Protocol::HttpBinary);
8788
}
8889

90+
#[cfg(feature = "http-proto")]
8991
#[test]
9092
fn test_handle_empty_response() {
9193
let empty = vec![];

opentelemetry-otlp/src/exporter/http/mod.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,7 @@ mod tests {
11311131
use opentelemetry_http::{Bytes, HttpClient};
11321132
use std::io::Read;
11331133

1134+
#[cfg(feature = "http-proto")]
11341135
#[test]
11351136
fn test_gzip_compression_and_decompression() {
11361137
let client = OtlpHttpClient::new(
@@ -1163,7 +1164,7 @@ mod tests {
11631164
assert_ne!(compressed_body, test_data.to_vec());
11641165
}
11651166

1166-
#[cfg(feature = "zstd-http")]
1167+
#[cfg(all(feature = "http-proto", feature = "zstd-http"))]
11671168
#[test]
11681169
fn test_zstd_compression_and_decompression() {
11691170
let client = OtlpHttpClient::new(
@@ -1194,6 +1195,7 @@ mod tests {
11941195
assert_ne!(compressed_body, test_data.to_vec());
11951196
}
11961197

1198+
#[cfg(feature = "http-proto")]
11971199
#[test]
11981200
fn test_no_compression_when_disabled() {
11991201
let client = OtlpHttpClient::new(
@@ -1216,7 +1218,7 @@ mod tests {
12161218
assert_eq!(content_encoding, None);
12171219
}
12181220

1219-
#[cfg(not(feature = "gzip-http"))]
1221+
#[cfg(all(feature = "http-proto", not(feature = "gzip-http")))]
12201222
#[test]
12211223
fn test_gzip_error_when_feature_disabled() {
12221224
let client = OtlpHttpClient::new(
@@ -1240,7 +1242,7 @@ mod tests {
12401242
.contains("gzip-http feature not enabled"));
12411243
}
12421244

1243-
#[cfg(not(feature = "zstd-http"))]
1245+
#[cfg(all(feature = "http-proto", not(feature = "zstd-http")))]
12441246
#[test]
12451247
fn test_zstd_error_when_feature_disabled() {
12461248
let client = OtlpHttpClient::new(
@@ -1352,7 +1354,7 @@ mod tests {
13521354
}
13531355
}
13541356

1355-
#[cfg(feature = "trace")]
1357+
#[cfg(all(feature = "trace", feature = "http-proto"))]
13561358
#[test]
13571359
fn test_build_trace_export_body_binary_protocol() {
13581360
let client = create_test_client(crate::Protocol::HttpBinary, None);
@@ -1378,7 +1380,7 @@ mod tests {
13781380
assert_eq!(content_encoding, None);
13791381
}
13801382

1381-
#[cfg(all(feature = "trace", feature = "gzip-http"))]
1383+
#[cfg(all(feature = "http-proto", feature = "trace", feature = "gzip-http"))]
13821384
#[test]
13831385
fn test_build_trace_export_body_with_compression() {
13841386
let client =
@@ -1400,7 +1402,7 @@ mod tests {
14001402
LogBatch::new(&[])
14011403
}
14021404

1403-
#[cfg(feature = "logs")]
1405+
#[cfg(all(feature = "http-proto", feature = "logs"))]
14041406
#[test]
14051407
fn test_build_logs_export_body_binary_protocol() {
14061408
let client = create_test_client(crate::Protocol::HttpBinary, None);
@@ -1426,7 +1428,7 @@ mod tests {
14261428
assert_eq!(content_encoding, None);
14271429
}
14281430

1429-
#[cfg(all(feature = "logs", feature = "gzip-http"))]
1431+
#[cfg(all(feature = "http-proto", feature = "logs", feature = "gzip-http"))]
14301432
#[test]
14311433
fn test_build_logs_export_body_with_compression() {
14321434
let client =
@@ -1440,7 +1442,7 @@ mod tests {
14401442
assert_eq!(content_encoding, Some("gzip"));
14411443
}
14421444

1443-
#[cfg(feature = "metrics")]
1445+
#[cfg(all(feature = "http-proto", feature = "metrics"))]
14441446
#[test]
14451447
fn test_build_metrics_export_body_binary_protocol() {
14461448
use opentelemetry_sdk::metrics::data::ResourceMetrics;
@@ -1470,7 +1472,7 @@ mod tests {
14701472
assert_eq!(content_encoding, None);
14711473
}
14721474

1473-
#[cfg(all(feature = "metrics", feature = "gzip-http"))]
1475+
#[cfg(all(feature = "http-proto", feature = "metrics", feature = "gzip-http"))]
14741476
#[test]
14751477
fn test_build_metrics_export_body_with_compression() {
14761478
use opentelemetry_sdk::metrics::data::ResourceMetrics;
@@ -1486,7 +1488,11 @@ mod tests {
14861488
assert_eq!(content_encoding, Some("gzip"));
14871489
}
14881490

1489-
#[cfg(all(feature = "metrics", not(feature = "gzip-http")))]
1491+
#[cfg(all(
1492+
feature = "http-proto",
1493+
feature = "metrics",
1494+
not(feature = "gzip-http")
1495+
))]
14901496
#[test]
14911497
fn test_build_metrics_export_body_compression_error_returns_none() {
14921498
use opentelemetry_sdk::metrics::data::ResourceMetrics;
@@ -1574,7 +1580,7 @@ mod tests {
15741580
assert_eq!(retry_policy.jitter_ms, 50);
15751581
}
15761582

1577-
#[cfg(feature = "experimental-http-retry")]
1583+
#[cfg(all(feature = "http-proto", feature = "experimental-http-retry"))]
15781584
#[test]
15791585
fn test_default_retry_policy_when_none_configured() {
15801586
let client = create_test_client(crate::Protocol::HttpBinary, None);
@@ -1586,7 +1592,7 @@ mod tests {
15861592
assert_eq!(client.retry_policy.jitter_ms, 100);
15871593
}
15881594

1589-
#[cfg(feature = "experimental-http-retry")]
1595+
#[cfg(all(feature = "http-proto", feature = "experimental-http-retry"))]
15901596
#[test]
15911597
fn test_custom_retry_policy_used() {
15921598
use crate::retry::RetryPolicy;

opentelemetry-otlp/src/exporter/http/trace.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ fn handle_partial_success(response_body: &[u8], protocol: Protocol) {
8282
mod tests {
8383
use super::*;
8484

85+
#[cfg(feature = "http-proto")]
8586
#[test]
8687
fn test_handle_invalid_protobuf() {
8788
// Corrupted/invalid protobuf data
@@ -91,6 +92,7 @@ mod tests {
9192
handle_partial_success(&invalid, Protocol::HttpBinary);
9293
}
9394

95+
#[cfg(feature = "http-proto")]
9496
#[test]
9597
fn test_handle_empty_response() {
9698
let empty = vec![];

opentelemetry-otlp/src/exporter/mod.rs

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ pub const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON: &str = "http/json";
3838

3939
/// Max waiting time for the backend to process each signal batch, defaults to 10 seconds.
4040
pub const OTEL_EXPORTER_OTLP_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_TIMEOUT";
41+
42+
/// Whether to disable TLS for the exporter's gRPC connection.
43+
/// Per the [OTLP exporter spec](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options),
44+
/// this only applies to gRPC endpoints that have no explicit scheme; an endpoint
45+
/// with an explicit scheme is used as-is. HTTP security is determined by the
46+
/// endpoint URL scheme.
47+
/// There is intentionally no programmatic builder method — this is env-var-only.
48+
/// Default: `false` (TLS is used).
49+
pub const OTEL_EXPORTER_OTLP_INSECURE: &str = "OTEL_EXPORTER_OTLP_INSECURE";
4150
/// Default max waiting time for the backend to process each signal batch.
4251
pub const OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT: Duration = Duration::from_millis(10000);
4352

@@ -195,6 +204,26 @@ fn resolve_compression_from_env(
195204
}
196205
}
197206

207+
/// Resolve whether the connection should be insecure (no TLS).
208+
///
209+
/// Priority:
210+
/// 1. Signal-specific env var (e.g., `OTEL_EXPORTER_OTLP_TRACES_INSECURE`)
211+
/// 2. Generic `OTEL_EXPORTER_OTLP_INSECURE`
212+
/// 3. Default: `false` (secure/TLS)
213+
///
214+
/// Values: `"true"` (case-insensitive) = insecure, everything else = secure.
215+
/// Per the spec, this only applies to gRPC connections.
216+
#[cfg(feature = "grpc-tonic")]
217+
pub(crate) fn resolve_insecure(signal_insecure_var: &str) -> bool {
218+
let value = std::env::var(signal_insecure_var)
219+
.ok()
220+
.or_else(|| std::env::var(OTEL_EXPORTER_OTLP_INSECURE).ok());
221+
match value {
222+
Some(val) => val.eq_ignore_ascii_case("true"),
223+
None => false,
224+
}
225+
}
226+
198227
/// Returns the default protocol based on enabled features.
199228
///
200229
/// Note: This does not consult environment variables. Protocol resolution
@@ -417,9 +446,11 @@ mod tests {
417446
async fn export_builder_error_invalid_grpc_endpoint() {
418447
use crate::{LogExporter, WithExportConfig};
419448

449+
// Use a URI with an explicit scheme but malformed host to ensure it
450+
// fails URI parsing regardless of INSECURE scheme-prepending logic
420451
let exporter_result = LogExporter::builder()
421452
.with_tonic()
422-
.with_endpoint("invalid_uri/something")
453+
.with_endpoint("http://[invalid")
423454
.with_timeout(std::time::Duration::from_secs(10))
424455
.build();
425456

@@ -460,7 +491,7 @@ mod tests {
460491
not(any(feature = "http-proto", feature = "http-json"))
461492
))]
462493
{
463-
assert_eq!(crate::exporter::default_protocol(), crate::Protocol::Grpc);
494+
assert_eq!(crate::Protocol::default(), crate::Protocol::Grpc);
464495
}
465496
}
466497

@@ -676,6 +707,21 @@ mod tests {
676707
);
677708
}
678709

710+
#[cfg(all(feature = "grpc-tonic", feature = "trace"))]
711+
#[test]
712+
fn test_resolve_insecure_signal_overrides_generic() {
713+
run_env_test(
714+
vec![
715+
(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "true"),
716+
(super::OTEL_EXPORTER_OTLP_INSECURE, "false"),
717+
],
718+
|| {
719+
let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE);
720+
assert!(insecure);
721+
},
722+
);
723+
}
724+
679725
#[cfg(feature = "http-proto")]
680726
#[test]
681727
fn test_resolve_protocol_code_overrides_all_envs() {
@@ -696,6 +742,21 @@ mod tests {
696742
);
697743
}
698744

745+
#[cfg(all(feature = "grpc-tonic", feature = "trace"))]
746+
#[test]
747+
fn test_resolve_insecure_default_is_false() {
748+
temp_env::with_vars_unset(
749+
[
750+
crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE,
751+
super::OTEL_EXPORTER_OTLP_INSECURE,
752+
],
753+
|| {
754+
let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE);
755+
assert!(!insecure);
756+
},
757+
);
758+
}
759+
699760
#[cfg(all(feature = "grpc-tonic", feature = "http-proto"))]
700761
#[test]
701762
fn test_resolve_protocol_falls_back_to_generic_env() {
@@ -716,4 +777,53 @@ mod tests {
716777
assert_eq!(protocol, Protocol::feature_default());
717778
});
718779
}
780+
781+
#[cfg(all(feature = "grpc-tonic", feature = "trace"))]
782+
#[test]
783+
fn test_resolve_insecure_case_insensitive() {
784+
run_env_test(
785+
vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "True")],
786+
|| {
787+
let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE);
788+
assert!(insecure);
789+
},
790+
);
791+
run_env_test(
792+
vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "TRUE")],
793+
|| {
794+
let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE);
795+
assert!(insecure);
796+
},
797+
);
798+
}
799+
800+
#[cfg(all(feature = "grpc-tonic", feature = "trace"))]
801+
#[test]
802+
fn test_resolve_insecure_falls_back_to_generic() {
803+
temp_env::with_var_unset(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, || {
804+
run_env_test(vec![(super::OTEL_EXPORTER_OTLP_INSECURE, "true")], || {
805+
let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE);
806+
assert!(insecure);
807+
});
808+
});
809+
}
810+
811+
#[cfg(all(feature = "grpc-tonic", feature = "trace"))]
812+
#[test]
813+
fn test_resolve_insecure_non_true_is_false() {
814+
run_env_test(
815+
vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "false")],
816+
|| {
817+
let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE);
818+
assert!(!insecure);
819+
},
820+
);
821+
run_env_test(
822+
vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "invalid")],
823+
|| {
824+
let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE);
825+
assert!(!insecure);
826+
},
827+
);
828+
}
719829
}

0 commit comments

Comments
 (0)