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: Option<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: None,
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    /// Compresses data using zstd.
102    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
126/// default protocol based on enabled features
127fn 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/// default user-agent headers
137#[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
147/// Provide access to the [ExportConfig] field within the exporter builders.
148pub trait HasExportConfig {
149    /// Return a mutable reference to the [ExportConfig] within the exporter builders.
150    fn export_config(&mut self) -> &mut ExportConfig;
151}
152
153/// Provide [ExportConfig] access to the [TonicExporterBuilder].
154#[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/// Provide [ExportConfig] access to the [HttpExporterBuilder].
162#[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
169/// Expose methods to override [ExportConfig].
170///
171/// This trait will be implemented for every struct that implemented [`HasExportConfig`] trait.
172///
173/// ## Examples
174/// ```
175/// # #[cfg(all(feature = "trace", feature = "grpc-tonic"))]
176/// # {
177/// use crate::opentelemetry_otlp::WithExportConfig;
178/// let exporter_builder = opentelemetry_otlp::SpanExporter::builder()
179///     .with_tonic()
180///     .with_endpoint("http://localhost:7201");
181/// # }
182/// ```
183pub trait WithExportConfig {
184    /// Set the address of the OTLP collector. If not set or set to empty string, the default address is used.
185    fn with_endpoint<T: Into<String>>(self, endpoint: T) -> Self;
186    /// Set the protocol to use when communicating with the collector.
187    ///
188    /// Note that protocols that are not supported by exporters will be ignored. The exporter
189    /// will use default protocol in this case.
190    ///
191    /// ## Note
192    /// All exporters in this crate only support one protocol, thus choosing the protocol is an no-op at the moment.
193    fn with_protocol(self, protocol: Protocol) -> Self;
194    /// Set the timeout to the collector.
195    fn with_timeout(self, timeout: Duration) -> Self;
196    /// Set export config. This will override all previous configuration.
197    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            // Format: (encoded, expected_decoded)
346            ("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            // Format: (input_str, expected_headers)
364            ("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            // Format: (input_str, expected_header)
385            ("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}