Skip to main content

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