telemetry_rust/
otlp.rs

1// Originally retired from davidB/tracing-opentelemetry-instrumentation-sdk
2// which is licensed under CC0 1.0 Universal
3// https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/blob/d3609ac2cc699d3a24fbf89754053cc8e938e3bf/LICENSE
4
5use 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
35// see https://opentelemetry.io/docs/reference/specification/protocol/exporter/
36pub 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
71/// turn a string of "k1=v1,k2=v2,..." into an iterator of (key, value) tuples
72fn 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
100/// see <https://opentelemetry.io/docs/reference/specification/sdk-environment-variables/#general-sdk-configuration>
101/// TODO log error and infered sampler
102fn 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    //TODO Log for invalid value (how to log)
131    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}