1use opentelemetry_otlp::{
8 ExporterBuildError, Protocol, SpanExporter, WithExportConfig, WithHttpConfig,
9};
10use opentelemetry_sdk::{
11 Resource,
12 trace::{Sampler, SdkTracerProvider as TracerProvider, TracerProviderBuilder},
13};
14use std::{collections::HashMap, num::ParseIntError, str::FromStr, time::Duration};
15
16pub use crate::filter::read_tracing_level_from_env as read_otel_log_level_from_env;
17use crate::util;
18
19#[derive(Debug)]
20struct InferredExportConfig {
21 endpoint: Option<String>,
22 protocol: Protocol,
23 timeout: Option<Duration>,
24}
25
26trait WithExportConfigExt: WithExportConfig {
27 fn with_export_config(self, cfg: InferredExportConfig) -> Self;
28}
29
30impl<B: WithExportConfig> WithExportConfigExt for B {
31 fn with_export_config(self, cfg: InferredExportConfig) -> Self {
32 let this = self.with_protocol(cfg.protocol);
33 let this = match cfg.endpoint {
34 Some(endpoint) => this.with_endpoint(endpoint),
35 None => this,
36 };
37 match cfg.timeout {
38 Some(timeout) => this.with_timeout(timeout),
39 None => this,
40 }
41 }
42}
43
44#[derive(thiserror::Error, Debug)]
49pub enum InitTracerError {
50 #[error("unsupported protocol {0:?} form env")]
56 UnsupportedEnvProtocol(String),
57
58 #[error("invalid timeout {0:?} form env: {1}")]
63 InvalidEnvTimeout(String, #[source] ParseIntError),
64
65 #[error(transparent)]
70 ExporterBuildError(#[from] ExporterBuildError),
71}
72
73#[must_use]
97pub fn identity(v: TracerProviderBuilder) -> TracerProviderBuilder {
98 v
99}
100
101pub fn init_tracer<F>(
138 resource: Resource,
139 transform: F,
140) -> Result<TracerProvider, InitTracerError>
141where
142 F: FnOnce(TracerProviderBuilder) -> TracerProviderBuilder,
143{
144 let (maybe_protocol, maybe_endpoint, maybe_timeout) = read_export_config_from_env();
145 let export_config = infer_export_config(
146 maybe_protocol.as_deref(),
147 maybe_endpoint.as_deref(),
148 maybe_timeout.as_deref(),
149 )?;
150 tracing::debug!(target: "otel::setup", ?export_config);
151 let exporter: SpanExporter = match export_config.protocol {
152 Protocol::HttpBinary => SpanExporter::builder()
153 .with_http()
154 .with_headers(read_headers_from_env())
155 .with_export_config(export_config)
156 .build()?,
157 Protocol::Grpc => SpanExporter::builder()
158 .with_tonic()
159 .with_export_config(export_config)
160 .build()?,
161 };
162
163 let tracer_provider_builder = TracerProvider::builder()
164 .with_batch_exporter(exporter)
165 .with_resource(resource)
166 .with_sampler(read_sampler_from_env());
167
168 Ok(transform(tracer_provider_builder).build())
169}
170
171fn parse_headers(val: &str) -> impl Iterator<Item = (String, String)> + '_ {
173 val.split(',').filter_map(|kv| {
174 kv.split_once('=')
175 .map(|(k, v)| (k.to_owned(), v.to_owned()))
176 })
177}
178fn read_headers_from_env() -> HashMap<String, String> {
179 let mut headers = HashMap::new();
180 headers.extend(parse_headers(
181 &util::env_var("OTEL_EXPORTER_OTLP_HEADERS").unwrap_or_default(),
182 ));
183 headers.extend(parse_headers(
184 &util::env_var("OTEL_EXPORTER_OTLP_TRACES_HEADERS").unwrap_or_default(),
185 ));
186 headers
187}
188fn read_export_config_from_env() -> (Option<String>, Option<String>, Option<String>) {
189 let maybe_endpoint = util::env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
190 .or_else(|| util::env_var("OTEL_EXPORTER_OTLP_ENDPOINT"));
191 let maybe_protocol = util::env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")
192 .or_else(|| util::env_var("OTEL_EXPORTER_OTLP_PROTOCOL"));
193 let maybe_timeout = util::env_var("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT")
194 .or_else(|| util::env_var("OTEL_EXPORTER_OTLP_TIMEOUT"));
195 (maybe_protocol, maybe_endpoint, maybe_timeout)
196}
197
198fn read_sampler_from_env() -> Sampler {
201 let mut name = util::env_var("OTEL_TRACES_SAMPLER")
202 .unwrap_or_default()
203 .to_lowercase();
204 let v = match name.as_str() {
205 "always_on" => Sampler::AlwaysOn,
206 "always_off" => Sampler::AlwaysOff,
207 "traceidratio" => Sampler::TraceIdRatioBased(read_sampler_arg_from_env(1f64)),
208 "parentbased_always_on" => Sampler::ParentBased(Box::new(Sampler::AlwaysOn)),
209 "parentbased_always_off" => Sampler::ParentBased(Box::new(Sampler::AlwaysOff)),
210 "parentbased_traceidratio" => Sampler::ParentBased(Box::new(
211 Sampler::TraceIdRatioBased(read_sampler_arg_from_env(1f64)),
212 )),
213 "jaeger_remote" => todo!("unsupported: OTEL_TRACES_SAMPLER='jaeger_remote'"),
214 "xray" => todo!("unsupported: OTEL_TRACES_SAMPLER='xray'"),
215 _ => {
216 name = "parentbased_always_on".to_string();
217 Sampler::ParentBased(Box::new(Sampler::AlwaysOn))
218 }
219 };
220 tracing::debug!(target: "otel::setup", OTEL_TRACES_SAMPLER = ?name);
221 v
222}
223
224fn read_sampler_arg_from_env<T>(default: T) -> T
225where
226 T: FromStr + Copy + std::fmt::Debug,
227{
228 let v = util::env_var("OTEL_TRACES_SAMPLER_ARG")
230 .map_or(default, |s| T::from_str(&s).unwrap_or(default));
231 tracing::debug!(target: "otel::setup", OTEL_TRACES_SAMPLER_ARG = ?v);
232 v
233}
234
235fn infer_export_config(
236 maybe_protocol: Option<&str>,
237 maybe_endpoint: Option<&str>,
238 maybe_timeout: Option<&str>,
239) -> Result<InferredExportConfig, InitTracerError> {
240 let protocol = match maybe_protocol {
241 Some("grpc") => Protocol::Grpc,
242 Some("http") | Some("http/protobuf") => Protocol::HttpBinary,
243 Some(other) => {
244 return Err(InitTracerError::UnsupportedEnvProtocol(other.to_owned()));
245 }
246 None => match maybe_endpoint {
247 Some(e) if e.contains(":4317") => Protocol::Grpc,
248 _ => Protocol::HttpBinary,
249 },
250 };
251
252 let timeout = maybe_timeout
253 .map(|millis| {
254 millis
255 .parse::<u64>()
256 .map_err(|err| InitTracerError::InvalidEnvTimeout(millis.to_owned(), err))
257 })
258 .transpose()?
259 .map(Duration::from_millis);
260
261 Ok(InferredExportConfig {
262 endpoint: maybe_endpoint.map(ToOwned::to_owned),
263 protocol,
264 timeout,
265 })
266}
267
268#[cfg(test)]
269mod tests {
270 use assert2::assert;
271 use rstest::rstest;
272
273 use super::*;
274 use Protocol::*;
275
276 #[rstest]
277 #[case(None, None, None, HttpBinary, None, None)]
278 #[case(Some("http/protobuf"), None, None, HttpBinary, None, None)]
279 #[case(Some("http"), None, None, HttpBinary, None, None)]
280 #[case(Some("grpc"), None, None, Grpc, None, None)]
281 #[case(
282 None,
283 Some("http://localhost:4317"),
284 None,
285 Grpc,
286 Some("http://localhost:4317"),
287 None
288 )]
289 #[case(
290 Some("http/protobuf"),
291 Some("http://localhost:4318"),
292 None,
293 HttpBinary,
294 Some("http://localhost:4318"),
295 None
296 )]
297 #[case(
298 Some("http/protobuf"),
299 Some("https://examples.com:4318"),
300 None,
301 HttpBinary,
302 Some("https://examples.com:4318"),
303 None
304 )]
305 #[case(
306 Some("http/protobuf"),
307 Some("https://examples.com:4317"),
308 Some("12345"),
309 HttpBinary,
310 Some("https://examples.com:4317"),
311 Some(Duration::from_millis(12345))
312 )]
313 fn test_infer_export_config(
314 #[case] traces_protocol: Option<&str>,
315 #[case] traces_endpoint: Option<&str>,
316 #[case] traces_timeout: Option<&str>,
317 #[case] expected_protocol: Protocol,
318 #[case] expected_endpoint: Option<&str>,
319 #[case] expected_timeout: Option<Duration>,
320 ) {
321 let InferredExportConfig {
322 protocol,
323 endpoint,
324 timeout,
325 } = infer_export_config(traces_protocol, traces_endpoint, traces_timeout)
326 .unwrap();
327
328 assert!(protocol == expected_protocol);
329 assert!(endpoint.as_deref() == expected_endpoint);
330 assert!(timeout == expected_timeout);
331 }
332
333 #[rstest]
334 #[case(Some("tonic"), None, r#"unsupported protocol "tonic" form env"#)]
335 #[case(
336 Some("http/protobuf"),
337 Some("-1"),
338 r#"invalid timeout "-1" form env: invalid digit found in string"#
339 )]
340 fn test_infer_export_config_error(
341 #[case] traces_protocol: Option<&str>,
342 #[case] traces_timeout: Option<&str>,
343 #[case] expected_error: &str,
344 ) {
345 let result = infer_export_config(traces_protocol, None, traces_timeout);
346
347 assert!(let Err(err) = result);
348
349 assert!(format!("{}", err) == expected_error);
350 }
351}