Skip to content

Commit 5be79c7

Browse files
gruebellalitbcijothomas
authored
refactor: prioritize code-based config over env vars in OTLP exporter (#2827)
Co-authored-by: Lalit Kumar Bhasin <[email protected]> Co-authored-by: Cijo Thomas <[email protected]> Co-authored-by: Cijo Thomas <[email protected]>
1 parent 06abe3d commit 5be79c7

File tree

4 files changed

+236
-109
lines changed

4 files changed

+236
-109
lines changed

opentelemetry-otlp/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
`Error` which contained many variants unrelated to building an exporter, the
1818
new one returns specific variants applicable to building an exporter. Some
1919
variants might be applicable only on select features.
20+
- **Breaking** `ExportConfig`'s `timeout` field is now optional(`Option<Duration>`)
21+
- **Breaking** Export configuration done via code is final. ENV variables cannot be used to override the code config.
22+
Do not use code based config, if there is desire to control the settings via ENV variables.
23+
List of ENV variables and corresponding setting being affected by this change.
24+
- `OTEL_EXPORTER_OTLP_ENDPOINT` -> `ExportConfig.endpoint`
25+
- `OTEL_EXPORTER_OTLP_TIMEOUT` -> `ExportConfig.timeout`
2026

2127
## 0.28.0
2228

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

Lines changed: 50 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
use super::{
2-
default_headers, default_protocol, parse_header_string, ExporterBuildError,
2+
default_headers, default_protocol, parse_header_string, resolve_timeout, ExporterBuildError,
33
OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT,
44
};
5-
use crate::{
6-
ExportConfig, Protocol, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS,
7-
OTEL_EXPORTER_OTLP_TIMEOUT,
8-
};
5+
use crate::{ExportConfig, Protocol, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS};
96
use http::{HeaderName, HeaderValue, Uri};
107
use opentelemetry_http::HttpClient;
118
use opentelemetry_proto::transform::common::tonic::ResourceAttributesWithSchema;
@@ -112,19 +109,10 @@ impl HttpExporterBuilder {
112109
let endpoint = resolve_http_endpoint(
113110
signal_endpoint_var,
114111
signal_endpoint_path,
115-
self.exporter_config.endpoint.clone(),
112+
self.exporter_config.endpoint.as_deref(),
116113
)?;
117114

118-
let timeout = match env::var(signal_timeout_var)
119-
.ok()
120-
.or(env::var(OTEL_EXPORTER_OTLP_TIMEOUT).ok())
121-
{
122-
Some(val) => match val.parse() {
123-
Ok(milli_seconds) => Duration::from_millis(milli_seconds),
124-
Err(_) => self.exporter_config.timeout,
125-
},
126-
None => self.exporter_config.timeout,
127-
};
115+
let timeout = resolve_timeout(signal_timeout_var, self.exporter_config.timeout.as_ref());
128116

129117
#[allow(unused_mut)] // TODO - clippy thinks mut is not needed, but it is
130118
let mut http_client = self.http_config.client.take();
@@ -371,30 +359,27 @@ fn build_endpoint_uri(endpoint: &str, path: &str) -> Result<Uri, ExporterBuildEr
371359
fn resolve_http_endpoint(
372360
signal_endpoint_var: &str,
373361
signal_endpoint_path: &str,
374-
provided_endpoint: Option<String>,
362+
provided_endpoint: Option<&str>,
375363
) -> Result<Uri, ExporterBuildError> {
376-
// per signal env var is not modified
377-
if let Some(endpoint) = env::var(signal_endpoint_var)
364+
// programmatic configuration overrides any value set via environment variables
365+
if let Some(provider_endpoint) = provided_endpoint {
366+
provider_endpoint
367+
.parse()
368+
.map_err(|er: http::uri::InvalidUri| {
369+
ExporterBuildError::InvalidUri(provider_endpoint.to_string(), er.to_string())
370+
})
371+
} else if let Some(endpoint) = env::var(signal_endpoint_var)
378372
.ok()
379373
.and_then(|s| s.parse().ok())
380374
{
381-
return Ok(endpoint);
382-
}
383-
384-
// if signal env var is not set, then we check if the OTEL_EXPORTER_OTLP_ENDPOINT is set
385-
if let Some(endpoint) = env::var(OTEL_EXPORTER_OTLP_ENDPOINT)
375+
// per signal env var is not modified
376+
Ok(endpoint)
377+
} else if let Some(endpoint) = env::var(OTEL_EXPORTER_OTLP_ENDPOINT)
386378
.ok()
387379
.and_then(|s| build_endpoint_uri(&s, signal_endpoint_path).ok())
388380
{
389-
return Ok(endpoint);
390-
}
391-
392-
if let Some(provider_endpoint) = provided_endpoint {
393-
provider_endpoint
394-
.parse()
395-
.map_err(|er: http::uri::InvalidUri| {
396-
ExporterBuildError::InvalidUri(provider_endpoint, er.to_string())
397-
})
381+
// if signal env var is not set, then we check if the OTEL_EXPORTER_OTLP_ENDPOINT env var is set
382+
Ok(endpoint)
398383
} else {
399384
build_endpoint_uri(
400385
OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT,
@@ -481,12 +466,9 @@ mod tests {
481466
run_env_test(
482467
vec![(OTEL_EXPORTER_OTLP_ENDPOINT, "http://example.com")],
483468
|| {
484-
let endpoint = resolve_http_endpoint(
485-
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
486-
"/v1/traces",
487-
Some("http://localhost:4317".to_string()),
488-
)
489-
.unwrap();
469+
let endpoint =
470+
resolve_http_endpoint(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "/v1/traces", None)
471+
.unwrap();
490472
assert_eq!(endpoint, "http://example.com/v1/traces");
491473
},
492474
)
@@ -496,20 +478,36 @@ mod tests {
496478
fn test_not_append_signal_path_to_signal_env() {
497479
run_env_test(
498480
vec![(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "http://example.com")],
481+
|| {
482+
let endpoint =
483+
resolve_http_endpoint(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "/v1/traces", None)
484+
.unwrap();
485+
assert_eq!(endpoint, "http://example.com");
486+
},
487+
)
488+
}
489+
490+
#[test]
491+
fn test_priority_of_signal_env_over_generic_env() {
492+
run_env_test(
493+
vec![
494+
(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "http://example.com"),
495+
(OTEL_EXPORTER_OTLP_ENDPOINT, "http://wrong.com"),
496+
],
499497
|| {
500498
let endpoint = super::resolve_http_endpoint(
501499
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
502500
"/v1/traces",
503-
Some("http://localhost:4317".to_string()),
501+
None,
504502
)
505503
.unwrap();
506504
assert_eq!(endpoint, "http://example.com");
507505
},
508-
)
506+
);
509507
}
510508

511509
#[test]
512-
fn test_priority_of_signal_env_over_generic_env() {
510+
fn test_priority_of_code_based_config_over_envs() {
513511
run_env_test(
514512
vec![
515513
(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "http://example.com"),
@@ -519,24 +517,20 @@ mod tests {
519517
let endpoint = super::resolve_http_endpoint(
520518
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
521519
"/v1/traces",
522-
Some("http://localhost:4317".to_string()),
520+
Some("http://localhost:4317"),
523521
)
524522
.unwrap();
525-
assert_eq!(endpoint, "http://example.com");
523+
assert_eq!(endpoint, "http://localhost:4317");
526524
},
527525
);
528526
}
529527

530528
#[test]
531-
fn test_use_provided_or_default_when_others_missing() {
529+
fn test_use_default_when_others_missing() {
532530
run_env_test(vec![], || {
533-
let endpoint = super::resolve_http_endpoint(
534-
"NON_EXISTENT_VAR",
535-
"/v1/traces",
536-
Some("http://localhost:4317".to_string()),
537-
)
538-
.unwrap();
539-
assert_eq!(endpoint, "http://localhost:4317/");
531+
let endpoint =
532+
super::resolve_http_endpoint("NON_EXISTENT_VAR", "/v1/traces", None).unwrap();
533+
assert_eq!(endpoint, "http://localhost:4318/v1/traces");
540534
});
541535
}
542536

@@ -568,7 +562,7 @@ mod tests {
568562
let endpoint = super::resolve_http_endpoint(
569563
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
570564
"/v1/traces",
571-
Some("http://localhost:4317".to_string()),
565+
None,
572566
)
573567
.unwrap();
574568
assert_eq!(endpoint, "http://example.com/v1/traces");
@@ -582,7 +576,7 @@ mod tests {
582576
let result = super::resolve_http_endpoint(
583577
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
584578
"/v1/traces",
585-
Some("-*/*-/*-//-/-/yet-another-invalid-uri".to_string()),
579+
Some("-*/*-/*-//-/-/yet-another-invalid-uri"),
586580
);
587581
assert!(result.is_err());
588582
// You may also want to assert on the specific error type if applicable
@@ -722,7 +716,7 @@ mod tests {
722716
let url = resolve_http_endpoint(
723717
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
724718
"/v1/traces",
725-
exporter.exporter_config.endpoint,
719+
exporter.exporter_config.endpoint.as_deref(),
726720
)
727721
.unwrap();
728722

@@ -737,7 +731,7 @@ mod tests {
737731
let url = resolve_http_endpoint(
738732
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
739733
"/v1/traces",
740-
exporter.exporter_config.endpoint,
734+
exporter.exporter_config.endpoint.as_deref(),
741735
)
742736
.unwrap();
743737

opentelemetry-otlp/src/exporter/mod.rs

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,27 +68,32 @@ pub(crate) mod tonic;
6868
/// Configuration for the OTLP exporter.
6969
#[derive(Debug)]
7070
pub struct ExportConfig {
71-
/// The address of the OTLP collector. If it's not provided via builder or environment variables.
71+
/// The address of the OTLP collector.
7272
/// Default address will be used based on the protocol.
73+
///
74+
/// Note: Programmatically setting this will override any value set via the environment variable.
7375
pub endpoint: Option<String>,
7476

7577
/// The protocol to use when communicating with the collector.
7678
pub protocol: Protocol,
7779

7880
/// The timeout to the collector.
79-
pub timeout: Duration,
81+
/// The default value is 10 seconds.
82+
///
83+
/// Note: Programmatically setting this will override any value set via the environment variable.
84+
pub timeout: Option<Duration>,
8085
}
8186

8287
impl Default for ExportConfig {
8388
fn default() -> Self {
8489
let protocol = default_protocol();
8590

86-
ExportConfig {
91+
Self {
8792
endpoint: None,
8893
// don't use default_endpoint(protocol) here otherwise we
8994
// won't know if user provided a value
9095
protocol,
91-
timeout: OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT,
96+
timeout: None,
9297
}
9398
}
9499
}
@@ -223,18 +228,24 @@ impl HasExportConfig for HttpExporterBuilder {
223228
/// ```
224229
pub trait WithExportConfig {
225230
/// Set the address of the OTLP collector. If not set or set to empty string, the default address is used.
231+
///
232+
/// Note: Programmatically setting this will override any value set via the environment variable.
226233
fn with_endpoint<T: Into<String>>(self, endpoint: T) -> Self;
227234
/// Set the protocol to use when communicating with the collector.
228235
///
229236
/// Note that protocols that are not supported by exporters will be ignored. The exporter
230237
/// will use default protocol in this case.
231238
///
232239
/// ## Note
233-
/// All exporters in this crate only support one protocol, thus choosing the protocol is an no-op at the moment.
240+
/// All exporters in this crate only support one protocol, thus choosing the protocol is a no-op at the moment.
234241
fn with_protocol(self, protocol: Protocol) -> Self;
235242
/// Set the timeout to the collector.
243+
///
244+
/// Note: Programmatically setting this will override any value set via the environment variable.
236245
fn with_timeout(self, timeout: Duration) -> Self;
237-
/// Set export config. This will override all previous configuration.
246+
/// Set export config. This will override all previous configurations.
247+
///
248+
/// Note: Programmatically setting this will override any value set via environment variables.
238249
fn with_export_config(self, export_config: ExportConfig) -> Self;
239250
}
240251

@@ -250,7 +261,7 @@ impl<B: HasExportConfig> WithExportConfig for B {
250261
}
251262

252263
fn with_timeout(mut self, timeout: Duration) -> Self {
253-
self.export_config().timeout = timeout;
264+
self.export_config().timeout = Some(timeout);
254265
self
255266
}
256267

@@ -262,6 +273,28 @@ impl<B: HasExportConfig> WithExportConfig for B {
262273
}
263274
}
264275

276+
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
277+
fn resolve_timeout(signal_timeout_var: &str, provided_timeout: Option<&Duration>) -> Duration {
278+
// programmatic configuration overrides any value set via environment variables
279+
if let Some(timeout) = provided_timeout {
280+
*timeout
281+
} else if let Some(timeout) = std::env::var(signal_timeout_var)
282+
.ok()
283+
.and_then(|s| s.parse().ok())
284+
{
285+
// per signal env var is not modified
286+
Duration::from_millis(timeout)
287+
} else if let Some(timeout) = std::env::var(OTEL_EXPORTER_OTLP_TIMEOUT)
288+
.ok()
289+
.and_then(|s| s.parse().ok())
290+
{
291+
// if signal env var is not set, then we check if the OTEL_EXPORTER_OTLP_TIMEOUT env var is set
292+
Duration::from_millis(timeout)
293+
} else {
294+
OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT
295+
}
296+
}
297+
265298
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
266299
fn parse_header_string(value: &str) -> impl Iterator<Item = (&str, String)> {
267300
value
@@ -342,7 +375,6 @@ mod tests {
342375
#[cfg(feature = "logs")]
343376
#[cfg(any(feature = "http-proto", feature = "http-json"))]
344377
#[test]
345-
#[ignore = "Unstable due to interference from env variable tests. Re-enable after https://github.com/open-telemetry/opentelemetry-rust/issues/2818 is resolved."]
346378
fn export_builder_error_invalid_http_endpoint() {
347379
use std::time::Duration;
348380

@@ -351,7 +383,7 @@ mod tests {
351383
let ex_config = ExportConfig {
352384
endpoint: Some("invalid_uri/something".to_string()),
353385
protocol: Protocol::HttpBinary,
354-
timeout: Duration::from_secs(10),
386+
timeout: Some(Duration::from_secs(10)),
355387
};
356388

357389
let exporter_result = LogExporter::builder()
@@ -371,7 +403,6 @@ mod tests {
371403

372404
#[cfg(feature = "grpc-tonic")]
373405
#[tokio::test]
374-
#[ignore = "Unstable due to interference from env variable tests. Re-enable after https://github.com/open-telemetry/opentelemetry-rust/issues/2818 is resolved."]
375406
async fn export_builder_error_invalid_grpc_endpoint() {
376407
use std::time::Duration;
377408

@@ -380,7 +411,7 @@ mod tests {
380411
let ex_config = ExportConfig {
381412
endpoint: Some("invalid_uri/something".to_string()),
382413
protocol: Protocol::Grpc,
383-
timeout: Duration::from_secs(10),
414+
timeout: Some(Duration::from_secs(10)),
384415
};
385416

386417
let exporter_result = LogExporter::builder()
@@ -500,4 +531,44 @@ mod tests {
500531
)
501532
}
502533
}
534+
535+
#[test]
536+
fn test_priority_of_signal_env_over_generic_env_for_timeout() {
537+
run_env_test(
538+
vec![
539+
(crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, "3000"),
540+
(super::OTEL_EXPORTER_OTLP_TIMEOUT, "2000"),
541+
],
542+
|| {
543+
let timeout =
544+
super::resolve_timeout(crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, None);
545+
assert_eq!(timeout.as_millis(), 3000);
546+
},
547+
);
548+
}
549+
550+
#[test]
551+
fn test_priority_of_code_based_config_over_envs_for_timeout() {
552+
run_env_test(
553+
vec![
554+
(crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, "3000"),
555+
(super::OTEL_EXPORTER_OTLP_TIMEOUT, "2000"),
556+
],
557+
|| {
558+
let timeout = super::resolve_timeout(
559+
crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT,
560+
Some(&std::time::Duration::from_millis(1000)),
561+
);
562+
assert_eq!(timeout.as_millis(), 1000);
563+
},
564+
);
565+
}
566+
567+
#[test]
568+
fn test_use_default_when_others_missing_for_timeout() {
569+
run_env_test(vec![], || {
570+
let timeout = super::resolve_timeout(crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, None);
571+
assert_eq!(timeout.as_millis(), 10_000);
572+
});
573+
}
503574
}

0 commit comments

Comments
 (0)