opentelemetry_otlp/exporter/
mod.rs

1//! OTLP exporter builder and configurations.
2//!
3//! OTLP supports sending data via different protocols and formats.
4
5#[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
16/// Target to which the exporter is going to send signals, defaults to https://localhost:4317.
17/// Learn about the relationship between this constant and metrics/spans/logs at
18/// <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#endpoint-urls-for-otlphttp>
19pub const OTEL_EXPORTER_OTLP_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT";
20/// Default target to which the exporter is going to send signals.
21pub const OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT: &str = OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT;
22/// Key-value pairs to be used as headers associated with gRPC or HTTP requests
23/// Example: `k1=v1,k2=v2`
24/// Note: as of now, this is only supported for HTTP requests.
25pub const OTEL_EXPORTER_OTLP_HEADERS: &str = "OTEL_EXPORTER_OTLP_HEADERS";
26/// Protocol the exporter will use. Either `http/protobuf` or `grpc`.
27pub const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL";
28/// Compression algorithm to use, defaults to none.
29pub const OTEL_EXPORTER_OTLP_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_COMPRESSION";
30
31#[cfg(feature = "http-json")]
32/// Default protocol, using http-json.
33pub const OTEL_EXPORTER_OTLP_PROTOCOL_DEFAULT: &str = OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON;
34#[cfg(all(feature = "http-proto", not(feature = "http-json")))]
35/// Default protocol, using http-proto.
36pub 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))]
41/// Default protocol, using grpc
42pub 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")))]
45/// Default protocol if no features are enabled.
46pub 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
52/// Max waiting time for the backend to process each signal batch, defaults to 10 seconds.
53pub const OTEL_EXPORTER_OTLP_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_TIMEOUT";
54/// Default max waiting time for the backend to process each signal batch.
55pub const OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT: u64 = 10;
56
57// Endpoints per protocol https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md
58#[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/// Configuration for the OTLP exporter.
68#[derive(Debug)]
69pub struct ExportConfig {
70    /// The address of the OTLP collector. If it's not provided via builder or environment variables.
71    /// Default address will be used based on the protocol.
72    pub endpoint: String,
73
74    /// The protocol to use when communicating with the collector.
75    pub protocol: Protocol,
76
77    /// The timeout to the collector.
78    pub timeout: Duration,
79}
80
81impl Default for ExportConfig {
82    fn default() -> Self {
83        let protocol = default_protocol();
84
85        ExportConfig {
86            endpoint: "".to_string(),
87            // don't use default_endpoint(protocol) here otherwise we
88            // won't know if user provided a value
89            protocol,
90            timeout: Duration::from_secs(OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT),
91        }
92    }
93}
94
95/// The compression algorithm to use when sending data.
96#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub enum Compression {
99    /// Compresses data using gzip.
100    Gzip,
101}
102
103impl Display for Compression {
104    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105        match self {
106            Compression::Gzip => write!(f, "gzip"),
107        }
108    }
109}
110
111impl FromStr for Compression {
112    type Err = Error;
113
114    fn from_str(s: &str) -> Result<Self, Self::Err> {
115        match s {
116            "gzip" => Ok(Compression::Gzip),
117            _ => Err(Error::UnsupportedCompressionAlgorithm(s.to_string())),
118        }
119    }
120}
121
122/// default protocol based on enabled features
123fn default_protocol() -> Protocol {
124    match OTEL_EXPORTER_OTLP_PROTOCOL_DEFAULT {
125        OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF => Protocol::HttpBinary,
126        OTEL_EXPORTER_OTLP_PROTOCOL_GRPC => Protocol::Grpc,
127        OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON => Protocol::HttpJson,
128        _ => Protocol::HttpBinary,
129    }
130}
131
132/// default user-agent headers
133#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
134fn default_headers() -> std::collections::HashMap<String, String> {
135    let mut headers = std::collections::HashMap::new();
136    headers.insert(
137        "User-Agent".to_string(),
138        format!("OTel OTLP Exporter Rust/{}", env!("CARGO_PKG_VERSION")),
139    );
140    headers
141}
142
143/// Provide access to the export config field within the exporter builders.
144pub trait HasExportConfig {
145    /// Return a mutable reference to the export config within the exporter builders.
146    fn export_config(&mut self) -> &mut ExportConfig;
147}
148
149#[cfg(feature = "grpc-tonic")]
150impl HasExportConfig for TonicExporterBuilder {
151    fn export_config(&mut self) -> &mut ExportConfig {
152        &mut self.exporter_config
153    }
154}
155
156#[cfg(any(feature = "http-proto", feature = "http-json"))]
157impl HasExportConfig for HttpExporterBuilder {
158    fn export_config(&mut self) -> &mut ExportConfig {
159        &mut self.exporter_config
160    }
161}
162
163/// Expose methods to override export configuration.
164///
165/// This trait will be implemented for every struct that implemented [`HasExportConfig`] trait.
166///
167/// ## Examples
168/// ```
169/// # #[cfg(all(feature = "trace", feature = "grpc-tonic"))]
170/// # {
171/// use crate::opentelemetry_otlp::WithExportConfig;
172/// let exporter_builder = opentelemetry_otlp::new_exporter()
173///     .tonic()
174///     .with_endpoint("http://localhost:7201");
175/// # }
176/// ```
177pub trait WithExportConfig {
178    /// Set the address of the OTLP collector. If not set or set to empty string, the default address is used.
179    fn with_endpoint<T: Into<String>>(self, endpoint: T) -> Self;
180    /// Set the protocol to use when communicating with the collector.
181    ///
182    /// Note that protocols that are not supported by exporters will be ignore. The exporter
183    /// will use default protocol in this case.
184    ///
185    /// ## Note
186    /// All exporters in this crate are only support one protocol thus choosing the protocol is an no-op at the moment
187    fn with_protocol(self, protocol: Protocol) -> Self;
188    /// Set the timeout to the collector.
189    fn with_timeout(self, timeout: Duration) -> Self;
190    /// Set export config. This will override all previous configuration.
191    fn with_export_config(self, export_config: ExportConfig) -> Self;
192}
193
194impl<B: HasExportConfig> WithExportConfig for B {
195    fn with_endpoint<T: Into<String>>(mut self, endpoint: T) -> Self {
196        self.export_config().endpoint = endpoint.into();
197        self
198    }
199
200    fn with_protocol(mut self, protocol: Protocol) -> Self {
201        self.export_config().protocol = protocol;
202        self
203    }
204
205    fn with_timeout(mut self, timeout: Duration) -> Self {
206        self.export_config().timeout = timeout;
207        self
208    }
209
210    fn with_export_config(mut self, exporter_config: ExportConfig) -> Self {
211        self.export_config().endpoint = exporter_config.endpoint;
212        self.export_config().protocol = exporter_config.protocol;
213        self.export_config().timeout = exporter_config.timeout;
214        self
215    }
216}
217
218#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
219fn parse_header_string(value: &str) -> impl Iterator<Item = (&str, String)> {
220    value
221        .split_terminator(',')
222        .map(str::trim)
223        .filter_map(parse_header_key_value_string)
224}
225
226#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
227fn url_decode(value: &str) -> Option<String> {
228    let mut result = String::with_capacity(value.len());
229    let mut chars_to_decode = Vec::<u8>::new();
230    let mut all_chars = value.chars();
231
232    loop {
233        let ch = all_chars.next();
234
235        if ch.is_some() && ch.unwrap() == '%' {
236            chars_to_decode.push(
237                u8::from_str_radix(&format!("{}{}", all_chars.next()?, all_chars.next()?), 16)
238                    .ok()?,
239            );
240            continue;
241        }
242
243        if !chars_to_decode.is_empty() {
244            result.push_str(std::str::from_utf8(&chars_to_decode).ok()?);
245            chars_to_decode.clear();
246        }
247
248        if let Some(c) = ch {
249            result.push(c);
250        } else {
251            return Some(result);
252        }
253    }
254}
255
256#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
257fn parse_header_key_value_string(key_value_string: &str) -> Option<(&str, String)> {
258    key_value_string
259        .split_once('=')
260        .map(|(key, value)| {
261            (
262                key.trim(),
263                url_decode(value.trim()).unwrap_or(value.to_string()),
264            )
265        })
266        .filter(|(key, value)| !key.is_empty() && !value.is_empty())
267}
268
269#[cfg(test)]
270#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
271mod tests {
272    pub(crate) fn run_env_test<T, F>(env_vars: T, f: F)
273    where
274        F: FnOnce(),
275        T: Into<Vec<(&'static str, &'static str)>>,
276    {
277        temp_env::with_vars(
278            env_vars
279                .into()
280                .iter()
281                .map(|&(k, v)| (k, Some(v)))
282                .collect::<Vec<(&'static str, Option<&'static str>)>>(),
283            f,
284        )
285    }
286
287    #[cfg(any(feature = "http-proto", feature = "http-json"))]
288    #[test]
289    fn test_default_http_endpoint() {
290        let exporter_builder = crate::new_exporter().http();
291
292        assert_eq!(exporter_builder.exporter_config.endpoint, "");
293    }
294
295    #[cfg(feature = "grpc-tonic")]
296    #[test]
297    fn test_default_tonic_endpoint() {
298        let exporter_builder = crate::new_exporter().tonic();
299
300        assert_eq!(exporter_builder.exporter_config.endpoint, "");
301    }
302
303    #[test]
304    fn test_default_protocol() {
305        #[cfg(all(
306            feature = "http-json",
307            not(any(feature = "grpc-tonic", feature = "http-proto"))
308        ))]
309        {
310            assert_eq!(
311                crate::exporter::default_protocol(),
312                crate::Protocol::HttpJson
313            );
314        }
315
316        #[cfg(all(
317            feature = "http-proto",
318            not(any(feature = "grpc-tonic", feature = "http-json"))
319        ))]
320        {
321            assert_eq!(
322                crate::exporter::default_protocol(),
323                crate::Protocol::HttpBinary
324            );
325        }
326
327        #[cfg(all(
328            feature = "grpc-tonic",
329            not(any(feature = "http-proto", feature = "http-json"))
330        ))]
331        {
332            assert_eq!(crate::exporter::default_protocol(), crate::Protocol::Grpc);
333        }
334    }
335
336    #[test]
337    fn test_url_decode() {
338        let test_cases = vec![
339            // Format: (encoded, expected_decoded)
340            ("v%201", Some("v 1")),
341            ("v 1", Some("v 1")),
342            ("%C3%B6%C3%A0%C2%A7%C3%96abcd%C3%84", Some("öà§ÖabcdÄ")),
343            ("v%XX1", None),
344        ];
345
346        for (encoded, expected_decoded) in test_cases {
347            assert_eq!(
348                super::url_decode(encoded),
349                expected_decoded.map(|v| v.to_string()),
350            )
351        }
352    }
353
354    #[test]
355    fn test_parse_header_string() {
356        let test_cases = vec![
357            // Format: (input_str, expected_headers)
358            ("k1=v1", vec![("k1", "v1")]),
359            ("k1=v1,k2=v2", vec![("k1", "v1"), ("k2", "v2")]),
360            ("k1=v1=10,k2,k3", vec![("k1", "v1=10")]),
361            ("k1=v1,,,k2,k3=10", vec![("k1", "v1"), ("k3", "10")]),
362        ];
363
364        for (input_str, expected_headers) in test_cases {
365            assert_eq!(
366                super::parse_header_string(input_str).collect::<Vec<_>>(),
367                expected_headers
368                    .into_iter()
369                    .map(|(k, v)| (k, v.to_string()))
370                    .collect::<Vec<_>>(),
371            )
372        }
373    }
374
375    #[test]
376    fn test_parse_header_key_value_string() {
377        let test_cases = vec![
378            // Format: (input_str, expected_header)
379            ("k1=v1", Some(("k1", "v1"))),
380            (
381                "Authentication=Basic AAA",
382                Some(("Authentication", "Basic AAA")),
383            ),
384            (
385                "Authentication=Basic%20AAA",
386                Some(("Authentication", "Basic AAA")),
387            ),
388            ("k1=%XX", Some(("k1", "%XX"))),
389            ("", None),
390            ("=v1", None),
391            ("k1=", None),
392        ];
393
394        for (input_str, expected_headers) in test_cases {
395            assert_eq!(
396                super::parse_header_key_value_string(input_str),
397                expected_headers.map(|(k, v)| (k, v.to_string())),
398            )
399        }
400    }
401}