use std::borrow::BorrowMut;
use std::env;
use std::sync::Arc;
#[cfg(feature = "collector_client")]
use std::time::Duration;
use http::Uri;
use opentelemetry::trace::TraceError;
#[cfg(feature = "collector_client")]
use opentelemetry_http::HttpClient;
use opentelemetry_sdk::trace::{BatchConfig, BatchSpanProcessor, Config, Tracer, TracerProvider};
#[cfg(feature = "collector_client")]
use crate::config::collector::http_client::CollectorHttpClient;
#[cfg(feature = "collector_client")]
use crate::exporter::collector::AsyncHttpClient;
#[cfg(feature = "wasm_collector_client")]
use crate::exporter::collector::WasmCollector;
use crate::exporter::config::{
build_config_and_process, install_tracer_provider_and_get_tracer, HasRequiredConfig,
TransformationConfig,
};
use crate::exporter::uploader::{AsyncUploader, Uploader};
use crate::{Exporter, JaegerTraceRuntime};
#[cfg(feature = "collector_client")]
mod http_client;
const ENV_ENDPOINT: &str = "OTEL_EXPORTER_JAEGER_ENDPOINT";
const DEFAULT_ENDPOINT: &str = "http://localhost:14250/api/trace";
#[cfg(feature = "collector_client")]
const ENV_TIMEOUT: &str = "OTEL_EXPORTER_JAEGER_TIMEOUT";
#[cfg(feature = "collector_client")]
const DEFAULT_COLLECTOR_TIMEOUT: Duration = Duration::from_secs(10);
const ENV_USERNAME: &str = "OTEL_EXPORTER_JAEGER_USER";
const ENV_PASSWORD: &str = "OTEL_EXPORTER_JAEGER_PASSWORD";
#[derive(Debug)]
#[deprecated(
since = "0.21.0",
note = "Please migrate to opentelemetry-otlp exporter."
)]
pub struct CollectorPipeline {
transformation_config: TransformationConfig,
trace_config: Option<Config>,
batch_config: Option<BatchConfig>,
#[cfg(feature = "collector_client")]
collector_timeout: Duration,
collector_endpoint: Option<String>,
collector_username: Option<String>,
collector_password: Option<String>,
client_config: ClientConfig,
}
impl Default for CollectorPipeline {
fn default() -> Self {
Self {
#[cfg(feature = "collector_client")]
collector_timeout: DEFAULT_COLLECTOR_TIMEOUT,
collector_endpoint: None,
collector_username: None,
collector_password: None,
client_config: ClientConfig::default(),
transformation_config: Default::default(),
trace_config: Default::default(),
batch_config: Some(Default::default()),
}
}
}
impl HasRequiredConfig for CollectorPipeline {
fn set_transformation_config<T>(&mut self, f: T)
where
T: FnOnce(&mut TransformationConfig),
{
f(self.transformation_config.borrow_mut())
}
fn set_trace_config(&mut self, config: Config) {
self.trace_config = Some(config)
}
fn set_batch_config(&mut self, config: BatchConfig) {
self.batch_config = Some(config)
}
}
#[derive(Debug)]
enum ClientConfig {
#[cfg(feature = "collector_client")]
Http { client_type: CollectorHttpClient },
#[cfg(feature = "wasm_collector_client")]
Wasm, }
#[allow(clippy::derivable_impls)]
impl Default for ClientConfig {
fn default() -> Self {
#[cfg(feature = "collector_client")]
{
ClientConfig::Http {
client_type: CollectorHttpClient::None,
}
}
#[cfg(not(feature = "collector_client"))]
ClientConfig::Wasm
}
}
#[cfg(feature = "collector_client")]
#[deprecated(
since = "0.21.0",
note = "Please migrate to opentelemetry-otlp exporter."
)]
pub fn new_collector_pipeline() -> CollectorPipeline {
CollectorPipeline::default()
}
#[cfg(feature = "wasm_collector_client")]
#[allow(clippy::field_reassign_with_default)]
#[deprecated(
since = "0.21.0",
note = "Please migrate to opentelemetry-otlp exporter."
)]
pub fn new_wasm_collector_pipeline() -> CollectorPipeline {
let mut pipeline = CollectorPipeline::default();
pipeline.client_config = ClientConfig::Wasm;
pipeline
}
impl CollectorPipeline {
#[cfg(feature = "collector_client")]
pub fn with_timeout(self, collector_timeout: Duration) -> Self {
Self {
collector_timeout,
..self
}
}
pub fn with_endpoint<T: Into<String>>(self, collector_endpoint: T) -> Self {
Self {
collector_endpoint: Some(collector_endpoint.into()),
..self
}
}
pub fn with_username<S: Into<String>>(self, collector_username: S) -> Self {
Self {
collector_username: Some(collector_username.into()),
..self
}
}
pub fn with_password<S: Into<String>>(self, collector_password: S) -> Self {
Self {
collector_password: Some(collector_password.into()),
..self
}
}
pub fn collector_username(&self) -> Option<String> {
self.collector_username.clone()
}
pub fn collector_password(&self) -> Option<String> {
self.collector_password.clone()
}
#[cfg(feature = "collector_client")]
pub fn with_http_client<T: HttpClient + 'static>(mut self, client: T) -> Self {
self.client_config = match self.client_config {
ClientConfig::Http { .. } => ClientConfig::Http {
client_type: CollectorHttpClient::Custom(Box::new(client)),
},
#[cfg(feature = "wasm_collector_client")]
ClientConfig::Wasm => ClientConfig::Wasm,
};
self
}
#[cfg(feature = "isahc_collector_client")]
pub fn with_isahc(self) -> Self {
Self {
client_config: ClientConfig::Http {
client_type: CollectorHttpClient::Isahc,
},
..self
}
}
#[cfg(feature = "reqwest_collector_client")]
pub fn with_reqwest(self) -> Self {
Self {
client_config: ClientConfig::Http {
client_type: CollectorHttpClient::Reqwest,
},
..self
}
}
#[cfg(feature = "reqwest_blocking_collector_client")]
pub fn with_reqwest_blocking(self) -> Self {
Self {
client_config: ClientConfig::Http {
client_type: CollectorHttpClient::ReqwestBlocking,
},
..self
}
}
#[cfg(feature = "hyper_collector_client")]
pub fn with_hyper(self) -> Self {
Self {
client_config: ClientConfig::Http {
client_type: CollectorHttpClient::Hyper,
},
..self
}
}
pub fn with_service_name<T: Into<String>>(mut self, service_name: T) -> Self {
self.set_transformation_config(|config| {
config.service_name = Some(service_name.into());
});
self
}
pub fn with_instrumentation_library_tags(mut self, should_export: bool) -> Self {
self.set_transformation_config(|config| {
config.export_instrument_library = should_export;
});
self
}
pub fn with_trace_config(mut self, config: Config) -> Self {
self.set_trace_config(config);
self
}
pub fn with_batch_processor_config(mut self, config: BatchConfig) -> Self {
self.set_batch_config(config);
self
}
pub fn build_batch<R: JaegerTraceRuntime>(
mut self,
runtime: R,
) -> Result<TracerProvider, TraceError> {
let mut builder = TracerProvider::builder();
let export_instrument_library = self.transformation_config.export_instrument_library;
let (config, process) = build_config_and_process(
self.trace_config.take(),
self.transformation_config.service_name.take(),
);
let batch_config = self.batch_config.take();
let uploader = self.build_uploader::<R>()?;
let exporter = Exporter::new(process.into(), export_instrument_library, uploader);
let batch_processor = BatchSpanProcessor::builder(exporter, runtime)
.with_batch_config(batch_config.unwrap_or_default())
.build();
builder = builder.with_span_processor(batch_processor);
builder = builder.with_config(config);
Ok(builder.build())
}
pub fn install_batch<R: JaegerTraceRuntime>(self, runtime: R) -> Result<Tracer, TraceError> {
let tracer_provider = self.build_batch(runtime)?;
install_tracer_provider_and_get_tracer(tracer_provider)
}
pub fn build_collector_exporter<R>(mut self) -> Result<crate::Exporter, TraceError>
where
R: JaegerTraceRuntime,
{
let export_instrument_library = self.transformation_config.export_instrument_library;
let (_, process) = build_config_and_process(
self.trace_config.take(),
self.transformation_config.service_name.take(),
);
let uploader = self.build_uploader::<R>()?;
let exporter = Exporter::new(process.into(), export_instrument_library, uploader);
Ok(exporter)
}
fn build_uploader<R>(self) -> Result<Arc<dyn Uploader>, crate::Error>
where
R: JaegerTraceRuntime,
{
let endpoint = self.resolve_endpoint()?;
let username = self.resolve_username();
let password = self.resolve_password();
#[cfg(feature = "collector_client")]
let timeout = self.resolve_timeout();
match self.client_config {
#[cfg(feature = "collector_client")]
ClientConfig::Http { client_type } => {
let client = client_type.build_client(username, password, timeout)?;
let collector = AsyncHttpClient::new(endpoint, client);
Ok(Arc::new(AsyncUploader::<R>::Collector(collector)))
}
#[cfg(feature = "wasm_collector_client")]
ClientConfig::Wasm => {
let collector = WasmCollector::new(endpoint, username, password)
.map_err::<crate::Error, _>(Into::into)?;
Ok(Arc::new(AsyncUploader::<R>::WasmCollector(collector)))
}
}
}
fn resolve_env_var(env_var: &'static str) -> Option<String> {
env::var(env_var).ok().filter(|var| !var.is_empty())
}
fn resolve_endpoint(&self) -> Result<Uri, crate::Error> {
let endpoint_from_env = Self::resolve_env_var(ENV_ENDPOINT)
.map(|endpoint| {
Uri::try_from(endpoint.as_str()).map_err::<crate::Error, _>(|err| {
crate::Error::ConfigError {
pipeline_name: "collector",
config_name: "collector_endpoint",
reason: format!("invalid uri from environment variable, {}", err),
}
})
})
.transpose()?;
Ok(match endpoint_from_env {
Some(endpoint) => endpoint,
None => {
if let Some(endpoint) = &self.collector_endpoint {
Uri::try_from(endpoint.as_str()).map_err::<crate::Error, _>(|err| {
crate::Error::ConfigError {
pipeline_name: "collector",
config_name: "collector_endpoint",
reason: format!("invalid uri from the builder, {}", err),
}
})?
} else {
Uri::try_from(DEFAULT_ENDPOINT).unwrap() }
}
})
}
#[cfg(feature = "collector_client")]
fn resolve_timeout(&self) -> Duration {
match Self::resolve_env_var(ENV_TIMEOUT) {
Some(timeout) => match timeout.parse() {
Ok(timeout) => Duration::from_millis(timeout),
Err(e) => {
eprintln!("{} malformed default to 10s: {}", ENV_TIMEOUT, e);
self.collector_timeout
}
},
None => self.collector_timeout,
}
}
fn resolve_username(&self) -> Option<String> {
Self::resolve_env_var(ENV_USERNAME).or_else(|| self.collector_username.clone())
}
fn resolve_password(&self) -> Option<String> {
Self::resolve_env_var(ENV_PASSWORD).or_else(|| self.collector_password.clone())
}
}
#[cfg(test)]
#[cfg(feature = "rt-tokio")]
mod tests {
use super::*;
#[test]
fn test_resolve_endpoint() {
struct TestCase<'a> {
description: &'a str,
env_var: &'a str,
builder_endpoint: Option<&'a str>,
expected_result: Result<Uri, crate::Error>,
}
let test_cases = vec![
TestCase {
description: "Positive: Endpoint from environment variable exists",
env_var: "http://example.com",
builder_endpoint: None,
expected_result: Ok(Uri::try_from("http://example.com").unwrap()),
},
TestCase {
description: "Positive: Endpoint from builder",
env_var: "",
builder_endpoint: Some("http://example.com"),
expected_result: Ok(Uri::try_from("http://example.com").unwrap()),
},
TestCase {
description: "Negative: Invalid URI from environment variable",
env_var: "invalid random uri",
builder_endpoint: None,
expected_result: Err(crate::Error::ConfigError {
pipeline_name: "collector",
config_name: "collector_endpoint",
reason: "invalid uri from environment variable, invalid uri character"
.to_string(),
}),
},
TestCase {
description: "Negative: Invalid URI from builder",
env_var: "",
builder_endpoint: Some("invalid random uri"),
expected_result: Err(crate::Error::ConfigError {
pipeline_name: "collector",
config_name: "collector_endpoint",
reason: "invalid uri from the builder, invalid uri character".to_string(),
}),
},
TestCase {
description: "Positive: Default endpoint (no environment variable set)",
env_var: "",
builder_endpoint: None,
expected_result: Ok(Uri::try_from(DEFAULT_ENDPOINT).unwrap()),
},
];
for test_case in test_cases {
env::set_var(ENV_ENDPOINT, test_case.env_var);
let builder = CollectorPipeline {
collector_endpoint: test_case.builder_endpoint.map(|s| s.to_string()),
..Default::default()
};
let result = builder.resolve_endpoint();
match test_case.expected_result {
Ok(expected) => {
assert_eq!(result.unwrap(), expected, "{}", test_case.description);
}
Err(expected_err) => {
assert!(
result.is_err(),
"{}, expected error, get {}",
test_case.description,
result.unwrap()
);
match (result.unwrap_err(), expected_err) {
(
crate::Error::ConfigError {
pipeline_name: result_pipeline_name,
config_name: result_config_name,
reason: result_reason,
},
crate::Error::ConfigError {
pipeline_name: expected_pipeline_name,
config_name: expected_config_name,
reason: expected_reason,
},
) => {
assert_eq!(
result_pipeline_name, expected_pipeline_name,
"{}",
test_case.description
);
assert_eq!(
result_config_name, expected_config_name,
"{}",
test_case.description
);
assert_eq!(result_reason, expected_reason, "{}", test_case.description);
}
_ => panic!("we don't expect collector to return other error"),
}
}
}
env::remove_var(ENV_ENDPOINT);
}
}
#[test]
fn test_resolve_timeout() {
struct TestCase<'a> {
description: &'a str,
env_var: &'a str,
builder_var: Option<Duration>,
expected_duration: Duration,
}
let test_cases = vec![
TestCase {
description: "Valid environment variable",
env_var: "5000",
builder_var: None,
expected_duration: Duration::from_millis(5000),
},
TestCase {
description: "Invalid environment variable",
env_var: "invalid",
builder_var: None,
expected_duration: DEFAULT_COLLECTOR_TIMEOUT,
},
TestCase {
description: "Missing environment variable",
env_var: "",
builder_var: Some(Duration::from_millis(5000)),
expected_duration: Duration::from_millis(5000),
},
];
for test_case in test_cases {
env::set_var(ENV_TIMEOUT, test_case.env_var);
let mut builder = CollectorPipeline::default();
if let Some(timeout) = test_case.builder_var {
builder = builder.with_timeout(timeout);
}
let result = builder.resolve_timeout();
assert_eq!(
result, test_case.expected_duration,
"{}",
test_case.description
);
env::remove_var(ENV_TIMEOUT);
}
}
}