#![allow(dead_code)]
use url::Url;
use crate::errors::TelemetryError;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum OtlpProtocol {
HttpProtobuf,
HttpJson,
#[cfg(feature = "otel-grpc")]
Grpc,
}
const DEFAULT_PROTOCOL: OtlpProtocol = OtlpProtocol::HttpProtobuf;
pub(super) fn resolve_protocol(raw: &str) -> Result<OtlpProtocol, TelemetryError> {
match raw.trim() {
"" => Ok(DEFAULT_PROTOCOL),
"http/protobuf" => Ok(OtlpProtocol::HttpProtobuf),
"http/json" => Ok(OtlpProtocol::HttpJson),
"grpc" => {
#[cfg(feature = "otel-grpc")]
{
Ok(OtlpProtocol::Grpc)
}
#[cfg(not(feature = "otel-grpc"))]
{
Err(TelemetryError::new(
"OTEL_EXPORTER_OTLP_PROTOCOL=grpc requires the `otel-grpc` cargo feature; \
rebuild with --features otel-grpc or use http/protobuf",
))
}
}
other => Err(TelemetryError::new(format!(
"unknown OTLP protocol {other:?}; expected one of: http/protobuf, http/json, grpc",
))),
}
}
pub(super) fn validate_endpoint(endpoint: &str) -> Result<(), TelemetryError> {
let parsed = Url::parse(endpoint)
.map_err(|e| TelemetryError::new(format!("invalid OTLP endpoint {endpoint:?}: {e}")))?;
match parsed.scheme() {
"http" | "https" => {}
scheme => {
return Err(TelemetryError::new(format!(
"invalid OTLP endpoint scheme {scheme:?} in {endpoint:?}: expected http or https",
)));
}
}
if parsed.port() == Some(0) {
return Err(TelemetryError::new(format!(
"invalid OTLP endpoint (port 0 is reserved): {endpoint:?}",
)));
}
if parsed.port().is_none() {
let authority = &endpoint[parsed.scheme().len() + 3..]; let host_port_path = authority.split('/').next().unwrap_or("");
let host_port = host_port_path.rsplit('@').next().unwrap_or(host_port_path);
let after_bracket = host_port.rsplit(']').next().unwrap_or(host_port);
if after_bracket.contains(':') {
return Err(TelemetryError::new(format!(
"invalid OTLP endpoint (empty port): {endpoint:?}",
)));
}
}
Ok(())
}
pub(super) fn validate_optional_endpoint(
endpoint: Option<&String>,
) -> Result<Option<String>, TelemetryError> {
match endpoint {
Some(endpoint) => {
validate_endpoint(endpoint)?;
Ok(Some(endpoint.clone()))
}
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_string_defaults_to_http_protobuf() {
assert_eq!(resolve_protocol("").unwrap(), OtlpProtocol::HttpProtobuf);
assert_eq!(resolve_protocol(" ").unwrap(), OtlpProtocol::HttpProtobuf);
}
#[test]
fn http_protobuf_and_json_accepted() {
assert_eq!(
resolve_protocol("http/protobuf").unwrap(),
OtlpProtocol::HttpProtobuf
);
assert_eq!(
resolve_protocol("http/json").unwrap(),
OtlpProtocol::HttpJson
);
}
#[test]
fn unknown_protocol_returns_error_listing_valid_values() {
let err = resolve_protocol("kafka").expect_err("unknown should fail");
assert!(
err.message.contains("unknown OTLP protocol"),
"unexpected: {}",
err.message
);
assert!(
err.message.contains("http/protobuf") && err.message.contains("grpc"),
"error must list valid values: {}",
err.message
);
}
#[cfg(not(feature = "otel-grpc"))]
#[test]
fn grpc_without_feature_returns_helpful_error() {
let err = resolve_protocol("grpc").expect_err("grpc without feature should fail");
assert!(
err.message.contains("otel-grpc"),
"error must mention the cargo feature: {}",
err.message
);
}
#[cfg(feature = "otel-grpc")]
#[test]
fn grpc_with_feature_is_accepted() {
assert_eq!(resolve_protocol("grpc").unwrap(), OtlpProtocol::Grpc);
}
#[test]
fn valid_http_endpoint_is_accepted() {
assert!(validate_endpoint("http://localhost:4318").is_ok());
assert!(validate_endpoint("http://collector.example.com/v1/traces").is_ok());
assert!(validate_endpoint("https://otel.example.com:4317/v1/metrics").is_ok());
}
#[test]
fn valid_https_endpoint_without_port_is_accepted() {
assert!(validate_endpoint("https://collector.example.com").is_ok());
}
#[test]
fn invalid_scheme_returns_error() {
let err = validate_endpoint("ftp://host:4318").expect_err("ftp should fail");
assert!(
err.message.contains("scheme") && err.message.contains("ftp"),
"error must mention bad scheme: {}",
err.message
);
}
#[test]
fn grpc_scheme_rejected() {
let err = validate_endpoint("grpc://host:4317").expect_err("grpc scheme should fail");
assert!(
err.message.contains("scheme"),
"error must mention bad scheme: {}",
err.message
);
}
#[test]
fn completely_unparseable_url_returns_error() {
let err = validate_endpoint("not_a_url").expect_err("invalid URL should fail");
assert!(
err.message.contains("invalid OTLP endpoint"),
"error must describe the problem: {}",
err.message
);
}
#[test]
fn out_of_range_port_returns_error() {
let err =
validate_endpoint("http://host:99999").expect_err("out-of-range port should fail");
assert!(
err.message.contains("invalid OTLP endpoint"),
"error must describe the problem: {}",
err.message
);
}
#[test]
fn empty_host_returns_error() {
let err = validate_endpoint("http://:4318/path").expect_err("empty host should fail");
assert!(
err.message.contains("invalid OTLP endpoint"),
"error must describe the problem: {}",
err.message
);
}
#[test]
fn empty_port_returns_error() {
let err = validate_endpoint("http://host:").expect_err("empty port should fail");
assert!(
err.message.contains("empty port"),
"error must mention empty port: {}",
err.message
);
}
#[test]
fn empty_port_with_path_returns_error() {
let err = validate_endpoint("http://host:/v1/traces")
.expect_err("empty port with path should fail");
assert!(
err.message.contains("empty port"),
"error must mention empty port: {}",
err.message
);
}
#[test]
fn ipv6_with_valid_port_accepted() {
assert!(validate_endpoint("http://[::1]:4318").is_ok());
}
#[test]
fn ipv6_no_port_accepted() {
assert!(validate_endpoint("http://[::1]").is_ok());
}
#[test]
fn parity_valid_endpoints() {
let valid = [
"http://localhost:4318",
"https://collector.example.com",
"http://host:4318/v1/traces",
"http://host",
"http://[::1]:4318",
"http://[::1]",
"https://otel.example.com:4317/v1/metrics",
"https://user:pw@collector.example/v1/logs",
"https://user:pw@collector.example:4318/v1/logs",
];
for ep in valid {
assert!(
validate_endpoint(ep).is_ok(),
"expected valid endpoint {ep:?} to be accepted"
);
}
}
#[test]
fn parity_invalid_endpoints() {
let invalid = [
"",
"not-a-url",
"ftp://host:4318",
"http://",
"http://host:bad",
"http://host:-1",
"http://host:0",
"http://host:99999",
"http://host:",
"http://host:/v1/traces",
];
for ep in invalid {
assert!(
validate_endpoint(ep).is_err(),
"expected invalid endpoint {ep:?} to be rejected"
);
}
}
}