Skip to main content

otel_init/
macros.rs

1//! Macros for building OpenTelemetry exporters.
2//!
3//! This module provides macros and utilities for building OTLP exporters
4//! with protocol detection from environment variables.
5
6use opentelemetry_otlp::{OTEL_EXPORTER_OTLP_PROTOCOL, Protocol};
7
8/// Parse an OTLP protocol value.
9///
10/// # Arguments
11///
12/// * `value` - The protocol value to parse.
13///
14/// # Returns
15///
16/// The parsed protocol, or `None` if the value is invalid.
17fn parse_protocol(value: &str) -> Option<Protocol> {
18    match value.trim().to_ascii_lowercase().as_str() {
19        "grpc" => Some(Protocol::Grpc),
20        "http/protobuf" | "http/proto" => Some(Protocol::HttpBinary),
21        "http/json" => Some(Protocol::HttpJson),
22        _ => None,
23    }
24}
25
26/// Get an OTLP protocol from an environment variable.
27///
28/// # Arguments
29///
30/// * `key` - The environment variable key.
31///
32/// # Returns
33///
34/// The parsed protocol, or `None` if the variable is unset or invalid.
35fn protocol_from_env(key: &str) -> Option<Protocol> {
36    std::env::var(key)
37        .ok()
38        .and_then(|value| parse_protocol(&value))
39}
40
41/// Resolve the OTLP protocol for a signal, with a global fallback.
42///
43/// # Arguments
44///
45/// * `signal_env` - The signal-specific environment variable key.
46///
47/// # Returns
48///
49/// The resolved protocol, defaulting to gRPC.
50pub fn protocol_for_signal(signal_env: &str) -> Protocol {
51    protocol_from_env(signal_env)
52        .or_else(|| protocol_from_env(OTEL_EXPORTER_OTLP_PROTOCOL))
53        .unwrap_or(Protocol::Grpc)
54}
55
56/// Build the exporter based on the configured protocol.
57///
58/// This macro creates an OTLP exporter using either gRPC (tonic) or HTTP transport
59/// based on the protocol configuration from environment variables.
60///
61/// # Arguments
62///
63/// * `$builder` - The exporter builder (e.g., `SpanExporter::builder()`)
64/// * `$protocol_env` - The signal-specific environment variable for protocol override
65/// * `$msg` - Error message prefix for build failures
66/// * `$config` - Optional closure to configure the builder before building
67///
68/// # Example
69///
70/// ```ignore
71/// use crate::macros::build_exporter;
72///
73/// let exporter = build_exporter!(
74///     opentelemetry_otlp::SpanExporter::builder(),
75///     "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL",
76///     "Failed to build span exporter"
77/// )?;
78/// ```
79macro_rules! build_exporter {
80    ($builder:expr, $protocol_env:expr, $msg:literal) => {
81        build_exporter!($builder, $protocol_env, $msg, |b| b)
82    };
83    ($builder:expr, $protocol_env:expr, $msg:literal, |$binder:ident| $config:expr) => {{
84        use ::anyhow::Context as _;
85        use ::opentelemetry_otlp::Protocol;
86        use ::opentelemetry_otlp::WithExportConfig as _;
87
88        let protocol = $crate::macros::protocol_for_signal($protocol_env);
89        match protocol {
90            Protocol::Grpc => {
91                let $binder = $builder.with_tonic();
92                let builder = $config;
93                builder.build().context(format!("{} (gRPC)", $msg))
94            }
95            _ => {
96                let $binder = $builder.with_http().with_protocol(protocol);
97                let builder = $config;
98                builder.build().context(format!("{} (HTTP)", $msg))
99            }
100        }
101    }};
102}
103
104pub(crate) use build_exporter;
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_parse_protocol() {
112        // Valid protocols - lowercase
113        assert_eq!(parse_protocol("grpc"), Some(Protocol::Grpc));
114        assert_eq!(parse_protocol("http/protobuf"), Some(Protocol::HttpBinary));
115        assert_eq!(parse_protocol("http/proto"), Some(Protocol::HttpBinary));
116        assert_eq!(parse_protocol("http/json"), Some(Protocol::HttpJson));
117
118        // Valid protocols - uppercase
119        assert_eq!(parse_protocol("GRPC"), Some(Protocol::Grpc));
120        assert_eq!(parse_protocol("HTTP/PROTOBUF"), Some(Protocol::HttpBinary));
121        assert_eq!(parse_protocol("HTTP/PROTO"), Some(Protocol::HttpBinary));
122        assert_eq!(parse_protocol("HTTP/JSON"), Some(Protocol::HttpJson));
123
124        // Valid protocols - mixed case
125        assert_eq!(parse_protocol("Grpc"), Some(Protocol::Grpc));
126        assert_eq!(parse_protocol("Http/Protobuf"), Some(Protocol::HttpBinary));
127        assert_eq!(parse_protocol("Http/Proto"), Some(Protocol::HttpBinary));
128        assert_eq!(parse_protocol("Http/Json"), Some(Protocol::HttpJson));
129
130        // Valid protocols - with whitespace
131        assert_eq!(parse_protocol(" grpc "), Some(Protocol::Grpc));
132        assert_eq!(
133            parse_protocol("  http/protobuf  "),
134            Some(Protocol::HttpBinary)
135        );
136        assert_eq!(parse_protocol("\thttp/proto\n"), Some(Protocol::HttpBinary));
137        assert_eq!(parse_protocol(" http/json "), Some(Protocol::HttpJson));
138
139        // Invalid protocols
140        assert_eq!(parse_protocol("invalid"), None);
141        assert_eq!(parse_protocol(""), None);
142        assert_eq!(parse_protocol("http"), None);
143        assert_eq!(parse_protocol("grpc/http"), None);
144        assert_eq!(parse_protocol("json"), None);
145    }
146
147    #[test]
148    fn test_protocol_from_env() {
149        // Test with unset environment variable
150        assert_eq!(protocol_from_env("NONEXISTENT_VAR_12345"), None);
151
152        // Test with valid protocol values
153        unsafe {
154            std::env::set_var("TEST_PROTOCOL_GRPC", "grpc");
155        }
156        assert_eq!(
157            protocol_from_env("TEST_PROTOCOL_GRPC"),
158            Some(Protocol::Grpc)
159        );
160        unsafe {
161            std::env::remove_var("TEST_PROTOCOL_GRPC");
162        }
163
164        unsafe {
165            std::env::set_var("TEST_PROTOCOL_HTTP_BINARY", "http/protobuf");
166        }
167        assert_eq!(
168            protocol_from_env("TEST_PROTOCOL_HTTP_BINARY"),
169            Some(Protocol::HttpBinary)
170        );
171        unsafe {
172            std::env::remove_var("TEST_PROTOCOL_HTTP_BINARY");
173        }
174
175        unsafe {
176            std::env::set_var("TEST_PROTOCOL_HTTP_JSON", "http/json");
177        }
178        assert_eq!(
179            protocol_from_env("TEST_PROTOCOL_HTTP_JSON"),
180            Some(Protocol::HttpJson)
181        );
182        unsafe {
183            std::env::remove_var("TEST_PROTOCOL_HTTP_JSON");
184        }
185
186        // Test with invalid protocol value
187        unsafe {
188            std::env::set_var("TEST_PROTOCOL_INVALID", "invalid");
189        }
190        assert_eq!(protocol_from_env("TEST_PROTOCOL_INVALID"), None);
191        unsafe {
192            std::env::remove_var("TEST_PROTOCOL_INVALID");
193        }
194
195        // Test with whitespace (should be trimmed)
196        unsafe {
197            std::env::set_var("TEST_PROTOCOL_WHITESPACE", " grpc ");
198        }
199        assert_eq!(
200            protocol_from_env("TEST_PROTOCOL_WHITESPACE"),
201            Some(Protocol::Grpc)
202        );
203        unsafe {
204            std::env::remove_var("TEST_PROTOCOL_WHITESPACE");
205        }
206    }
207
208    #[test]
209    fn test_protocol_for_signal() {
210        // Test default fallback to gRPC when no env vars are set
211        // Clean up any existing env vars first
212        unsafe {
213            std::env::remove_var("TEST_SIGNAL_SPECIFIC");
214            std::env::remove_var(OTEL_EXPORTER_OTLP_PROTOCOL);
215        }
216        assert_eq!(protocol_for_signal("TEST_SIGNAL_SPECIFIC"), Protocol::Grpc);
217
218        // Test signal-specific override
219        unsafe {
220            std::env::set_var("TEST_SIGNAL_SPECIFIC", "http/json");
221        }
222        assert_eq!(
223            protocol_for_signal("TEST_SIGNAL_SPECIFIC"),
224            Protocol::HttpJson
225        );
226        unsafe {
227            std::env::remove_var("TEST_SIGNAL_SPECIFIC");
228        }
229
230        // Test global fallback when signal-specific is not set
231        unsafe {
232            std::env::set_var(OTEL_EXPORTER_OTLP_PROTOCOL, "http/protobuf");
233        }
234        assert_eq!(
235            protocol_for_signal("TEST_SIGNAL_SPECIFIC"),
236            Protocol::HttpBinary
237        );
238        unsafe {
239            std::env::remove_var(OTEL_EXPORTER_OTLP_PROTOCOL);
240        }
241
242        // Test signal-specific takes precedence over global
243        unsafe {
244            std::env::set_var("TEST_SIGNAL_SPECIFIC", "http/json");
245            std::env::set_var(OTEL_EXPORTER_OTLP_PROTOCOL, "http/protobuf");
246        }
247        assert_eq!(
248            protocol_for_signal("TEST_SIGNAL_SPECIFIC"),
249            Protocol::HttpJson
250        );
251        unsafe {
252            std::env::remove_var("TEST_SIGNAL_SPECIFIC");
253            std::env::remove_var(OTEL_EXPORTER_OTLP_PROTOCOL);
254        }
255
256        // Test invalid signal-specific falls back to global
257        unsafe {
258            std::env::set_var("TEST_SIGNAL_SPECIFIC", "invalid");
259            std::env::set_var(OTEL_EXPORTER_OTLP_PROTOCOL, "grpc");
260        }
261        assert_eq!(protocol_for_signal("TEST_SIGNAL_SPECIFIC"), Protocol::Grpc);
262        unsafe {
263            std::env::remove_var("TEST_SIGNAL_SPECIFIC");
264            std::env::remove_var(OTEL_EXPORTER_OTLP_PROTOCOL);
265        }
266
267        // Test invalid signal-specific and invalid global falls back to default
268        unsafe {
269            std::env::set_var("TEST_SIGNAL_SPECIFIC", "invalid");
270            std::env::set_var(OTEL_EXPORTER_OTLP_PROTOCOL, "also_invalid");
271        }
272        assert_eq!(protocol_for_signal("TEST_SIGNAL_SPECIFIC"), Protocol::Grpc);
273        unsafe {
274            std::env::remove_var("TEST_SIGNAL_SPECIFIC");
275            std::env::remove_var(OTEL_EXPORTER_OTLP_PROTOCOL);
276        }
277    }
278}