1use opentelemetry_otlp::{
6 ExportConfig, ExporterBuildError, Protocol, SpanExporter, WithExportConfig,
7 WithHttpConfig,
8};
9use opentelemetry_sdk::{
10 trace::{Sampler, SdkTracerProvider as TracerProvider, TracerProviderBuilder},
11 Resource,
12};
13use std::{collections::HashMap, num::ParseIntError, str::FromStr, time::Duration};
14
15pub use crate::filter::read_tracing_level_from_env as read_otel_log_level_from_env;
16use crate::util;
17
18#[derive(thiserror::Error, Debug)]
19pub enum InitTracerError {
20 #[error("unsupported protocol {0:?} form env")]
21 UnsupportedEnvProtocol(String),
22
23 #[error("invalid timeout {0:?} form env: {1}")]
24 InvalidEnvTimeout(String, #[source] ParseIntError),
25
26 #[error(transparent)]
27 ExporterBuildError(#[from] ExporterBuildError),
28}
29
30#[must_use]
31pub fn identity(v: TracerProviderBuilder) -> TracerProviderBuilder {
32 v
33}
34
35pub fn init_tracer<F>(
37 resource: Resource,
38 transform: F,
39) -> Result<TracerProvider, InitTracerError>
40where
41 F: FnOnce(TracerProviderBuilder) -> TracerProviderBuilder,
42{
43 let (maybe_protocol, maybe_endpoint, maybe_timeout) = read_export_config_from_env();
44 let export_config = infer_export_config(
45 maybe_protocol.as_deref(),
46 maybe_endpoint.as_deref(),
47 maybe_timeout.as_deref(),
48 )?;
49 tracing::debug!(target: "otel::setup", export_config = format!("{export_config:?}"));
50 let exporter: SpanExporter = match export_config.protocol {
51 Protocol::HttpBinary => SpanExporter::builder()
52 .with_http()
53 .with_headers(read_headers_from_env())
54 .with_export_config(export_config)
55 .build()?,
56 Protocol::Grpc => SpanExporter::builder()
57 .with_tonic()
58 .with_export_config(export_config)
59 .build()?,
60 Protocol::HttpJson => unreachable!("HttpJson protocol is not supported"),
61 };
62
63 let tracer_provider_builder = TracerProvider::builder()
64 .with_batch_exporter(exporter)
65 .with_resource(resource)
66 .with_sampler(read_sampler_from_env());
67
68 Ok(transform(tracer_provider_builder).build())
69}
70
71fn parse_headers(val: &str) -> impl Iterator<Item = (String, String)> + '_ {
73 val.split(',').filter_map(|kv| {
74 let s = kv
75 .split_once('=')
76 .map(|(k, v)| (k.to_owned(), v.to_owned()));
77 s
78 })
79}
80fn read_headers_from_env() -> HashMap<String, String> {
81 let mut headers = HashMap::new();
82 headers.extend(parse_headers(
83 &util::env_var("OTEL_EXPORTER_OTLP_HEADERS").unwrap_or_default(),
84 ));
85 headers.extend(parse_headers(
86 &util::env_var("OTEL_EXPORTER_OTLP_TRACES_HEADERS").unwrap_or_default(),
87 ));
88 headers
89}
90fn read_export_config_from_env() -> (Option<String>, Option<String>, Option<String>) {
91 let maybe_endpoint = util::env_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
92 .or_else(|| util::env_var("OTEL_EXPORTER_OTLP_ENDPOINT"));
93 let maybe_protocol = util::env_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")
94 .or_else(|| util::env_var("OTEL_EXPORTER_OTLP_PROTOCOL"));
95 let maybe_timeout = util::env_var("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT")
96 .or_else(|| util::env_var("OTEL_EXPORTER_OTLP_TIMEOUT"));
97 (maybe_protocol, maybe_endpoint, maybe_timeout)
98}
99
100fn read_sampler_from_env() -> Sampler {
103 let mut name = util::env_var("OTEL_TRACES_SAMPLER")
104 .unwrap_or_default()
105 .to_lowercase();
106 let v = match name.as_str() {
107 "always_on" => Sampler::AlwaysOn,
108 "always_off" => Sampler::AlwaysOff,
109 "traceidratio" => Sampler::TraceIdRatioBased(read_sampler_arg_from_env(1f64)),
110 "parentbased_always_on" => Sampler::ParentBased(Box::new(Sampler::AlwaysOn)),
111 "parentbased_always_off" => Sampler::ParentBased(Box::new(Sampler::AlwaysOff)),
112 "parentbased_traceidratio" => Sampler::ParentBased(Box::new(
113 Sampler::TraceIdRatioBased(read_sampler_arg_from_env(1f64)),
114 )),
115 "jaeger_remote" => todo!("unsupported: OTEL_TRACES_SAMPLER='jaeger_remote'"),
116 "xray" => todo!("unsupported: OTEL_TRACES_SAMPLER='xray'"),
117 _ => {
118 name = "parentbased_always_on".to_string();
119 Sampler::ParentBased(Box::new(Sampler::AlwaysOn))
120 }
121 };
122 tracing::debug!(target: "otel::setup", OTEL_TRACES_SAMPLER = ?name);
123 v
124}
125
126fn read_sampler_arg_from_env<T>(default: T) -> T
127where
128 T: FromStr + Copy + std::fmt::Debug,
129{
130 let v = util::env_var("OTEL_TRACES_SAMPLER_ARG")
132 .map_or(default, |s| T::from_str(&s).unwrap_or(default));
133 tracing::debug!(target: "otel::setup", OTEL_TRACES_SAMPLER_ARG = ?v);
134 v
135}
136
137fn infer_export_config(
138 maybe_protocol: Option<&str>,
139 maybe_endpoint: Option<&str>,
140 maybe_timeout: Option<&str>,
141) -> Result<ExportConfig, InitTracerError> {
142 let protocol = match maybe_protocol {
143 Some("grpc") => Protocol::Grpc,
144 Some("http") | Some("http/protobuf") => Protocol::HttpBinary,
145 Some(other) => {
146 return Err(InitTracerError::UnsupportedEnvProtocol(other.to_owned()))
147 }
148 None => match maybe_endpoint {
149 Some(e) if e.contains(":4317") => Protocol::Grpc,
150 _ => Protocol::HttpBinary,
151 },
152 };
153
154 let timeout = maybe_timeout
155 .map(|millis| {
156 millis
157 .parse::<u64>()
158 .map_err(|err| InitTracerError::InvalidEnvTimeout(millis.to_owned(), err))
159 })
160 .transpose()?
161 .map(Duration::from_millis);
162
163 Ok(ExportConfig {
164 endpoint: maybe_endpoint.map(ToOwned::to_owned),
165 protocol,
166 timeout,
167 })
168}
169
170#[cfg(test)]
171mod tests {
172 use assert2::{assert, let_assert};
173 use rstest::rstest;
174
175 use super::*;
176 use Protocol::*;
177
178 #[rstest]
179 #[case(None, None, None, HttpBinary, None, None)]
180 #[case(Some("http/protobuf"), None, None, HttpBinary, None, None)]
181 #[case(Some("http"), None, None, HttpBinary, None, None)]
182 #[case(Some("grpc"), None, None, Grpc, None, None)]
183 #[case(
184 None,
185 Some("http://localhost:4317"),
186 None,
187 Grpc,
188 Some("http://localhost:4317"),
189 None
190 )]
191 #[case(
192 Some("http/protobuf"),
193 Some("http://localhost:4318"),
194 None,
195 HttpBinary,
196 Some("http://localhost:4318"),
197 None
198 )]
199 #[case(
200 Some("http/protobuf"),
201 Some("https://examples.com:4318"),
202 None,
203 HttpBinary,
204 Some("https://examples.com:4318"),
205 None
206 )]
207 #[case(
208 Some("http/protobuf"),
209 Some("https://examples.com:4317"),
210 Some("12345"),
211 HttpBinary,
212 Some("https://examples.com:4317"),
213 Some(Duration::from_millis(12345))
214 )]
215 fn test_infer_export_config(
216 #[case] traces_protocol: Option<&str>,
217 #[case] traces_endpoint: Option<&str>,
218 #[case] traces_timeout: Option<&str>,
219 #[case] expected_protocol: Protocol,
220 #[case] expected_endpoint: Option<&str>,
221 #[case] expected_timeout: Option<Duration>,
222 ) {
223 let ExportConfig {
224 protocol,
225 endpoint,
226 timeout,
227 } = infer_export_config(traces_protocol, traces_endpoint, traces_timeout)
228 .unwrap();
229
230 assert!(protocol == expected_protocol);
231 assert!(endpoint.as_deref() == expected_endpoint);
232 assert!(timeout == expected_timeout);
233 }
234
235 #[rstest]
236 #[case(Some("tonic"), None, r#"unsupported protocol "tonic" form env"#)]
237 #[case(
238 Some("http/protobuf"),
239 Some("-1"),
240 r#"invalid timeout "-1" form env: invalid digit found in string"#
241 )]
242 fn test_infer_export_config_error(
243 #[case] traces_protocol: Option<&str>,
244 #[case] traces_timeout: Option<&str>,
245 #[case] expected_error: &str,
246 ) {
247 let result = infer_export_config(traces_protocol, None, traces_timeout);
248
249 let_assert!(Err(err) = result);
250
251 assert!(format!("{}", err) == expected_error);
252 }
253}