use std::fmt::Debug;
use opentelemetry_sdk::error::OTelSdkResult;
use opentelemetry_sdk::trace::SpanData;
use crate::ExporterBuildError;
#[cfg(feature = "grpc-tonic")]
use crate::{
exporter::tonic::{HasTonicConfig, TonicExporterBuilder},
TonicExporterBuilderSet,
};
#[cfg(any(feature = "http-proto", feature = "http-json"))]
use crate::{
exporter::http::{HasHttpConfig, HttpExporterBuilder},
HttpExporterBuilderSet,
};
use crate::{exporter::HasExportConfig, NoExporterBuilderSet};
pub const OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
pub const OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT";
pub const OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION";
pub const OTEL_EXPORTER_OTLP_TRACES_HEADERS: &str = "OTEL_EXPORTER_OTLP_TRACES_HEADERS";
pub const OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
#[derive(Debug, Default, Clone)]
pub struct SpanExporterBuilder<C> {
client: C,
}
impl SpanExporterBuilder<NoExporterBuilderSet> {
pub fn new() -> Self {
SpanExporterBuilder::default()
}
#[cfg(feature = "grpc-tonic")]
pub fn with_tonic(self) -> SpanExporterBuilder<TonicExporterBuilderSet> {
SpanExporterBuilder {
client: TonicExporterBuilderSet(TonicExporterBuilder::default()),
}
}
#[cfg(any(feature = "http-proto", feature = "http-json"))]
pub fn with_http(self) -> SpanExporterBuilder<HttpExporterBuilderSet> {
SpanExporterBuilder {
client: HttpExporterBuilderSet(HttpExporterBuilder::default()),
}
}
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
pub fn build(self) -> Result<SpanExporter, ExporterBuildError> {
let protocol = crate::exporter::resolve_protocol(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, None);
match protocol {
#[cfg(feature = "grpc-tonic")]
crate::Protocol::Grpc => self.with_tonic().build(),
#[cfg(feature = "http-proto")]
crate::Protocol::HttpBinary => self.with_http().build(),
#[cfg(feature = "http-json")]
crate::Protocol::HttpJson => self.with_http().build(),
}
}
}
#[cfg(feature = "grpc-tonic")]
impl SpanExporterBuilder<TonicExporterBuilderSet> {
pub fn build(self) -> Result<SpanExporter, ExporterBuildError> {
let span_exporter = self.client.0.build_span_exporter()?;
opentelemetry::otel_debug!(name: "SpanExporterBuilt");
Ok(span_exporter)
}
}
#[cfg(any(feature = "http-proto", feature = "http-json"))]
impl SpanExporterBuilder<HttpExporterBuilderSet> {
pub fn build(self) -> Result<SpanExporter, ExporterBuildError> {
let span_exporter = self.client.0.build_span_exporter()?;
Ok(span_exporter)
}
}
#[cfg(feature = "grpc-tonic")]
impl HasExportConfig for SpanExporterBuilder<TonicExporterBuilderSet> {
fn export_config(&mut self) -> &mut crate::exporter::ExportConfig {
&mut self.client.0.exporter_config
}
}
#[cfg(any(feature = "http-proto", feature = "http-json"))]
impl HasExportConfig for SpanExporterBuilder<HttpExporterBuilderSet> {
fn export_config(&mut self) -> &mut crate::exporter::ExportConfig {
&mut self.client.0.exporter_config
}
}
#[cfg(feature = "grpc-tonic")]
impl HasTonicConfig for SpanExporterBuilder<TonicExporterBuilderSet> {
fn tonic_config(&mut self) -> &mut crate::exporter::tonic::TonicConfig {
&mut self.client.0.tonic_config
}
}
#[cfg(any(feature = "http-proto", feature = "http-json"))]
impl HasHttpConfig for SpanExporterBuilder<HttpExporterBuilderSet> {
fn http_client_config(&mut self) -> &mut crate::exporter::http::HttpConfig {
&mut self.client.0.http_config
}
}
#[derive(Debug)]
pub struct SpanExporter {
client: SupportedTransportClient,
}
#[derive(Debug)]
enum SupportedTransportClient {
#[cfg(feature = "grpc-tonic")]
Tonic(crate::exporter::tonic::trace::TonicTracesClient),
#[cfg(any(feature = "http-proto", feature = "http-json"))]
Http(crate::exporter::http::OtlpHttpClient),
}
impl SpanExporter {
pub fn builder() -> SpanExporterBuilder<NoExporterBuilderSet> {
SpanExporterBuilder::default()
}
#[cfg(any(feature = "http-proto", feature = "http-json"))]
pub(crate) fn from_http(client: crate::exporter::http::OtlpHttpClient) -> Self {
SpanExporter {
client: SupportedTransportClient::Http(client),
}
}
#[cfg(feature = "grpc-tonic")]
pub(crate) fn from_tonic(client: crate::exporter::tonic::trace::TonicTracesClient) -> Self {
SpanExporter {
client: SupportedTransportClient::Tonic(client),
}
}
}
impl opentelemetry_sdk::trace::SpanExporter for SpanExporter {
async fn export(&self, batch: Vec<SpanData>) -> OTelSdkResult {
match &self.client {
#[cfg(feature = "grpc-tonic")]
SupportedTransportClient::Tonic(client) => client.export(batch).await,
#[cfg(any(feature = "http-proto", feature = "http-json"))]
SupportedTransportClient::Http(client) => client.export(batch).await,
}
}
fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) {
match &mut self.client {
#[cfg(feature = "grpc-tonic")]
SupportedTransportClient::Tonic(client) => client.set_resource(resource),
#[cfg(any(feature = "http-proto", feature = "http-json"))]
SupportedTransportClient::Http(client) => client.set_resource(resource),
}
}
}
#[cfg(test)]
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
mod tests {
use crate::SpanExporter;
#[test]
fn build_with_default_transport() {
let result = SpanExporter::builder().build();
assert!(result.is_ok(), "build() should succeed: {:?}", result.err());
}
#[cfg(all(feature = "grpc-tonic", feature = "http-proto"))]
#[test]
fn build_auto_select_respects_signal_protocol_env() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
temp_env::with_vars(
vec![
(super::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, Some("grpc")),
(crate::OTEL_EXPORTER_OTLP_PROTOCOL, None::<&str>),
],
|| {
let result = SpanExporter::builder().build();
assert!(
result.is_ok(),
"build() with TRACES_PROTOCOL=grpc should succeed: {:?}",
result.err()
);
},
);
temp_env::with_vars(
vec![
(
super::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
Some("http/protobuf"),
),
(crate::OTEL_EXPORTER_OTLP_PROTOCOL, None::<&str>),
],
|| {
let result = SpanExporter::builder().build();
assert!(
result.is_ok(),
"build() with TRACES_PROTOCOL=http/protobuf should succeed: {:?}",
result.err()
);
},
);
});
}
#[cfg(all(feature = "grpc-tonic", feature = "http-proto"))]
#[test]
fn build_auto_select_signal_env_overrides_generic() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
temp_env::with_vars(
vec![
(super::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, Some("grpc")),
(crate::OTEL_EXPORTER_OTLP_PROTOCOL, Some("http/protobuf")),
],
|| {
let result = SpanExporter::builder().build();
assert!(
result.is_ok(),
"signal-specific protocol should override generic: {:?}",
result.err()
);
},
);
});
}
#[cfg(all(feature = "grpc-tonic", feature = "http-proto"))]
#[test]
fn build_tonic_with_http_protocol_returns_error() {
use crate::exporter::WithExportConfig;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let result = SpanExporter::builder()
.with_tonic()
.with_protocol(crate::Protocol::HttpBinary)
.build();
assert!(result.is_err(), "tonic with HTTP protocol should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("not compatible with gRPC transport"),
"error should mention transport incompatibility, got: {err}"
);
});
}
#[cfg(all(feature = "grpc-tonic", feature = "http-proto"))]
#[test]
fn build_http_with_grpc_protocol_returns_error() {
use crate::exporter::WithExportConfig;
let result = SpanExporter::builder()
.with_http()
.with_protocol(crate::Protocol::Grpc)
.build();
assert!(result.is_err(), "HTTP with gRPC protocol should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("not compatible with HTTP transport"),
"error should mention transport incompatibility, got: {err}"
);
}
}