qrt_log_utils/
lib.rs

1use opentelemetry::{global, KeyValue};
2use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
3use opentelemetry_otlp::{LogExporter, MetricExporter, SpanExporter, WithExportConfig};
4use opentelemetry_sdk::logs::SdkLoggerProvider;
5use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
6use opentelemetry_sdk::trace::SdkTracerProvider;
7use opentelemetry_sdk::Resource;
8use std::sync::{Arc, OnceLock};
9use std::time::Duration;
10use tracing_subscriber::filter::filter_fn;
11use tracing_subscriber::prelude::*;
12use tracing_subscriber::EnvFilter;
13
14// pub use for client
15pub use opentelemetry;
16use opentelemetry::trace::TracerProvider;
17pub use tracing;
18
19const DEFAULT_ENDPOINT: &'static str = "http://localhost:4317";
20
21pub struct LoggerContext {
22    pub logger_provider: SdkLoggerProvider,
23    pub tracer_provider: SdkTracerProvider,
24    pub meter_provider: SdkMeterProvider,
25}
26
27impl LoggerContext {
28    pub fn shudown(self) {
29        let _ = self.meter_provider.shutdown();
30        let _ = self.tracer_provider.shutdown();
31        let _ = self.logger_provider.shutdown();
32    }
33}
34
35fn get_resource(service_name: &'static str) -> Resource {
36    static RESOURCE: OnceLock<Resource> = OnceLock::new();
37    RESOURCE
38        .get_or_init(|| {
39            let mut builder = Resource::builder().with_service_name(service_name);
40
41            #[cfg(feature = "detect-host")]
42            {
43                builder =
44                    builder.with_attribute(gen_host_info().expect("failed to build host info"));
45            }
46
47            builder.build()
48        })
49        .clone()
50}
51fn init_traces(endpoint: &str, service_name: &'static str) -> SdkTracerProvider {
52    let exporter = SpanExporter::builder()
53        .with_tonic()
54        .with_endpoint(endpoint)
55        .build()
56        .expect("Failed to create span exporter");
57    SdkTracerProvider::builder()
58        .with_resource(get_resource(service_name))
59        .with_batch_exporter(exporter)
60        .build()
61}
62
63fn init_metrics(endpoint: &str, service_name: &'static str) -> SdkMeterProvider {
64    let exporter = MetricExporter::builder()
65        .with_tonic()
66        .with_endpoint(endpoint)
67        .build()
68        .expect("Failed to create metric exporter");
69
70    let reader = PeriodicReader::builder(exporter)
71        .with_interval(Duration::from_secs(1))
72        .build();
73    SdkMeterProvider::builder()
74        .with_reader(reader)
75        .with_resource(get_resource(service_name))
76        .build()
77}
78
79fn init_logs(endpoint: &str, service_name: &'static str) -> SdkLoggerProvider {
80    let exporter = LogExporter::builder()
81        .with_tonic()
82        .with_endpoint(endpoint)
83        .build()
84        .expect("Failed to create log exporter");
85
86    SdkLoggerProvider::builder()
87        .with_resource(get_resource(service_name))
88        .with_batch_exporter(exporter)
89        .build()
90}
91
92fn target_in_whitelist(target: &str, whitelist: &[String]) -> bool {
93    whitelist.iter().any(|crate_name| {
94        if target == crate_name {
95            true
96        } else if target.starts_with(crate_name) {
97            target[crate_name.len()..].starts_with("::")
98        } else {
99            false
100        }
101    })
102}
103
104fn init_tracer(
105    application_name: &'static str,
106    endpoint: &str,
107    provider: SdkTracerProvider,
108    crate_whitelist: Vec<String>,
109) -> SdkLoggerProvider {
110    let logger_provider = init_logs(endpoint, application_name);
111
112    // Create a new OpenTelemetryTracingBridge using the above LoggerProvider.
113    let otel_layer = OpenTelemetryTracingBridge::new(&logger_provider);
114
115    // For the OpenTelemetry layer, add a tracing filter to filter events from
116    // OpenTelemetry and its dependent crates (opentelemetry-otlp uses crates
117    // like reqwest/tonic etc.) from being sent back to OTel itself, thus
118    // preventing infinite telemetry generation. The filter levels are set as
119    // follows:
120    // - Allow `info` level and above by default.
121    // - Restrict `opentelemetry`, `hyper`, `tonic`, and `reqwest` completely.
122    // Note: This will also drop events from crates like `tonic` etc. even when
123    // they are used outside the OTLP Exporter. For more details, see:
124    // https://github.com/open-telemetry/opentelemetry-rust/issues/761
125    let filter_otel = EnvFilter::new("info")
126        .add_directive("hyper=off".parse().unwrap())
127        .add_directive("opentelemetry=off".parse().unwrap())
128        .add_directive("tonic=off".parse().unwrap())
129        .add_directive("h2=off".parse().unwrap());
130    let otel_layer = otel_layer.with_filter(filter_otel);
131
132    // Create a new tracing::Fmt layer to print the logs to stdout. It has a
133    // default filter of `info` level and above, and `debug` and above for logs
134    // from OpenTelemetry crates. The filter levels can be customized as needed.
135    let filter = EnvFilter::from_default_env()
136        .add_directive(format!("{}=info", &application_name).parse().unwrap());
137    println!(
138        "filter={} crate_whitelist={}",
139        &filter,
140        &crate_whitelist.join(",")
141    );
142    let tracer = provider.tracer("trace_demo");
143    let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
144    let fmt_layer = tracing_subscriber::fmt::layer()
145        .with_thread_names(true)
146        .with_filter(filter);
147
148    let crate_whitelist: Vec<String> = crate_whitelist
149        .into_iter()
150        .filter_map(|entry| {
151            let trimmed = entry.trim();
152            if trimmed.is_empty() {
153                None
154            } else {
155                Some(trimmed.to_string())
156            }
157        })
158        .collect();
159    let whitelist = Arc::new(crate_whitelist);
160    let whitelist_active = !whitelist.is_empty();
161    let whitelist_filter = filter_fn({
162        let whitelist = Arc::clone(&whitelist);
163        move |metadata| {
164            if whitelist_active {
165                target_in_whitelist(metadata.target(), whitelist.as_slice())
166            } else {
167                true
168            }
169        }
170    });
171    let otel_layer = otel_layer.with_filter(whitelist_filter.clone());
172    let telemetry = telemetry.with_filter(whitelist_filter.clone());
173    let fmt_layer = fmt_layer.with_filter(whitelist_filter);
174
175    // Initialize the tracing subscriber with the OpenTelemetry layer and the
176    // Fmt layer.
177    tracing_subscriber::registry()
178        .with(otel_layer)
179        .with(telemetry)
180        .with(fmt_layer)
181        .init();
182    logger_provider
183}
184
185#[derive(Debug, Default)]
186pub struct LoggerConfig {
187    pub endpoint: Option<String>,
188    /// When non-empty, only logs/traces originating from the listed crates are emitted.
189    pub crate_whitelist: Vec<String>,
190}
191
192impl LoggerConfig {
193    /// Start building a logger configuration with fluent setters.
194    pub fn builder() -> LoggerConfigBuilder {
195        LoggerConfigBuilder::default()
196    }
197}
198
199#[derive(Debug, Default)]
200pub struct LoggerConfigBuilder {
201    endpoint: Option<String>,
202    crate_whitelist: Vec<String>,
203}
204
205impl LoggerConfigBuilder {
206    /// Override the OTLP endpoint (defaults to `http://localhost:4317`).
207    #[must_use]
208    pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
209        self.endpoint = Some(endpoint.into());
210        self
211    }
212
213    /// Add a crate to the whitelist (ignored if blank).
214    #[must_use]
215    pub fn add_whitelist_crate(mut self, crate_name: impl Into<String>) -> Self {
216        let name = crate_name.into();
217        if !name.trim().is_empty() {
218            self.crate_whitelist.push(name);
219        }
220        self
221    }
222
223    /// Extend the whitelist with multiple crate names.
224    #[must_use]
225    pub fn add_whitelist_crates<I, S>(mut self, crates: I) -> Self
226    where
227        I: IntoIterator<Item = S>,
228        S: Into<String>,
229    {
230        for entry in crates {
231            self = self.add_whitelist_crate(entry);
232        }
233        self
234    }
235
236    /// Build the final configuration.
237    pub fn build(self) -> LoggerConfig {
238        LoggerConfig {
239            endpoint: self.endpoint,
240            crate_whitelist: self.crate_whitelist,
241        }
242    }
243}
244
245pub fn init_logger(service_name: &'static str, log_config: LoggerConfig) -> LoggerContext {
246    let LoggerConfig {
247        endpoint,
248        crate_whitelist,
249    } = log_config;
250    let endpoint = endpoint
251        .as_ref()
252        .map(|v| v.as_str())
253        .unwrap_or(DEFAULT_ENDPOINT);
254    // default to allow log from current application
255    let tracer_provider = init_traces(endpoint, service_name);
256    let logger_provider = init_tracer(
257        service_name,
258        endpoint,
259        tracer_provider.clone(),
260        crate_whitelist,
261    );
262    global::set_tracer_provider(tracer_provider.clone());
263    let meter_provider = init_metrics(endpoint, service_name);
264    global::set_meter_provider(meter_provider.clone());
265    LoggerContext {
266        logger_provider,
267        tracer_provider,
268        meter_provider,
269    }
270}
271
272#[cfg(feature = "detect-host")]
273fn gen_host_info() -> Result<KeyValue, std::io::Error> {
274    hostname::get().map(|val| {
275        KeyValue::new(
276            "host.name".to_string(),
277            val.into_string().unwrap_or_else(|_| "unknown".into()),
278        )
279    })
280}
281
282#[cfg(test)]
283mod test {
284    use std::time::Duration;
285
286    use opentelemetry::{global, KeyValue};
287    use tracing::info;
288    use tracing::instrument;
289
290    use crate::{init_logger, LoggerConfig};
291
292    #[instrument]
293    fn foo() {
294        // let trace = register_dist_tracing_root(TraceId::default(), None);
295        // println!("trace value: {:?}", trace);
296        info!("test");
297        bar();
298    }
299
300    #[instrument]
301    fn bar() {
302        // let trace = register_dist_tracing_root(TraceId::default(), None);
303        // println!("trace value: {:?}", trace);
304        info!("test2");
305    }
306
307    #[tokio::test]
308    async fn test_logger() {
309        init_logger("test_logger", LoggerConfig::default());
310        for _ in 0..100 {
311            // let span = span!(Level::Info, "my_span");
312            // let _guard = span.enter();
313            foo();
314        }
315        tokio::time::sleep(Duration::from_secs(4)).await;
316    }
317
318    #[instrument]
319    fn add_counter() {
320        let counter = global::meter("aaa").f64_counter("testCounterF64").build();
321        counter.add(10f64, &[KeyValue::new("rate", "standard")]);
322    }
323
324    #[test]
325    fn target_whitelist_matching() {
326        let whitelist = vec!["crate_a".to_string(), "crate_b".to_string()];
327        assert!(super::target_in_whitelist("crate_a", &whitelist));
328        assert!(super::target_in_whitelist(
329            "crate_a::module::sub",
330            &whitelist
331        ));
332        assert!(super::target_in_whitelist("crate_b::something", &whitelist));
333        assert!(!super::target_in_whitelist("crate", &whitelist));
334        assert!(!super::target_in_whitelist("crate_c::foo", &whitelist));
335    }
336}