opentelemetry_otlp/exporter/
mod.rs1#[cfg(any(feature = "http-proto", feature = "http-json"))]
6use crate::exporter::http::HttpExporterBuilder;
7#[cfg(feature = "grpc-tonic")]
8use crate::exporter::tonic::TonicExporterBuilder;
9use crate::{Error, Protocol};
10#[cfg(feature = "serialize")]
11use serde::{Deserialize, Serialize};
12use std::fmt::{Display, Formatter};
13use std::str::FromStr;
14use std::time::Duration;
15
16pub const OTEL_EXPORTER_OTLP_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT";
20pub const OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT: &str = OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT;
22pub const OTEL_EXPORTER_OTLP_HEADERS: &str = "OTEL_EXPORTER_OTLP_HEADERS";
26pub const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL";
28pub const OTEL_EXPORTER_OTLP_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_COMPRESSION";
30
31#[cfg(feature = "http-json")]
32pub const OTEL_EXPORTER_OTLP_PROTOCOL_DEFAULT: &str = OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON;
34#[cfg(all(feature = "http-proto", not(feature = "http-json")))]
35pub const OTEL_EXPORTER_OTLP_PROTOCOL_DEFAULT: &str = OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF;
37#[cfg(all(
38 feature = "grpc-tonic",
39 not(any(feature = "http-proto", feature = "http-json"))
40))]
41pub const OTEL_EXPORTER_OTLP_PROTOCOL_DEFAULT: &str = OTEL_EXPORTER_OTLP_PROTOCOL_GRPC;
43
44#[cfg(not(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json")))]
45pub const OTEL_EXPORTER_OTLP_PROTOCOL_DEFAULT: &str = "";
47
48const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF: &str = "http/protobuf";
49const OTEL_EXPORTER_OTLP_PROTOCOL_GRPC: &str = "grpc";
50const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON: &str = "http/json";
51
52pub const OTEL_EXPORTER_OTLP_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_TIMEOUT";
54pub const OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT: u64 = 10;
56
57#[cfg(feature = "grpc-tonic")]
59const OTEL_EXPORTER_OTLP_GRPC_ENDPOINT_DEFAULT: &str = "http://localhost:4317";
60const OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT: &str = "http://localhost:4318";
61
62#[cfg(any(feature = "http-proto", feature = "http-json"))]
63pub(crate) mod http;
64#[cfg(feature = "grpc-tonic")]
65pub(crate) mod tonic;
66
67#[derive(Debug)]
69pub struct ExportConfig {
70 pub endpoint: Option<String>,
73
74 pub protocol: Protocol,
76
77 pub timeout: Duration,
79}
80
81impl Default for ExportConfig {
82 fn default() -> Self {
83 let protocol = default_protocol();
84
85 ExportConfig {
86 endpoint: None,
87 protocol,
90 timeout: Duration::from_secs(OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT),
91 }
92 }
93}
94
95#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub enum Compression {
99 Gzip,
101 Zstd,
103}
104
105impl Display for Compression {
106 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
107 match self {
108 Compression::Gzip => write!(f, "gzip"),
109 Compression::Zstd => write!(f, "zstd"),
110 }
111 }
112}
113
114impl FromStr for Compression {
115 type Err = Error;
116
117 fn from_str(s: &str) -> Result<Self, Self::Err> {
118 match s {
119 "gzip" => Ok(Compression::Gzip),
120 "zstd" => Ok(Compression::Zstd),
121 _ => Err(Error::UnsupportedCompressionAlgorithm(s.to_string())),
122 }
123 }
124}
125
126fn default_protocol() -> Protocol {
128 match OTEL_EXPORTER_OTLP_PROTOCOL_DEFAULT {
129 OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF => Protocol::HttpBinary,
130 OTEL_EXPORTER_OTLP_PROTOCOL_GRPC => Protocol::Grpc,
131 OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON => Protocol::HttpJson,
132 _ => Protocol::HttpBinary,
133 }
134}
135
136#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
138fn default_headers() -> std::collections::HashMap<String, String> {
139 let mut headers = std::collections::HashMap::new();
140 headers.insert(
141 "User-Agent".to_string(),
142 format!("OTel OTLP Exporter Rust/{}", env!("CARGO_PKG_VERSION")),
143 );
144 headers
145}
146
147pub trait HasExportConfig {
149 fn export_config(&mut self) -> &mut ExportConfig;
151}
152
153#[cfg(feature = "grpc-tonic")]
155impl HasExportConfig for TonicExporterBuilder {
156 fn export_config(&mut self) -> &mut ExportConfig {
157 &mut self.exporter_config
158 }
159}
160
161#[cfg(any(feature = "http-proto", feature = "http-json"))]
163impl HasExportConfig for HttpExporterBuilder {
164 fn export_config(&mut self) -> &mut ExportConfig {
165 &mut self.exporter_config
166 }
167}
168
169pub trait WithExportConfig {
184 fn with_endpoint<T: Into<String>>(self, endpoint: T) -> Self;
186 fn with_protocol(self, protocol: Protocol) -> Self;
194 fn with_timeout(self, timeout: Duration) -> Self;
196 fn with_export_config(self, export_config: ExportConfig) -> Self;
198}
199
200impl<B: HasExportConfig> WithExportConfig for B {
201 fn with_endpoint<T: Into<String>>(mut self, endpoint: T) -> Self {
202 self.export_config().endpoint = Some(endpoint.into());
203 self
204 }
205
206 fn with_protocol(mut self, protocol: Protocol) -> Self {
207 self.export_config().protocol = protocol;
208 self
209 }
210
211 fn with_timeout(mut self, timeout: Duration) -> Self {
212 self.export_config().timeout = timeout;
213 self
214 }
215
216 fn with_export_config(mut self, exporter_config: ExportConfig) -> Self {
217 self.export_config().endpoint = exporter_config.endpoint;
218 self.export_config().protocol = exporter_config.protocol;
219 self.export_config().timeout = exporter_config.timeout;
220 self
221 }
222}
223
224#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
225fn parse_header_string(value: &str) -> impl Iterator<Item = (&str, String)> {
226 value
227 .split_terminator(',')
228 .map(str::trim)
229 .filter_map(parse_header_key_value_string)
230}
231
232#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
233fn url_decode(value: &str) -> Option<String> {
234 let mut result = String::with_capacity(value.len());
235 let mut chars_to_decode = Vec::<u8>::new();
236 let mut all_chars = value.chars();
237
238 loop {
239 let ch = all_chars.next();
240
241 if ch.is_some() && ch.unwrap() == '%' {
242 chars_to_decode.push(
243 u8::from_str_radix(&format!("{}{}", all_chars.next()?, all_chars.next()?), 16)
244 .ok()?,
245 );
246 continue;
247 }
248
249 if !chars_to_decode.is_empty() {
250 result.push_str(std::str::from_utf8(&chars_to_decode).ok()?);
251 chars_to_decode.clear();
252 }
253
254 if let Some(c) = ch {
255 result.push(c);
256 } else {
257 return Some(result);
258 }
259 }
260}
261
262#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
263fn parse_header_key_value_string(key_value_string: &str) -> Option<(&str, String)> {
264 key_value_string
265 .split_once('=')
266 .map(|(key, value)| {
267 (
268 key.trim(),
269 url_decode(value.trim()).unwrap_or(value.to_string()),
270 )
271 })
272 .filter(|(key, value)| !key.is_empty() && !value.is_empty())
273}
274
275#[cfg(test)]
276#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
277mod tests {
278 pub(crate) fn run_env_test<T, F>(env_vars: T, f: F)
279 where
280 F: FnOnce(),
281 T: Into<Vec<(&'static str, &'static str)>>,
282 {
283 temp_env::with_vars(
284 env_vars
285 .into()
286 .iter()
287 .map(|&(k, v)| (k, Some(v)))
288 .collect::<Vec<(&'static str, Option<&'static str>)>>(),
289 f,
290 )
291 }
292
293 #[cfg(any(feature = "http-proto", feature = "http-json"))]
294 #[test]
295 fn test_default_http_endpoint() {
296 let exporter_builder = crate::HttpExporterBuilder::default();
297
298 assert_eq!(exporter_builder.exporter_config.endpoint, None);
299 }
300
301 #[cfg(feature = "grpc-tonic")]
302 #[test]
303 fn test_default_tonic_endpoint() {
304 let exporter_builder = crate::TonicExporterBuilder::default();
305
306 assert_eq!(exporter_builder.exporter_config.endpoint, None);
307 }
308
309 #[test]
310 fn test_default_protocol() {
311 #[cfg(all(
312 feature = "http-json",
313 not(any(feature = "grpc-tonic", feature = "http-proto"))
314 ))]
315 {
316 assert_eq!(
317 crate::exporter::default_protocol(),
318 crate::Protocol::HttpJson
319 );
320 }
321
322 #[cfg(all(
323 feature = "http-proto",
324 not(any(feature = "grpc-tonic", feature = "http-json"))
325 ))]
326 {
327 assert_eq!(
328 crate::exporter::default_protocol(),
329 crate::Protocol::HttpBinary
330 );
331 }
332
333 #[cfg(all(
334 feature = "grpc-tonic",
335 not(any(feature = "http-proto", feature = "http-json"))
336 ))]
337 {
338 assert_eq!(crate::exporter::default_protocol(), crate::Protocol::Grpc);
339 }
340 }
341
342 #[test]
343 fn test_url_decode() {
344 let test_cases = vec![
345 ("v%201", Some("v 1")),
347 ("v 1", Some("v 1")),
348 ("%C3%B6%C3%A0%C2%A7%C3%96abcd%C3%84", Some("öà§ÖabcdÄ")),
349 ("v%XX1", None),
350 ];
351
352 for (encoded, expected_decoded) in test_cases {
353 assert_eq!(
354 super::url_decode(encoded),
355 expected_decoded.map(|v| v.to_string()),
356 )
357 }
358 }
359
360 #[test]
361 fn test_parse_header_string() {
362 let test_cases = vec![
363 ("k1=v1", vec![("k1", "v1")]),
365 ("k1=v1,k2=v2", vec![("k1", "v1"), ("k2", "v2")]),
366 ("k1=v1=10,k2,k3", vec![("k1", "v1=10")]),
367 ("k1=v1,,,k2,k3=10", vec![("k1", "v1"), ("k3", "10")]),
368 ];
369
370 for (input_str, expected_headers) in test_cases {
371 assert_eq!(
372 super::parse_header_string(input_str).collect::<Vec<_>>(),
373 expected_headers
374 .into_iter()
375 .map(|(k, v)| (k, v.to_string()))
376 .collect::<Vec<_>>(),
377 )
378 }
379 }
380
381 #[test]
382 fn test_parse_header_key_value_string() {
383 let test_cases = vec![
384 ("k1=v1", Some(("k1", "v1"))),
386 (
387 "Authentication=Basic AAA",
388 Some(("Authentication", "Basic AAA")),
389 ),
390 (
391 "Authentication=Basic%20AAA",
392 Some(("Authentication", "Basic AAA")),
393 ),
394 ("k1=%XX", Some(("k1", "%XX"))),
395 ("", None),
396 ("=v1", None),
397 ("k1=", None),
398 ];
399
400 for (input_str, expected_headers) in test_cases {
401 assert_eq!(
402 super::parse_header_key_value_string(input_str),
403 expected_headers.map(|(k, v)| (k, v.to_string())),
404 )
405 }
406 }
407}