1use opentelemetry_otlp::{
8 ExportConfig, ExporterBuildError, Protocol, SpanExporter, WithExportConfig,
9 WithHttpConfig,
10};
11use opentelemetry_sdk::{
12 Resource,
13 trace::{Sampler, SdkTracerProvider as TracerProvider, TracerProviderBuilder},
14};
15use std::{collections::HashMap, num::ParseIntError, str::FromStr, time::Duration};
16
17pub use crate::filter::read_tracing_level_from_env as read_otel_log_level_from_env;
18use crate::util;
19
20#[derive(thiserror::Error, Debug)]
25pub enum InitTracerError {
26 #[error("unsupported protocol {0:?} form env")]
32 UnsupportedEnvProtocol(String),
33
34 #[error("invalid timeout {0:?} form env: {1}")]
39 InvalidEnvTimeout(String, #[source] ParseIntError),
40
41 #[error(transparent)]
46 ExporterBuildError(#[from] ExporterBuildError),
47}
48
49#[must_use]
73pub fn identity(v: TracerProviderBuilder) -> TracerProviderBuilder {
74 v
75}
76
77pub fn init_tracer<F>(
114 resource: Resource,
115 transform: F,
116) -> Result<TracerProvider, InitTracerError>
117where
118 F: FnOnce(TracerProviderBuilder) -> TracerProviderBuilder,
119{
120 let (maybe_protocol, maybe_endpoint, maybe_timeout) = read_export_config_from_env();
121 let export_config = infer_export_config(
122 maybe_protocol.as_deref(),
123 maybe_endpoint.as_deref(),
124 maybe_timeout.as_deref(),
125 )?;
126 tracing::debug!(target: "otel::setup", export_config = format!("{export_config:?}"));
127 let exporter: SpanExporter = match export_config.protocol {
128 Protocol::HttpBinary => SpanExporter::builder()
129 .with_http()
130 .with_headers(read_headers_from_env())
131 .with_export_config(export_config)
132 .build()?,
133 Protocol::Grpc => SpanExporter::builder()
134 .with_tonic()
135 .with_export_config(export_config)
136 .build()?,
137 Protocol::HttpJson => unreachable!("HttpJson protocol is not supported"),
138 };
139
140 let tracer_provider_builder = TracerProvider::builder()
141 .with_batch_exporter(exporter)
142 .with_resource(resource)
143 .with_sampler(read_sampler_from_env());
144
145 Ok(transform(tracer_provider_builder).build())
146}
147
148fn parse_headers(val: &str) -> impl Iterator<Item = (String, String)> + '_ {
150 val.split(',').filter_map(|kv| {
151 kv.split_once('=')
152 .map(|(k, v)| (k.to_owned(), v.to_owned()))
153 })
154}
155fn read_headers_from_env() -> HashMap<String, String> {
156 let mut headers = HashMap::new();
157 headers.extend(parse_headers(
158 &util::env_var("OTEL_EXPORTER_OTLP_HEADERS").unwrap_or_default(),
159 ));
160 headers.extend(parse_headers(
161 &util::env_var("OTEL_EXPORTER_OTLP_TRACES_HEADERS").unwrap_or_default(),
162 ));
163 headers
164}
165fn read_export_config_from_env() -> (Option<String>, Option<String>, Option<String>) {
166 let maybe_endpoint = util::env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
167 .or_else(|| util::env_var("OTEL_EXPORTER_OTLP_ENDPOINT"));
168 let maybe_protocol = util::env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")
169 .or_else(|| util::env_var("OTEL_EXPORTER_OTLP_PROTOCOL"));
170 let maybe_timeout = util::env_var("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT")
171 .or_else(|| util::env_var("OTEL_EXPORTER_OTLP_TIMEOUT"));
172 (maybe_protocol, maybe_endpoint, maybe_timeout)
173}
174
175fn read_sampler_from_env() -> Sampler {
178 let mut name = util::env_var("OTEL_TRACES_SAMPLER")
179 .unwrap_or_default()
180 .to_lowercase();
181 let v = match name.as_str() {
182 "always_on" => Sampler::AlwaysOn,
183 "always_off" => Sampler::AlwaysOff,
184 "traceidratio" => Sampler::TraceIdRatioBased(read_sampler_arg_from_env(1f64)),
185 "parentbased_always_on" => Sampler::ParentBased(Box::new(Sampler::AlwaysOn)),
186 "parentbased_always_off" => Sampler::ParentBased(Box::new(Sampler::AlwaysOff)),
187 "parentbased_traceidratio" => Sampler::ParentBased(Box::new(
188 Sampler::TraceIdRatioBased(read_sampler_arg_from_env(1f64)),
189 )),
190 "jaeger_remote" => todo!("unsupported: OTEL_TRACES_SAMPLER='jaeger_remote'"),
191 "xray" => todo!("unsupported: OTEL_TRACES_SAMPLER='xray'"),
192 _ => {
193 name = "parentbased_always_on".to_string();
194 Sampler::ParentBased(Box::new(Sampler::AlwaysOn))
195 }
196 };
197 tracing::debug!(target: "otel::setup", OTEL_TRACES_SAMPLER = ?name);
198 v
199}
200
201fn read_sampler_arg_from_env<T>(default: T) -> T
202where
203 T: FromStr + Copy + std::fmt::Debug,
204{
205 let v = util::env_var("OTEL_TRACES_SAMPLER_ARG")
207 .map_or(default, |s| T::from_str(&s).unwrap_or(default));
208 tracing::debug!(target: "otel::setup", OTEL_TRACES_SAMPLER_ARG = ?v);
209 v
210}
211
212fn infer_export_config(
213 maybe_protocol: Option<&str>,
214 maybe_endpoint: Option<&str>,
215 maybe_timeout: Option<&str>,
216) -> Result<ExportConfig, InitTracerError> {
217 let protocol = match maybe_protocol {
218 Some("grpc") => Protocol::Grpc,
219 Some("http") | Some("http/protobuf") => Protocol::HttpBinary,
220 Some(other) => {
221 return Err(InitTracerError::UnsupportedEnvProtocol(other.to_owned()));
222 }
223 None => match maybe_endpoint {
224 Some(e) if e.contains(":4317") => Protocol::Grpc,
225 _ => Protocol::HttpBinary,
226 },
227 };
228
229 let timeout = maybe_timeout
230 .map(|millis| {
231 millis
232 .parse::<u64>()
233 .map_err(|err| InitTracerError::InvalidEnvTimeout(millis.to_owned(), err))
234 })
235 .transpose()?
236 .map(Duration::from_millis);
237
238 Ok(ExportConfig {
239 endpoint: maybe_endpoint.map(ToOwned::to_owned),
240 protocol,
241 timeout,
242 })
243}
244
245#[cfg(test)]
246mod tests {
247 use assert2::{assert, let_assert};
248 use rstest::rstest;
249
250 use super::*;
251 use Protocol::*;
252
253 #[rstest]
254 #[case(None, None, None, HttpBinary, None, None)]
255 #[case(Some("http/protobuf"), None, None, HttpBinary, None, None)]
256 #[case(Some("http"), None, None, HttpBinary, None, None)]
257 #[case(Some("grpc"), None, None, Grpc, None, None)]
258 #[case(
259 None,
260 Some("http://localhost:4317"),
261 None,
262 Grpc,
263 Some("http://localhost:4317"),
264 None
265 )]
266 #[case(
267 Some("http/protobuf"),
268 Some("http://localhost:4318"),
269 None,
270 HttpBinary,
271 Some("http://localhost:4318"),
272 None
273 )]
274 #[case(
275 Some("http/protobuf"),
276 Some("https://examples.com:4318"),
277 None,
278 HttpBinary,
279 Some("https://examples.com:4318"),
280 None
281 )]
282 #[case(
283 Some("http/protobuf"),
284 Some("https://examples.com:4317"),
285 Some("12345"),
286 HttpBinary,
287 Some("https://examples.com:4317"),
288 Some(Duration::from_millis(12345))
289 )]
290 fn test_infer_export_config(
291 #[case] traces_protocol: Option<&str>,
292 #[case] traces_endpoint: Option<&str>,
293 #[case] traces_timeout: Option<&str>,
294 #[case] expected_protocol: Protocol,
295 #[case] expected_endpoint: Option<&str>,
296 #[case] expected_timeout: Option<Duration>,
297 ) {
298 let ExportConfig {
299 protocol,
300 endpoint,
301 timeout,
302 } = infer_export_config(traces_protocol, traces_endpoint, traces_timeout)
303 .unwrap();
304
305 assert!(protocol == expected_protocol);
306 assert!(endpoint.as_deref() == expected_endpoint);
307 assert!(timeout == expected_timeout);
308 }
309
310 #[rstest]
311 #[case(Some("tonic"), None, r#"unsupported protocol "tonic" form env"#)]
312 #[case(
313 Some("http/protobuf"),
314 Some("-1"),
315 r#"invalid timeout "-1" form env: invalid digit found in string"#
316 )]
317 fn test_infer_export_config_error(
318 #[case] traces_protocol: Option<&str>,
319 #[case] traces_timeout: Option<&str>,
320 #[case] expected_error: &str,
321 ) {
322 let result = infer_export_config(traces_protocol, None, traces_timeout);
323
324 let_assert!(Err(err) = result);
325
326 assert!(format!("{}", err) == expected_error);
327 }
328}