diode_base/
tracing.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::str::FromStr as _;
4use std::sync::Arc;
5use std::time::Duration;
6
7use diode::{App, AppBuilder, StdError};
8use duration_str::deserialize_option_duration;
9use opentelemetry::trace::{SpanKind, TracerProvider as _};
10use opentelemetry::{Key, KeyValue};
11use opentelemetry_otlp::WithExportConfig;
12use opentelemetry_sdk::export::trace::{ExportResult, SpanData, SpanExporter};
13use opentelemetry_sdk::trace::TracerProvider;
14use opentelemetry_sdk::{Resource, runtime};
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16use tracing_subscriber::filter::{Directive, EnvFilter};
17use tracing_subscriber::layer::SubscriberExt as _;
18use tracing_subscriber::util::SubscriberInitExt;
19use tracing_subscriber::{Registry, reload};
20
21use crate::{AddDaemonExt, CancellationToken, Config, ConfigSection, Daemon, DynamicConfig};
22
23pub struct Tracing {
24    default_level: tracing::Level,
25    directives: Vec<Directive>,
26    reload_handle: reload::Handle<EnvFilter, Registry>,
27    tracer_provider: TracerProvider,
28}
29
30impl Tracing {
31    pub fn build(app: &mut AppBuilder) -> Result<(), StdError> {
32        if app.has_component::<Self>() {
33            // Ensure that the tracing daemon is added.
34            if !app.has_daemon::<TracingDaemon>() {
35                app.add_daemon(TracingDaemon);
36            }
37            return Ok(());
38        }
39        let config = match app
40            .get_component_ref::<Config>()
41            .unwrap()
42            .get::<Option<TracingConfig>>("tracing")?
43        {
44            Some(v) => v,
45            None => return Ok(()),
46        };
47        let mut directives = Vec::new();
48        for directive in config.directives {
49            directives.push(directive.parse().map_err(Box::new)?);
50        }
51        // Setup dynamic config level filter.
52        let (env_filter, reload_handle) =
53            reload::Layer::new(new_env_filter(&directives, config.level));
54        // Setup OpenTelemetry tracer.
55        let tracer_provider = {
56            if let Some(otlp_exporter) = config.otlp_exporter {
57                let exporter_builder = opentelemetry_otlp::SpanExporter::builder()
58                    .with_tonic()
59                    .with_endpoint(
60                        otlp_exporter
61                            .endpoint
62                            .unwrap_or(DEFAULT_OTLP_EXPORTER_ENDPOINT.into()),
63                    )
64                    .with_timeout(
65                        otlp_exporter
66                            .timeout
67                            .unwrap_or(DEFAULT_OTLP_EXPORTER_TIMEOUT),
68                    );
69                let exporter = CustomSpanExporter::new(exporter_builder.build().unwrap());
70                TracerProvider::builder()
71                    .with_resource(Resource::new(vec![KeyValue::new(
72                        "service.name",
73                        otlp_exporter.service_name.unwrap_or("unknown".into()),
74                    )]))
75                    .with_batch_exporter(exporter, runtime::Tokio)
76                    .build()
77            } else {
78                TracerProvider::builder().build()
79            }
80        };
81        // Setup tracing registry.
82        tracing_subscriber::registry()
83            .with(env_filter)
84            .with(tracing_subscriber::fmt::Layer::default())
85            .with(tracing_opentelemetry::layer().with_tracer(tracer_provider.tracer("")))
86            .init();
87        // Add app components.
88        app.add_component(Self {
89            default_level: config.level,
90            directives,
91            reload_handle,
92            tracer_provider,
93        });
94        app.add_daemon(TracingDaemon);
95        Ok(())
96    }
97}
98
99impl Drop for Tracing {
100    fn drop(&mut self) {
101        if let Err(err) = self.tracer_provider.shutdown() {
102            tracing::error!("Cannot shutdown tracer provider: {err}");
103        }
104    }
105}
106
107struct TracingDaemon;
108
109const TRACING_LEVEL_CONFIG_KEY: &str = "tracing_level";
110
111impl Daemon for TracingDaemon {
112    async fn run(&self, app: &App, shutdown: CancellationToken) -> Result<(), StdError> {
113        let tracing = app.get_component_ref::<Tracing>().unwrap();
114        let default_level = tracing.default_level;
115        let directives = tracing.directives.clone();
116        let reload_handle = tracing.reload_handle.clone();
117        if let Some(dynamic_config) = app.get_component::<Arc<DynamicConfig>>() {
118            dynamic_config.subscribe(TRACING_LEVEL_CONFIG_KEY, move |level: Option<String>| {
119                let level = match level {
120                    Some(v) => match tracing::Level::from_str(&v) {
121                        Ok(v) => v,
122                        Err(err) => {
123                            tracing::error!("Cannot parse tracing level: {}", err);
124                            return;
125                        }
126                    },
127                    None => default_level,
128                };
129                reload_handle
130                    .reload(new_env_filter(&directives, level))
131                    .unwrap();
132            });
133        }
134        shutdown.cancelled_owned().await;
135        Ok(())
136    }
137}
138
139impl Default for TracingConfig {
140    fn default() -> Self {
141        Self {
142            level: default_level(),
143            directives: Default::default(),
144            otlp_exporter: None,
145        }
146    }
147}
148
149#[derive(Serialize, Deserialize)]
150pub struct TracingOtlpExporterConfig {
151    #[serde(default)]
152    pub service_name: Option<String>,
153    #[serde(default)]
154    pub endpoint: Option<String>,
155    #[serde(default, deserialize_with = "deserialize_option_duration")]
156    pub timeout: Option<Duration>,
157}
158
159#[derive(Serialize, Deserialize)]
160pub struct TracingConfig {
161    #[serde(
162        serialize_with = "serialize_level",
163        deserialize_with = "deserialize_level",
164        default = "default_level"
165    )]
166    pub level: tracing::Level,
167    #[serde(default)]
168    pub directives: Vec<String>,
169    #[serde(default)]
170    pub otlp_exporter: Option<TracingOtlpExporterConfig>,
171}
172
173impl ConfigSection for TracingConfig {
174    fn key() -> &'static str {
175        "tracing"
176    }
177}
178
179fn new_env_filter(directives: &Vec<Directive>, level: tracing::Level) -> EnvFilter {
180    let mut filter = EnvFilter::default();
181    for directive in directives {
182        filter = filter.add_directive(directive.clone());
183    }
184    filter.add_directive(level.into())
185}
186
187const DEFAULT_OTLP_EXPORTER_ENDPOINT: &str = "https://localhost:4317/v1/traces";
188const DEFAULT_OTLP_EXPORTER_TIMEOUT: Duration = Duration::from_secs(10);
189
190#[derive(Debug)]
191struct CustomSpanExporter<T> {
192    inner: T,
193}
194
195impl<T> CustomSpanExporter<T> {
196    pub fn new(inner: T) -> Self {
197        Self { inner }
198    }
199}
200
201impl<T> SpanExporter for CustomSpanExporter<T>
202where
203    T: SpanExporter,
204{
205    fn export(
206        &mut self,
207        mut batch: Vec<SpanData>,
208    ) -> Pin<Box<dyn Future<Output = ExportResult> + Send + 'static>> {
209        const OTEL_NAME_KEY: Key = Key::from_static_str("otel.name");
210        const OTEL_KIND_KEY: Key = Key::from_static_str("otel.kind");
211        const TRACE_ID_KEY: Key = Key::from_static_str("trace_id");
212        for span in batch.iter_mut() {
213            let mut otel_name = None;
214            let mut otel_kind = None;
215            span.attributes.retain(|v| {
216                if v.key == OTEL_NAME_KEY {
217                    otel_name = Some(v.value.clone());
218                    false
219                } else if v.key == OTEL_KIND_KEY {
220                    otel_kind = Some(v.value.clone());
221                    false
222                } else if v.key == TRACE_ID_KEY {
223                    false
224                } else {
225                    true
226                }
227            });
228            if let Some(v) = otel_name {
229                span.name = v.to_string().into();
230            }
231            if let Some(v) = otel_kind {
232                match v.as_str().as_ref() {
233                    "server" => span.span_kind = SpanKind::Server,
234                    "client" => span.span_kind = SpanKind::Client,
235                    "consumer" => span.span_kind = SpanKind::Consumer,
236                    "producer" => span.span_kind = SpanKind::Producer,
237                    _ => {}
238                }
239            }
240        }
241        self.inner.export(batch)
242    }
243
244    fn shutdown(&mut self) {
245        self.inner.shutdown();
246    }
247
248    fn force_flush(&mut self) -> Pin<Box<dyn Future<Output = ExportResult> + Send + 'static>> {
249        self.inner.force_flush()
250    }
251
252    fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) {
253        self.inner.set_resource(resource);
254    }
255}
256
257fn serialize_level<S>(v: &tracing::Level, serializer: S) -> Result<S::Ok, S::Error>
258where
259    S: Serializer,
260{
261    serializer.serialize_str(v.as_str())
262}
263
264fn deserialize_level<'de, D>(deserializer: D) -> Result<tracing::Level, D::Error>
265where
266    D: Deserializer<'de>,
267{
268    use serde::de::Error;
269    String::deserialize(deserializer)
270        .and_then(|v| tracing::Level::from_str(&v).map_err(|v| Error::custom(format!("{v}"))))
271}
272
273fn default_level() -> tracing::Level {
274    tracing::Level::DEBUG
275}