#[cfg(any(feature = "http-proto", feature = "http-json"))]
use crate::exporter::http::HttpExporterBuilder;
#[cfg(feature = "grpc-tonic")]
use crate::exporter::tonic::TonicExporterBuilder;
use crate::Protocol;
#[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use std::time::Duration;
use thiserror::Error;
pub const OTEL_EXPORTER_OTLP_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT";
pub const OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT: &str = OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT;
pub const OTEL_EXPORTER_OTLP_HEADERS: &str = "OTEL_EXPORTER_OTLP_HEADERS";
pub const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL";
pub const OTEL_EXPORTER_OTLP_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_COMPRESSION";
pub const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF: &str = "http/protobuf";
pub const OTEL_EXPORTER_OTLP_PROTOCOL_GRPC: &str = "grpc";
pub const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON: &str = "http/json";
pub const OTEL_EXPORTER_OTLP_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_TIMEOUT";
pub const OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT: Duration = Duration::from_millis(10000);
#[cfg(feature = "grpc-tonic")]
const OTEL_EXPORTER_OTLP_GRPC_ENDPOINT_DEFAULT: &str = "http://localhost:4317";
const OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT: &str = "http://localhost:4318";
#[cfg(any(feature = "http-proto", feature = "http-json"))]
pub(crate) mod http;
#[cfg(feature = "grpc-tonic")]
pub(crate) mod tonic;
#[derive(Debug, Default)]
pub(crate) struct ExportConfig {
pub endpoint: Option<String>,
pub protocol: Option<Protocol>,
pub timeout: Option<Duration>,
}
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
pub(crate) fn resolve_protocol(
signal_protocol_var: &str,
provided_protocol: Option<Protocol>,
) -> Protocol {
if let Some(protocol) = provided_protocol {
return protocol;
}
if let Some(protocol) = Protocol::parse_from_env_var(signal_protocol_var) {
return protocol;
}
if let Some(protocol) = Protocol::from_env() {
return protocol;
}
Protocol::feature_default()
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ExporterBuildError {
#[error("Spawning a new thread failed. Unable to create Reqwest-Blocking client.")]
ThreadSpawnFailed,
#[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),
#[error("no http client specified")]
NoHttpClient,
#[error("unsupported compression algorithm '{0}'")]
UnsupportedCompressionAlgorithm(String),
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
#[error("invalid URI {0}. Reason {1}")]
InvalidUri(String, String),
#[error("{name}: {reason}")]
InvalidConfig {
name: String,
reason: String,
},
#[error("Reason: {0}")]
InternalFailure(String),
}
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Compression {
Gzip,
Zstd,
}
impl Display for Compression {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Compression::Gzip => write!(f, "gzip"),
Compression::Zstd => write!(f, "zstd"),
}
}
}
impl FromStr for Compression {
type Err = ExporterBuildError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"gzip" => Ok(Compression::Gzip),
"zstd" => Ok(Compression::Zstd),
_ => Err(ExporterBuildError::UnsupportedCompressionAlgorithm(
s.to_string(),
)),
}
}
}
#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))]
fn resolve_compression_from_env(
config_compression: Option<Compression>,
signal_env_var: &str,
) -> Result<Option<Compression>, ExporterBuildError> {
if let Some(compression) = config_compression {
Ok(Some(compression))
} else if let Ok(compression) = std::env::var(signal_env_var) {
Ok(Some(compression.parse::<Compression>()?))
} else if let Ok(compression) = std::env::var(OTEL_EXPORTER_OTLP_COMPRESSION) {
Ok(Some(compression.parse::<Compression>()?))
} else {
Ok(None)
}
}
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
impl Default for Protocol {
fn default() -> Self {
Protocol::feature_default()
}
}
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
fn default_headers() -> std::collections::HashMap<String, String> {
let mut headers = std::collections::HashMap::new();
headers.insert(
"User-Agent".to_string(),
format!("OTel-OTLP-Exporter-Rust/{}", env!("CARGO_PKG_VERSION")),
);
headers
}
pub(crate) trait HasExportConfig {
fn export_config(&mut self) -> &mut ExportConfig;
}
#[cfg(feature = "grpc-tonic")]
impl HasExportConfig for TonicExporterBuilder {
fn export_config(&mut self) -> &mut ExportConfig {
&mut self.exporter_config
}
}
#[cfg(any(feature = "http-proto", feature = "http-json"))]
impl HasExportConfig for HttpExporterBuilder {
fn export_config(&mut self) -> &mut ExportConfig {
&mut self.exporter_config
}
}
pub trait WithExportConfig {
fn with_endpoint<T: Into<String>>(self, endpoint: T) -> Self;
fn with_protocol(self, protocol: Protocol) -> Self;
fn with_timeout(self, timeout: Duration) -> Self;
}
impl<B: HasExportConfig> WithExportConfig for B {
fn with_endpoint<T: Into<String>>(mut self, endpoint: T) -> Self {
self.export_config().endpoint = Some(endpoint.into());
self
}
fn with_protocol(mut self, protocol: Protocol) -> Self {
self.export_config().protocol = Some(protocol);
self
}
fn with_timeout(mut self, timeout: Duration) -> Self {
self.export_config().timeout = Some(timeout);
self
}
}
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
fn resolve_timeout(signal_timeout_var: &str, provided_timeout: Option<&Duration>) -> Duration {
if let Some(timeout) = provided_timeout {
*timeout
} else if let Some(timeout) = std::env::var(signal_timeout_var)
.ok()
.and_then(|s| s.parse().ok())
{
Duration::from_millis(timeout)
} else if let Some(timeout) = std::env::var(OTEL_EXPORTER_OTLP_TIMEOUT)
.ok()
.and_then(|s| s.parse().ok())
{
Duration::from_millis(timeout)
} else {
OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT
}
}
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
fn parse_header_string(value: &str) -> impl Iterator<Item = (&str, String)> {
value
.split_terminator(',')
.map(str::trim)
.filter_map(parse_header_key_value_string)
}
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
fn url_decode(value: &str) -> Option<String> {
let mut result = String::with_capacity(value.len());
let mut chars_to_decode = Vec::<u8>::new();
let mut all_chars = value.chars();
loop {
let ch = all_chars.next();
if ch.is_some() && ch.unwrap() == '%' {
chars_to_decode.push(
u8::from_str_radix(&format!("{}{}", all_chars.next()?, all_chars.next()?), 16)
.ok()?,
);
continue;
}
if !chars_to_decode.is_empty() {
result.push_str(std::str::from_utf8(&chars_to_decode).ok()?);
chars_to_decode.clear();
}
if let Some(c) = ch {
result.push(c);
} else {
return Some(result);
}
}
}
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
fn parse_header_key_value_string(key_value_string: &str) -> Option<(&str, String)> {
key_value_string
.split_once('=')
.map(|(key, value)| {
(
key.trim(),
url_decode(value.trim()).unwrap_or(value.to_string()),
)
})
.filter(|(key, value)| !key.is_empty() && !value.is_empty())
}
#[cfg(test)]
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
mod tests {
pub(crate) fn run_env_test<T, F>(env_vars: T, f: F)
where
F: FnOnce(),
T: Into<Vec<(&'static str, &'static str)>>,
{
temp_env::with_vars(
env_vars
.into()
.iter()
.map(|&(k, v)| (k, Some(v)))
.collect::<Vec<(&'static str, Option<&'static str>)>>(),
f,
)
}
#[cfg(any(feature = "http-proto", feature = "http-json"))]
#[test]
fn test_default_http_endpoint() {
let exporter_builder = crate::HttpExporterBuilder::default();
assert_eq!(exporter_builder.exporter_config.endpoint, None);
}
#[cfg(feature = "logs")]
#[cfg(any(feature = "http-proto", feature = "http-json"))]
#[test]
fn export_builder_error_invalid_http_endpoint() {
use crate::{LogExporter, WithExportConfig};
let exporter_result = LogExporter::builder()
.with_http()
.with_endpoint("invalid_uri/something")
.with_timeout(std::time::Duration::from_secs(10))
.build();
assert!(
matches!(
exporter_result,
Err(crate::exporter::ExporterBuildError::InvalidUri(_, _))
),
"Expected InvalidUri error, but got {exporter_result:?}"
);
}
#[cfg(feature = "grpc-tonic")]
#[tokio::test]
async fn export_builder_error_invalid_grpc_endpoint() {
use crate::{LogExporter, WithExportConfig};
let exporter_result = LogExporter::builder()
.with_tonic()
.with_endpoint("invalid_uri/something")
.with_timeout(std::time::Duration::from_secs(10))
.build();
assert!(matches!(
exporter_result,
Err(crate::exporter::ExporterBuildError::InvalidUri(_, _))
));
}
#[cfg(feature = "grpc-tonic")]
#[test]
fn test_default_tonic_endpoint() {
let exporter_builder = crate::TonicExporterBuilder::default();
assert_eq!(exporter_builder.exporter_config.endpoint, None);
}
#[test]
fn test_default_protocol() {
#[cfg(all(
feature = "http-json",
not(any(feature = "grpc-tonic", feature = "http-proto"))
))]
{
assert_eq!(crate::Protocol::default(), crate::Protocol::HttpJson);
}
#[cfg(all(
feature = "http-proto",
not(any(feature = "grpc-tonic", feature = "http-json"))
))]
{
assert_eq!(crate::Protocol::default(), crate::Protocol::HttpBinary);
}
#[cfg(all(
feature = "grpc-tonic",
not(any(feature = "http-proto", feature = "http-json"))
))]
{
assert_eq!(crate::exporter::default_protocol(), crate::Protocol::Grpc);
}
}
#[test]
fn test_protocol_from_env() {
use crate::{Protocol, OTEL_EXPORTER_OTLP_PROTOCOL};
temp_env::with_var_unset(OTEL_EXPORTER_OTLP_PROTOCOL, || {
assert_eq!(Protocol::from_env(), None);
});
#[cfg(feature = "grpc-tonic")]
run_env_test(vec![(OTEL_EXPORTER_OTLP_PROTOCOL, "grpc")], || {
assert_eq!(Protocol::from_env(), Some(Protocol::Grpc));
});
#[cfg(feature = "http-proto")]
run_env_test(vec![(OTEL_EXPORTER_OTLP_PROTOCOL, "http/protobuf")], || {
assert_eq!(Protocol::from_env(), Some(Protocol::HttpBinary));
});
#[cfg(feature = "http-json")]
run_env_test(vec![(OTEL_EXPORTER_OTLP_PROTOCOL, "http/json")], || {
assert_eq!(Protocol::from_env(), Some(Protocol::HttpJson));
});
run_env_test(vec![(OTEL_EXPORTER_OTLP_PROTOCOL, "invalid")], || {
assert_eq!(Protocol::from_env(), None);
});
}
#[test]
fn test_default_protocol_ignores_env() {
run_env_test(vec![], || {
assert_eq!(
crate::Protocol::default(),
crate::Protocol::feature_default()
);
});
#[cfg(all(feature = "grpc-tonic", feature = "http-json"))]
run_env_test(vec![(crate::OTEL_EXPORTER_OTLP_PROTOCOL, "grpc")], || {
assert_eq!(
crate::Protocol::default(),
crate::Protocol::feature_default()
);
});
}
#[test]
fn test_url_decode() {
let test_cases = vec![
("v%201", Some("v 1")),
("v 1", Some("v 1")),
("%C3%B6%C3%A0%C2%A7%C3%96abcd%C3%84", Some("öà §ÖabcdÄ")),
("v%XX1", None),
];
for (encoded, expected_decoded) in test_cases {
assert_eq!(
super::url_decode(encoded),
expected_decoded.map(|v| v.to_string()),
)
}
}
#[test]
fn test_parse_header_string() {
let test_cases = vec![
("k1=v1", vec![("k1", "v1")]),
("k1=v1,k2=v2", vec![("k1", "v1"), ("k2", "v2")]),
("k1=v1=10,k2,k3", vec![("k1", "v1=10")]),
("k1=v1,,,k2,k3=10", vec![("k1", "v1"), ("k3", "10")]),
];
for (input_str, expected_headers) in test_cases {
assert_eq!(
super::parse_header_string(input_str).collect::<Vec<_>>(),
expected_headers
.into_iter()
.map(|(k, v)| (k, v.to_string()))
.collect::<Vec<_>>(),
)
}
}
#[test]
fn test_parse_header_key_value_string() {
let test_cases = vec![
("k1=v1", Some(("k1", "v1"))),
(
"Authentication=Basic AAA",
Some(("Authentication", "Basic AAA")),
),
(
"Authentication=Basic%20AAA",
Some(("Authentication", "Basic AAA")),
),
("k1=%XX", Some(("k1", "%XX"))),
("", None),
("=v1", None),
("k1=", None),
];
for (input_str, expected_headers) in test_cases {
assert_eq!(
super::parse_header_key_value_string(input_str),
expected_headers.map(|(k, v)| (k, v.to_string())),
)
}
}
#[test]
fn test_priority_of_signal_env_over_generic_env_for_timeout() {
run_env_test(
vec![
(crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, "3000"),
(super::OTEL_EXPORTER_OTLP_TIMEOUT, "2000"),
],
|| {
let timeout =
super::resolve_timeout(crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, None);
assert_eq!(timeout.as_millis(), 3000);
},
);
}
#[test]
fn test_priority_of_code_based_config_over_envs_for_timeout() {
run_env_test(
vec![
(crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, "3000"),
(super::OTEL_EXPORTER_OTLP_TIMEOUT, "2000"),
],
|| {
let timeout = super::resolve_timeout(
crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT,
Some(&std::time::Duration::from_millis(1000)),
);
assert_eq!(timeout.as_millis(), 1000);
},
);
}
#[test]
fn test_use_default_when_others_missing_for_timeout() {
run_env_test(vec![], || {
let timeout = super::resolve_timeout(crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, None);
assert_eq!(timeout.as_millis(), 10_000);
});
}
#[test]
fn test_protocol_parse_from_env_var() {
use crate::Protocol;
temp_env::with_var_unset("MY_CUSTOM_PROTOCOL_VAR", || {
assert_eq!(Protocol::parse_from_env_var("MY_CUSTOM_PROTOCOL_VAR"), None);
});
#[cfg(feature = "http-proto")]
run_env_test(vec![("MY_CUSTOM_PROTOCOL_VAR", "http/protobuf")], || {
assert_eq!(
Protocol::parse_from_env_var("MY_CUSTOM_PROTOCOL_VAR"),
Some(Protocol::HttpBinary)
);
});
#[cfg(feature = "grpc-tonic")]
run_env_test(vec![("MY_CUSTOM_PROTOCOL_VAR", "grpc")], || {
assert_eq!(
Protocol::parse_from_env_var("MY_CUSTOM_PROTOCOL_VAR"),
Some(Protocol::Grpc)
);
});
run_env_test(vec![("MY_CUSTOM_PROTOCOL_VAR", "invalid")], || {
assert_eq!(Protocol::parse_from_env_var("MY_CUSTOM_PROTOCOL_VAR"), None);
});
}
#[cfg(feature = "http-proto")]
#[test]
fn test_resolve_protocol_signal_env_overrides_generic() {
use crate::Protocol;
run_env_test(
vec![
(crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, "http/protobuf"),
(crate::OTEL_EXPORTER_OTLP_PROTOCOL, "grpc"),
],
|| {
let protocol =
super::resolve_protocol(crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, None);
assert_eq!(protocol, Protocol::HttpBinary);
},
);
}
#[cfg(feature = "http-proto")]
#[test]
fn test_resolve_protocol_code_overrides_all_envs() {
use crate::Protocol;
run_env_test(
vec![
(crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, "grpc"),
(crate::OTEL_EXPORTER_OTLP_PROTOCOL, "grpc"),
],
|| {
let protocol = super::resolve_protocol(
crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
Some(Protocol::HttpBinary),
);
assert_eq!(protocol, Protocol::HttpBinary);
},
);
}
#[cfg(all(feature = "grpc-tonic", feature = "http-proto"))]
#[test]
fn test_resolve_protocol_falls_back_to_generic_env() {
use crate::Protocol;
run_env_test(vec![(crate::OTEL_EXPORTER_OTLP_PROTOCOL, "grpc")], || {
let protocol = super::resolve_protocol(crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, None);
assert_eq!(protocol, Protocol::Grpc);
});
}
#[test]
fn test_resolve_protocol_falls_back_to_feature_default() {
use crate::Protocol;
run_env_test(vec![], || {
let protocol = super::resolve_protocol(crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, None);
assert_eq!(protocol, Protocol::feature_default());
});
}
}