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}