unc_o11y/
subscriber.rs

1use crate::opentelemetry::add_opentelemetry_layer;
2use crate::reload::{
3    set_default_otlp_level, set_log_layer_handle, set_otlp_layer_handle, LogLayer, SimpleLogLayer,
4};
5use crate::{log_counter, OpenTelemetryLevel};
6use std::path::PathBuf;
7use tracing::subscriber::DefaultGuard;
8use tracing_appender::non_blocking::NonBlocking;
9use tracing_subscriber::layer::SubscriberExt;
10use tracing_subscriber::registry::LookupSpan;
11use tracing_subscriber::{fmt, reload, EnvFilter, Layer};
12use unc_crypto::PublicKey;
13use unc_primitives_core::types::AccountId;
14
15/// The resource representing a registered subscriber.
16///
17/// Once dropped, the subscriber is unregistered, and the output is flushed. Any messages output
18/// after this value is dropped will be delivered to a previously active subscriber, if any.
19pub struct DefaultSubscriberGuard<S> {
20    // NB: the field order matters here. I would've used `ManuallyDrop` to indicate this
21    // particularity, but somebody decided at some point that doing so is unconventional Rust and
22    // that implicit is better than explicit.
23    //
24    // We must first drop the `local_subscriber_guard` so that no new messages are delivered to
25    // this subscriber while we take care of flushing the messages already in queue. If dropped the
26    // other way around, the events/spans generated while the subscriber drop guard runs would be
27    // lost.
28    subscriber: Option<S>,
29    local_subscriber_guard: Option<DefaultGuard>,
30    #[allow(dead_code)] // This field is never read, but has semantic purpose as a drop guard.
31    writer_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
32    #[allow(dead_code)] // This field is never read, but has semantic purpose as a drop guard.
33    io_trace_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
34}
35
36/// Configures exporter of span and trace data.
37#[derive(Debug, Default, clap::Parser)]
38pub struct Options {
39    /// Enables export of span data using opentelemetry exporters.
40    #[clap(long, value_enum, default_value = "off")]
41    opentelemetry: OpenTelemetryLevel,
42
43    /// Whether the log needs to be colored.
44    #[clap(long, value_enum, default_value = "auto")]
45    color: ColorOutput,
46
47    /// Enable logging of spans. For instance, this prints timestamps of entering and exiting a span,
48    /// together with the span duration and used/idle CPU time.
49    #[clap(long)]
50    log_span_events: bool,
51
52    /// Enable JSON output of IO events, written to a file.
53    #[clap(long)]
54    record_io_trace: Option<PathBuf>,
55}
56
57impl<S: tracing::Subscriber + Send + Sync> DefaultSubscriberGuard<S> {
58    /// Register this default subscriber globally , for all threads.
59    ///
60    /// Must not be called more than once. Mutually exclusive with `Self::local`.
61    pub fn global(mut self) -> Self {
62        if let Some(subscriber) = self.subscriber.take() {
63            tracing::subscriber::set_global_default(subscriber)
64                .expect("could not set a global subscriber");
65        } else {
66            panic!("trying to set a default subscriber that has been already taken")
67        }
68        self
69    }
70
71    /// Register this default subscriber for the current thread.
72    ///
73    /// Must not be called more than once. Mutually exclusive with `Self::global`.
74    pub fn local(mut self) -> Self {
75        if let Some(subscriber) = self.subscriber.take() {
76            self.local_subscriber_guard = Some(tracing::subscriber::set_default(subscriber));
77        } else {
78            panic!("trying to set a default subscriber that has been already taken")
79        }
80        self
81    }
82}
83
84/// Whether to use colored log format.
85/// Option `Auto` enables color output only if the logging is done to a terminal and
86/// `NO_COLOR` environment variable is not set.
87#[derive(clap::ValueEnum, Debug, Clone, Default)]
88pub enum ColorOutput {
89    #[default]
90    Always,
91    Never,
92    Auto,
93}
94
95fn is_terminal() -> bool {
96    use std::io::IsTerminal;
97    std::io::stderr().is_terminal()
98}
99
100fn add_simple_log_layer<S, W>(
101    filter: EnvFilter,
102    writer: W,
103    ansi: bool,
104    with_span_events: bool,
105    subscriber: S,
106) -> SimpleLogLayer<S, W>
107where
108    S: tracing::Subscriber + for<'span> LookupSpan<'span> + Send + Sync,
109    W: for<'writer> fmt::MakeWriter<'writer> + 'static,
110{
111    let layer = fmt::layer()
112        .with_ansi(ansi)
113        .with_span_events(get_fmt_span(with_span_events))
114        .with_writer(writer)
115        .with_filter(filter);
116
117    subscriber.with(layer)
118}
119
120fn get_fmt_span(with_span_events: bool) -> fmt::format::FmtSpan {
121    if with_span_events {
122        fmt::format::FmtSpan::ENTER | fmt::format::FmtSpan::CLOSE
123    } else {
124        fmt::format::FmtSpan::NONE
125    }
126}
127
128fn add_non_blocking_log_layer<S>(
129    filter: EnvFilter,
130    writer: NonBlocking,
131    ansi: bool,
132    with_span_events: bool,
133    subscriber: S,
134) -> (LogLayer<S>, reload::Handle<EnvFilter, S>)
135where
136    S: tracing::Subscriber + for<'span> LookupSpan<'span> + Send + Sync,
137{
138    let (filter, handle) = reload::Layer::<EnvFilter, S>::new(filter);
139
140    let layer = fmt::layer()
141        .with_ansi(ansi)
142        .with_span_events(get_fmt_span(with_span_events))
143        .with_writer(writer)
144        .with_filter(filter);
145
146    (subscriber.with(layer), handle)
147}
148
149/// The constructed layer writes storage and DB events in a custom format to a
150/// specified file.
151///
152/// This layer is useful to collect detailed IO access patterns for block
153/// production. Typically used for debugging IO and to replay on the estimator.
154#[cfg(feature = "io_trace")]
155pub fn make_io_tracing_layer<S>(
156    file: std::fs::File,
157) -> (
158    tracing_subscriber::filter::Filtered<crate::io_tracer::IoTraceLayer, EnvFilter, S>,
159    tracing_appender::non_blocking::WorkerGuard,
160)
161where
162    S: tracing::Subscriber + for<'span> LookupSpan<'span>,
163{
164    use std::io::BufWriter;
165    let (base_io_layer, guard) = crate::io_tracer::IoTraceLayer::new(BufWriter::new(file));
166    let io_layer = base_io_layer.with_filter(EnvFilter::new(
167        "store=trace,vm_logic=trace,host-function=trace,runtime=debug,crate::io_tracer=trace,io_tracer_count=trace",
168    ));
169    (io_layer, guard)
170}
171
172fn use_color_output(options: &Options) -> bool {
173    match options.color {
174        ColorOutput::Always => true,
175        ColorOutput::Never => false,
176        ColorOutput::Auto => use_color_auto(),
177    }
178}
179
180pub(crate) fn use_color_auto() -> bool {
181    std::env::var_os("NO_COLOR").is_none() && is_terminal()
182}
183
184/// Constructs a subscriber set to the option appropriate for the UNC code.
185///
186/// Subscriber enables only logging.
187///
188/// # Example
189///
190/// ```rust
191/// let filter = unc_o11y::EnvFilterBuilder::from_env().finish().unwrap();
192/// let _subscriber = unc_o11y::default_subscriber(filter, &Default::default()).global();
193/// ```
194pub fn default_subscriber(
195    env_filter: EnvFilter,
196    options: &Options,
197) -> DefaultSubscriberGuard<impl tracing::Subscriber + for<'span> LookupSpan<'span> + Send + Sync> {
198    let color_output = use_color_output(options);
199
200    let make_writer = || {
201        let stderr = std::io::stderr();
202        std::io::LineWriter::new(stderr)
203    };
204
205    let subscriber = tracing_subscriber::registry();
206    let subscriber = subscriber.with(log_counter::LogCounter::default());
207    let subscriber = add_simple_log_layer(
208        env_filter,
209        make_writer,
210        color_output,
211        options.log_span_events,
212        subscriber,
213    );
214
215    #[allow(unused_mut)]
216    let mut io_trace_guard = None;
217    #[cfg(feature = "io_trace")]
218    let subscriber = subscriber.with(options.record_io_trace.as_ref().map(|output_path| {
219        let (sub, guard) = make_io_tracing_layer(
220            std::fs::File::create(output_path)
221                .expect("unable to create or truncate IO trace output file"),
222        );
223        io_trace_guard = Some(guard);
224        sub
225    }));
226
227    DefaultSubscriberGuard {
228        subscriber: Some(subscriber),
229        local_subscriber_guard: None,
230        writer_guard: None,
231        io_trace_guard,
232    }
233}
234
235/// Constructs a subscriber set to the option appropriate for the UNC code.
236///
237/// The subscriber enables logging, tracing and io tracing.
238/// Subscriber creation needs an async runtime.
239pub async fn default_subscriber_with_opentelemetry(
240    env_filter: EnvFilter,
241    options: &Options,
242    chain_id: String,
243    node_public_key: PublicKey,
244    account_id: Option<AccountId>,
245) -> DefaultSubscriberGuard<impl tracing::Subscriber + Send + Sync> {
246    let color_output = use_color_output(options);
247
248    // Do not lock the `stderr` here to allow for things like `dbg!()` work during development.
249    let stderr = std::io::stderr();
250    let lined_stderr = std::io::LineWriter::new(stderr);
251    let (writer, writer_guard) = tracing_appender::non_blocking(lined_stderr);
252
253    let subscriber = tracing_subscriber::registry();
254    // Installs LogCounter as the innermost layer.
255    let subscriber = subscriber.with(log_counter::LogCounter::default());
256
257    set_default_otlp_level(options.opentelemetry);
258
259    let (subscriber, handle) = add_non_blocking_log_layer(
260        env_filter,
261        writer,
262        color_output,
263        options.log_span_events,
264        subscriber,
265    );
266    set_log_layer_handle(handle);
267
268    let (subscriber, handle) = add_opentelemetry_layer(
269        options.opentelemetry,
270        chain_id,
271        node_public_key,
272        account_id,
273        subscriber,
274    )
275    .await;
276    set_otlp_layer_handle(handle);
277
278    #[allow(unused_mut)]
279    let mut io_trace_guard = None;
280    #[cfg(feature = "io_trace")]
281    let subscriber = subscriber.with(options.record_io_trace.as_ref().map(|output_path| {
282        let (sub, guard) = make_io_tracing_layer(
283            std::fs::File::create(output_path)
284                .expect("unable to create or truncate IO trace output file"),
285        );
286        io_trace_guard = Some(guard);
287        sub
288    }));
289
290    DefaultSubscriberGuard {
291        subscriber: Some(subscriber),
292        local_subscriber_guard: None,
293        writer_guard: Some(writer_guard),
294        io_trace_guard,
295    }
296}