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::OnceLock;
9use std::time::Duration;
10use tracing_subscriber::prelude::*;
11use tracing_subscriber::EnvFilter;
12
13// pub use for client
14pub use opentelemetry;
15use opentelemetry::trace::TracerProvider;
16pub use tracing;
17
18const DEFAULT_ENDPOINT: &'static str = "http://localhost:4317";
19
20pub struct LoggerContext {
21    pub logger_provider: SdkLoggerProvider,
22    pub tracer_provider: SdkTracerProvider,
23    pub meter_provider: SdkMeterProvider,
24}
25
26impl LoggerContext {
27    pub fn shudown(self) {
28        let _ = self.meter_provider.shutdown();
29        let _ = self.tracer_provider.shutdown();
30        let _ = self.logger_provider.shutdown();
31    }
32}
33
34fn get_resource(service_name: &'static str) -> Resource {
35    static RESOURCE: OnceLock<Resource> = OnceLock::new();
36    RESOURCE
37        .get_or_init(|| {
38            let mut builder = Resource::builder().with_service_name(service_name);
39
40            #[cfg(feature = "detect-host")]
41            {
42                builder =
43                    builder.with_attribute(gen_host_info().expect("failed to build host info"));
44            }
45
46            builder.build()
47        })
48        .clone()
49}
50fn init_traces(endpoint: &str, service_name: &'static str) -> SdkTracerProvider {
51    let exporter = SpanExporter::builder()
52        .with_tonic()
53        .with_endpoint(endpoint)
54        .build()
55        .expect("Failed to create span exporter");
56    SdkTracerProvider::builder()
57        .with_resource(get_resource(service_name))
58        .with_batch_exporter(exporter)
59        .build()
60}
61
62fn init_metrics(endpoint: &str, service_name: &'static str) -> SdkMeterProvider {
63    let exporter = MetricExporter::builder()
64        .with_tonic()
65        .with_endpoint(endpoint)
66        .build()
67        .expect("Failed to create metric exporter");
68
69    let reader = PeriodicReader::builder(exporter)
70        .with_interval(Duration::from_secs(1))
71        .build();
72    SdkMeterProvider::builder()
73        .with_reader(reader)
74        .with_resource(get_resource(service_name))
75        .build()
76}
77
78fn init_logs(endpoint: &str, service_name: &'static str) -> SdkLoggerProvider {
79    let exporter = LogExporter::builder()
80        .with_tonic()
81        .with_endpoint(endpoint)
82        .build()
83        .expect("Failed to create log exporter");
84
85    SdkLoggerProvider::builder()
86        .with_resource(get_resource(service_name))
87        .with_batch_exporter(exporter)
88        .build()
89}
90
91fn init_tracer(
92    application_name: &'static str,
93    endpoint: &str,
94    provider: SdkTracerProvider,
95) -> SdkLoggerProvider {
96    let logger_provider = init_logs(endpoint, application_name);
97
98    // Create a new OpenTelemetryTracingBridge using the above LoggerProvider.
99    let otel_layer = OpenTelemetryTracingBridge::new(&logger_provider);
100
101    // For the OpenTelemetry layer, add a tracing filter to filter events from
102    // OpenTelemetry and its dependent crates (opentelemetry-otlp uses crates
103    // like reqwest/tonic etc.) from being sent back to OTel itself, thus
104    // preventing infinite telemetry generation. The filter levels are set as
105    // follows:
106    // - Allow `info` level and above by default.
107    // - Restrict `opentelemetry`, `hyper`, `tonic`, and `reqwest` completely.
108    // Note: This will also drop events from crates like `tonic` etc. even when
109    // they are used outside the OTLP Exporter. For more details, see:
110    // https://github.com/open-telemetry/opentelemetry-rust/issues/761
111    let filter_otel = EnvFilter::new("info")
112        .add_directive("hyper=off".parse().unwrap())
113        .add_directive("opentelemetry=off".parse().unwrap())
114        .add_directive("tonic=off".parse().unwrap())
115        .add_directive("h2=off".parse().unwrap());
116    let otel_layer = otel_layer.with_filter(filter_otel);
117
118    // Create a new tracing::Fmt layer to print the logs to stdout. It has a
119    // default filter of `info` level and above, and `debug` and above for logs
120    // from OpenTelemetry crates. The filter levels can be customized as needed.
121    let filter = EnvFilter::from_default_env()
122        .add_directive(format!("{}=info", &application_name).parse().unwrap());
123    println!("filter={}", &filter);
124    let tracer = provider.tracer("trace_demo");
125    let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
126    let fmt_layer = tracing_subscriber::fmt::layer()
127        .with_thread_names(true)
128        .with_filter(filter);
129
130    // Initialize the tracing subscriber with the OpenTelemetry layer and the
131    // Fmt layer.
132    tracing_subscriber::registry()
133        .with(otel_layer)
134        .with(telemetry)
135        .with(fmt_layer)
136        .init();
137    logger_provider
138}
139
140#[derive(Debug, Default)]
141pub struct LoggerConfig {
142    pub endpoint: Option<String>,
143}
144
145pub fn init_logger(service_name: &'static str, log_config: LoggerConfig) -> LoggerContext {
146    let endpoint = log_config
147        .endpoint
148        .as_ref()
149        .map(|v| v.as_str())
150        .unwrap_or(DEFAULT_ENDPOINT);
151    // default to allow log from current application
152    let tracer_provider = init_traces(endpoint, service_name);
153    let logger_provider = init_tracer(service_name, endpoint, tracer_provider.clone());
154    global::set_tracer_provider(tracer_provider.clone());
155    let meter_provider = init_metrics(endpoint, service_name);
156    global::set_meter_provider(meter_provider.clone());
157    LoggerContext {
158        logger_provider,
159        tracer_provider,
160        meter_provider,
161    }
162}
163
164#[cfg(feature = "detect-host")]
165fn gen_host_info() -> Result<KeyValue, std::io::Error> {
166    hostname::get().map(|val| {
167        KeyValue::new(
168            "host.name".to_string(),
169            val.into_string().unwrap_or_else(|_| "unknown".into()),
170        )
171    })
172}
173
174#[cfg(test)]
175mod test {
176    use std::time::Duration;
177
178    use opentelemetry::{global, KeyValue};
179    use tracing::info;
180    use tracing::instrument;
181
182    use crate::{init_logger, LoggerConfig};
183
184    #[instrument]
185    fn foo() {
186        // let trace = register_dist_tracing_root(TraceId::default(), None);
187        // println!("trace value: {:?}", trace);
188        info!("test");
189        bar();
190    }
191
192    #[instrument]
193    fn bar() {
194        // let trace = register_dist_tracing_root(TraceId::default(), None);
195        // println!("trace value: {:?}", trace);
196        info!("test2");
197    }
198
199    #[tokio::test]
200    async fn test_logger() {
201        init_logger("test_logger", LoggerConfig::default());
202        for _ in 0..100 {
203            // let span = span!(Level::Info, "my_span");
204            // let _guard = span.enter();
205            foo();
206        }
207        tokio::time::sleep(Duration::from_secs(4)).await;
208    }
209
210    #[instrument]
211    fn add_counter() {
212        let counter = global::meter("aaa").f64_counter("testCounterF64").build();
213        counter.add(10f64, &[KeyValue::new("rate", "standard")]);
214    }
215}