Skip to main content

tracing_configuration/
otel.rs

1use std::{
2    collections::{BTreeMap, HashMap},
3    str::FromStr,
4    time::Duration,
5};
6
7use opentelemetry::{
8    metrics::MeterProvider as _, trace::TracerProvider as _, InstrumentationScopeBuilder, KeyValue,
9};
10use opentelemetry_otlp::{
11    Compression, ExporterBuildError, HasExportConfig, HasHttpConfig, HasTonicConfig,
12    MetricExporterBuilder, Protocol, SpanExporterBuilder, WithExportConfig, WithHttpConfig,
13    WithTonicConfig as _,
14};
15use opentelemetry_sdk::{
16    metrics::{MeterProviderBuilder, PeriodicReader, PeriodicReaderBuilder},
17    resource::ResourceBuilder,
18    trace::{SdkTracerProvider, TracerProviderBuilder},
19};
20#[cfg(feature = "schemars1")]
21use schemars::JsonSchema;
22use serde::{Deserialize, Serialize};
23use serde_with::*;
24use tracing_core::Subscriber;
25use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
26use tracing_subscriber::registry::LookupSpan;
27
28use crate::ParseError;
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
31#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
32#[serde(deny_unknown_fields, rename_all = "kebab-case")]
33pub struct Tracer {
34    #[serde(default)]
35    pub scope: InstrumentationScope,
36    #[serde(default)]
37    pub provider: TracerProvider,
38}
39
40impl Tracer {
41    pub fn build(
42        self,
43    ) -> Result<(opentelemetry_sdk::trace::Tracer, SdkTracerProvider), ExporterBuildError> {
44        let Self { provider, scope } = self;
45        provider.builder().map(|b| {
46            let p = b.build();
47            (p.tracer_with_scope(scope.builder().build()), p)
48        })
49    }
50
51    pub fn layer<S>(
52        self,
53    ) -> Result<
54        (
55            OpenTelemetryLayer<S, opentelemetry_sdk::trace::Tracer>,
56            SdkTracerProvider,
57        ),
58        ExporterBuildError,
59    >
60    where
61        S: Subscriber + for<'any> LookupSpan<'any>,
62    {
63        self.build()
64            .map(|(trc, p)| (OpenTelemetryLayer::new(trc), p))
65    }
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
69#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
70#[serde(deny_unknown_fields, rename_all = "kebab-case")]
71pub struct InstrumentationScope {
72    #[serde(default)]
73    pub name: String,
74    pub version: Option<String>,
75    pub schema_url: Option<String>,
76    #[serde(default)]
77    pub attributes: BTreeMap<String, Value>,
78}
79
80impl InstrumentationScope {
81    pub fn builder(self) -> InstrumentationScopeBuilder {
82        let Self {
83            name,
84            version,
85            schema_url,
86            attributes,
87        } = self;
88        let b = opentelemetry::InstrumentationScope::builder(name);
89        let b = apply(b, version, |b, v| b.with_version(v));
90        let b = apply(b, schema_url, |b, v| b.with_schema_url(v));
91        b.with_attributes(attributes.into_iter().map(|(k, v)| KeyValue::new(k, v)))
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
96#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
97#[serde(deny_unknown_fields, rename_all = "kebab-case")]
98pub struct TracerProvider {
99    #[serde(default)]
100    pub resource: Resource,
101    #[serde(default)]
102    pub sampler: Sampler,
103    #[serde(default)]
104    pub exporters: Vec<SpanExporter>,
105
106    pub max_events_per_span: Option<u32>,
107    pub max_attributes_per_span: Option<u32>,
108    pub max_links_per_span: Option<u32>,
109    pub max_attributes_per_event: Option<u32>,
110    pub max_attributes_per_link: Option<u32>,
111}
112
113impl TracerProvider {
114    pub fn builder(self) -> Result<TracerProviderBuilder, ExporterBuildError> {
115        let Self {
116            exporters,
117            sampler,
118            max_events_per_span,
119            max_attributes_per_span,
120            max_links_per_span,
121            max_attributes_per_event,
122            max_attributes_per_link,
123            resource,
124        } = self;
125
126        let b = TracerProviderBuilder::default();
127
128        let b = apply(b, max_events_per_span, |b, v| b.with_max_events_per_span(v));
129        let b = apply(b, max_attributes_per_span, |b, v| {
130            b.with_max_attributes_per_span(v)
131        });
132        let b = apply(b, max_links_per_span, |b, v| b.with_max_links_per_span(v));
133        let b = apply(b, max_attributes_per_event, |b, v| {
134            b.with_max_attributes_per_event(v)
135        });
136        let b = apply(b, max_attributes_per_link, |b, v| {
137            b.with_max_attributes_per_link(v)
138        });
139
140        let b = b.with_resource(resource.builder().build());
141
142        Ok(exporters
143            .into_iter()
144            .try_fold(
145                b,
146                |acc,
147                 SpanExporter {
148                     batch,
149                     transport,
150                     export,
151                 }| {
152                    match transport {
153                        Transport::Http(http_config) => export
154                            .apply(http_config.apply(SpanExporterBuilder::new().with_http()))
155                            .build(),
156                        Transport::Grpc(tonic_config) => export
157                            .apply(tonic_config.apply(SpanExporterBuilder::new().with_tonic()))
158                            .build(),
159                    }
160                    .map(|exporter| match batch.unwrap_or(true) {
161                        true => acc.with_batch_exporter(exporter),
162                        false => acc.with_simple_exporter(exporter),
163                    })
164                },
165            )?
166            .with_sampler(match sampler {
167                Sampler::Always => opentelemetry_sdk::trace::Sampler::AlwaysOn,
168                Sampler::Never => opentelemetry_sdk::trace::Sampler::AlwaysOff,
169                Sampler::Ratio(it) => opentelemetry_sdk::trace::Sampler::TraceIdRatioBased(it),
170            }))
171    }
172}
173
174#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, PartialOrd, Default)]
175#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
176#[serde(deny_unknown_fields, rename_all = "kebab-case")]
177pub struct Resource {
178    #[serde(default)]
179    pub attributes: BTreeMap<String, Value>,
180    pub schema_url: Option<String>,
181    pub detect: Option<bool>,
182}
183
184impl Resource {
185    pub fn builder(self) -> ResourceBuilder {
186        let Self {
187            attributes,
188            schema_url,
189            detect,
190        } = self;
191        apply(
192            match detect.unwrap_or(true) {
193                true => opentelemetry_sdk::Resource::builder(),
194                false => opentelemetry_sdk::Resource::builder_empty(),
195            },
196            schema_url,
197            |it, url| it.with_schema_url([], url),
198        )
199        .with_attributes(attributes.into_iter().map(|(k, v)| KeyValue::new(k, v)))
200    }
201}
202
203#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
204#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
205#[serde(untagged)]
206pub enum Value {
207    Bool(bool),
208    I64(i64),
209    F64(f64),
210    String(String),
211    Array(Array),
212}
213
214impl From<Value> for opentelemetry::Value {
215    fn from(value: Value) -> Self {
216        match value {
217            Value::Bool(it) => Self::Bool(it),
218            Value::I64(it) => Self::I64(it),
219            Value::F64(it) => Self::F64(it),
220            Value::String(it) => Self::String(it.into()),
221            Value::Array(it) => Self::Array(it.into()),
222        }
223    }
224}
225
226#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
227#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
228#[serde(untagged)]
229pub enum Array {
230    Bool(Vec<bool>),
231    I64(Vec<i64>),
232    F64(Vec<f64>),
233    String(Vec<String>),
234}
235
236impl From<Array> for opentelemetry::Array {
237    fn from(value: Array) -> Self {
238        match value {
239            Array::Bool(items) => Self::Bool(items),
240            Array::I64(items) => Self::I64(items),
241            Array::F64(items) => Self::F64(items),
242            Array::String(items) => Self::String(items.into_iter().map(Into::into).collect()),
243        }
244    }
245}
246
247#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize, Default)]
248#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
249#[serde(deny_unknown_fields, rename_all = "kebab-case")]
250pub enum Sampler {
251    #[default]
252    Always,
253    Never,
254    Ratio(f64),
255}
256
257impl Sampler {
258    const PARSE_ERROR: &str = "Expected one of `always`, `never` or `ratio=<float>`";
259}
260
261impl FromStr for Sampler {
262    type Err = ParseError;
263    fn from_str(s: &str) -> Result<Self, Self::Err> {
264        Ok(match s {
265            "always" => Self::Always,
266            "never" => Self::Never,
267            _ => match s.strip_prefix("ratio=").and_then(|s| s.parse().ok()) {
268                Some(it) => Self::Ratio(it),
269                None => return Err(ParseError(Self::PARSE_ERROR)),
270            },
271        })
272    }
273}
274
275#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)]
276#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
277#[serde(deny_unknown_fields, rename_all = "kebab-case")]
278pub struct SpanExporter {
279    pub transport: Transport,
280    #[serde(default)]
281    pub export: ExportConfig,
282    pub batch: Option<bool>,
283}
284
285#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)]
286#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
287#[serde(deny_unknown_fields, rename_all = "kebab-case")]
288pub enum Transport {
289    Http(HttpConfig),
290    Grpc(TonicConfig),
291}
292
293#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq, Default)]
294#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
295#[serde(deny_unknown_fields, rename_all = "kebab-case")]
296pub struct HttpConfig {
297    #[serde(default, with = "As::<Option<FromInto<_Compression>>>")]
298    #[cfg_attr(feature = "schemars1", schemars(with = "Option<_Compression>"))]
299    pub compression: Option<Compression>,
300    #[serde(default)]
301    pub headers: HashMap<String, String>,
302}
303
304impl HttpConfig {
305    pub fn apply<T: HasHttpConfig>(self, to: T) -> T {
306        let Self {
307            compression,
308            headers,
309        } = self;
310        match compression {
311            Some(it) => to.with_compression(it),
312            None => to,
313        }
314        .with_headers(headers)
315    }
316}
317
318#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq, Default)]
319#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
320#[serde(deny_unknown_fields, rename_all = "kebab-case")]
321pub struct TonicConfig {
322    #[serde(default, with = "As::<Option<FromInto<_Compression>>>")]
323    #[cfg_attr(feature = "schemars1", schemars(with = "Option<_Compression>"))]
324    pub compression: Option<Compression>,
325}
326
327impl TonicConfig {
328    pub fn apply<T: HasTonicConfig>(self, to: T) -> T {
329        let Self { compression } = self;
330        match compression {
331            Some(it) => to.with_compression(it),
332            None => to,
333        }
334    }
335}
336
337#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq, Default)]
338#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
339#[serde(deny_unknown_fields, rename_all = "kebab-case")]
340pub struct ExportConfig {
341    pub endpoint: Option<String>,
342    pub timeout: Option<Duration>,
343
344    #[serde(default, with = "As::<Option<FromInto<_Protocol>>>")]
345    #[cfg_attr(feature = "schemars1", schemars(with = "Option<_Protocol>"))]
346    pub protocol: Option<Protocol>,
347}
348
349impl ExportConfig {
350    pub fn apply<T: HasExportConfig>(self, mut to: T) -> T {
351        let Self {
352            endpoint,
353            protocol,
354            timeout,
355        } = self;
356        let protocol = protocol.unwrap_or(to.export_config().protocol);
357        to.with_export_config(opentelemetry_otlp::ExportConfig {
358            endpoint,
359            protocol,
360            timeout,
361        })
362    }
363}
364
365#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
366#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
367#[serde(deny_unknown_fields, rename_all = "kebab-case")]
368pub struct Meter {
369    #[serde(default)]
370    pub scope: InstrumentationScope,
371    #[serde(default)]
372    pub provider: MeterProvider,
373}
374
375impl Meter {
376    pub fn build(self) -> Result<opentelemetry::metrics::Meter, ExporterBuildError> {
377        let Self { scope, provider } = self;
378        provider
379            .builder()
380            .map(|b| b.build().meter_with_scope(scope.builder().build()))
381    }
382}
383
384#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
385#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
386#[serde(deny_unknown_fields, rename_all = "kebab-case")]
387pub struct MetricExporter {
388    #[serde(default)]
389    pub http: HttpConfig,
390    #[serde(default)]
391    pub export: ExportConfig,
392    pub temporality: Option<Temporality>,
393    pub interval: Option<Duration>,
394}
395
396impl MetricExporter {
397    pub fn builder(
398        self,
399    ) -> Result<PeriodicReaderBuilder<opentelemetry_otlp::MetricExporter>, ExporterBuildError> {
400        let Self {
401            temporality,
402            http,
403            export,
404            interval,
405        } = self;
406        Ok(apply(
407            PeriodicReader::builder(
408                apply(
409                    export.apply(http.apply(MetricExporterBuilder::new().with_http())),
410                    temporality.map(Into::into),
411                    MetricExporterBuilder::with_temporality,
412                )
413                .build()?,
414            ),
415            interval,
416            |it, dur| it.with_interval(dur),
417        ))
418    }
419}
420
421#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)]
422#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
423#[serde(deny_unknown_fields, rename_all = "kebab-case")]
424pub struct MeterProvider {
425    #[serde(default)]
426    pub exporters: Vec<MetricExporter>,
427    #[serde(default)]
428    pub resource: Resource,
429}
430
431impl MeterProvider {
432    pub fn builder(self) -> Result<MeterProviderBuilder, ExporterBuildError> {
433        let Self {
434            exporters,
435            resource,
436        } = self;
437        exporters.into_iter().try_fold(
438            MeterProviderBuilder::default().with_resource(resource.builder().build()),
439            |acc, el| el.builder().map(|it| acc.with_reader(it.build())),
440        )
441    }
442    pub fn layer<S>(
443        self,
444    ) -> Result<MetricsLayer<S, opentelemetry_sdk::metrics::SdkMeterProvider>, ExporterBuildError>
445    where
446        S: Subscriber + for<'any> LookupSpan<'any>,
447    {
448        self.builder().map(|it| MetricsLayer::new(it.build()))
449    }
450}
451
452#[derive(Deserialize, Serialize, Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
453#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
454#[serde(deny_unknown_fields, rename_all = "kebab-case")]
455pub enum Temporality {
456    Cumulative,
457    Delta,
458    LowMemory,
459}
460
461impl From<Temporality> for opentelemetry_sdk::metrics::Temporality {
462    fn from(value: Temporality) -> Self {
463        match value {
464            Temporality::Cumulative => Self::Cumulative,
465            Temporality::Delta => Self::Delta,
466            Temporality::LowMemory => Self::LowMemory,
467        }
468    }
469}
470
471macro_rules! conv_enum {
472    (
473        #[convert($ty:ty)]
474        $(#[$enum_meta:meta])*
475        $enum_vis:vis enum $enum_name:ident {
476            $(
477                $(#[$variant_meta:meta])*
478                $variant_name:ident
479            ),* $(,)?
480        }
481    ) => {
482        $(#[$enum_meta])*
483        $enum_vis enum $enum_name {
484            $(
485                $(#[$variant_meta])*
486                $variant_name,
487            )*
488        }
489        impl From<$ty> for $enum_name {
490            fn from(value: $ty) -> Self {
491                match value {
492                    $(
493                        <$ty>::$variant_name => Self::$variant_name,
494                    )*
495                }
496            }
497        }
498        impl From<$enum_name> for $ty {
499            fn from(value: $enum_name) -> Self {
500                match value {
501                    $(
502                        $enum_name::$variant_name => Self::$variant_name,
503                    )*
504                }
505            }
506        }
507    };
508}
509
510conv_enum! {
511#[convert(Protocol)]
512#[derive(Deserialize, Serialize, Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
513#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
514#[serde(deny_unknown_fields, rename_all = "kebab-case")]
515enum _Protocol {
516    Grpc,
517    HttpBinary,
518    HttpJson,
519}}
520
521conv_enum! {
522#[convert(Compression)]
523#[derive(Deserialize, Serialize, Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
524#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
525#[serde(deny_unknown_fields, rename_all = "kebab-case")]
526enum _Compression {
527    Gzip,
528    Zstd,
529}}
530
531fn apply<T, V>(t: T, v: Option<V>, f: impl FnOnce(T, V) -> T) -> T {
532    match v {
533        Some(v) => f(t, v),
534        None => t,
535    }
536}