Skip to main content

arti/
logging.rs

1//! Configure tracing subscribers for Arti
2
3use anyhow::{Context, Result, anyhow};
4use derive_deftly::Deftly;
5use fs_mistrust::Mistrust;
6use serde::{Deserialize, Serialize};
7use std::io::IsTerminal as _;
8use std::path::Path;
9use std::str::FromStr;
10use std::time::Duration;
11use tor_basic_utils::PathExt as _;
12use tor_config::ConfigBuildError;
13use tor_config::derive::prelude::*;
14use tor_config_path::{CfgPath, CfgPathResolver};
15use tor_error::warn_report;
16use tracing::{Subscriber, error};
17use tracing_appender::non_blocking::WorkerGuard;
18use tracing_subscriber::layer::SubscriberExt;
19use tracing_subscriber::prelude::*;
20use tracing_subscriber::{Layer, filter::Targets, fmt, registry};
21
22mod fields;
23#[cfg(feature = "opentelemetry")]
24mod otlp_file_exporter;
25mod time;
26
27/// Structure to hold our logging configuration options
28#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
29#[derive_deftly(TorConfig)]
30#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
31#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
32pub(crate) struct LoggingConfig {
33    /// Filtering directives that determine tracing levels as described at
34    /// <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/targets/struct.Targets.html#impl-FromStr>
35    ///
36    /// You can override this setting with the -l, --log-level command line parameter.
37    ///
38    /// Example: "info,tor_proto::channel=trace"
39    #[deftly(tor_config(default = "default_console_filter()"))]
40    console: Option<String>,
41
42    /// Filtering directives for the journald logger.
43    ///
44    /// Only takes effect if Arti is built with the `journald` filter.
45    #[deftly(tor_config(
46        build = r#"|this: &Self| tor_config::resolve_option(&this.journald, || None)"#
47    ))]
48    journald: Option<String>,
49
50    /// Configuration for logging spans with OpenTelemetry.
51    #[deftly(tor_config(
52        sub_builder,
53        cfg = r#" feature = "opentelemetry" "#,
54        cfg_desc = "with opentelemetry support"
55    ))]
56    opentelemetry: OpentelemetryConfig,
57
58    /// Configuration for passing information to tokio-console.
59    #[deftly(tor_config(
60        sub_builder,
61        cfg = r#" feature = "tokio-console" "#,
62        cfg_desc = "with tokio-console support"
63    ))]
64    tokio_console: TokioConsoleConfig,
65
66    /// Configuration for one or more logfiles.
67    ///
68    /// The default is not to log to any files.
69    #[deftly(tor_config(list(element(build), listtype = "LogfileList"), default = "vec![]"))]
70    files: Vec<LogfileConfig>,
71
72    /// If set to true, we disable safe logging on _all logs_, and store
73    /// potentially sensitive information at level `info` or higher.
74    ///
75    /// This can be useful for debugging, but it increases the value of your
76    /// logs to an attacker.  Do not turn this on in production unless you have
77    /// a good log rotation mechanism.
78    //
79    // TODO: Eventually we might want to make this more complex, and add a
80    // per-log mechanism to turn off unsafe logging. Alternatively, we might do
81    // that by extending the filter syntax implemented by `tracing` to have an
82    // "unsafe" flag on particular lines.
83    #[deftly(tor_config(default))]
84    log_sensitive_information: bool,
85
86    /// If set to true, promote Tor protocol-violation reports to warning level.
87    #[deftly(tor_config(default))]
88    protocol_warnings: bool,
89
90    /// An approximate granularity with which log times should be displayed.
91    ///
92    /// This value controls every log time that arti outputs; it doesn't have any
93    /// effect on times written by other logging programs like `journald`.
94    ///
95    /// We may round this value up for convenience: For example, if you say
96    /// "2.5s", we may treat it as if you had said "3s."
97    ///
98    /// The default is "1s", or one second.
99    #[deftly(tor_config(default = "std::time::Duration::new(1,0)"))]
100    time_granularity: std::time::Duration,
101}
102
103/// Return a default tracing filter value for `logging.console`.
104#[allow(clippy::unnecessary_wraps)]
105fn default_console_filter() -> Option<String> {
106    Some("info".to_owned())
107}
108
109/// Configuration information for an (optionally rotating) logfile.
110#[derive(Debug, Deftly, Clone, Eq, PartialEq)]
111#[derive_deftly(TorConfig)]
112#[deftly(tor_config(no_default_trait))]
113#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
114#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
115pub(crate) struct LogfileConfig {
116    /// How often to rotate the file?
117    #[deftly(tor_config(default))]
118    rotate: LogRotation,
119    /// Where to write the files?
120    #[deftly(tor_config(no_default))]
121    path: CfgPath,
122    /// Filter to apply before writing
123    #[deftly(tor_config(no_default))]
124    filter: String,
125}
126
127/// How often to rotate a log file
128#[derive(Debug, Default, Clone, Serialize, Deserialize, Copy, Eq, PartialEq)]
129#[non_exhaustive]
130#[serde(rename_all = "lowercase")]
131#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
132pub(crate) enum LogRotation {
133    /// Rotate logs daily
134    Daily,
135    /// Rotate logs hourly
136    Hourly,
137    /// Never rotate the log
138    #[default]
139    Never,
140}
141
142/// Configuration for exporting spans with OpenTelemetry.
143#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
144#[derive_deftly(TorConfig)]
145#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
146#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
147pub(crate) struct OpentelemetryConfig {
148    /// Write spans to a file in OTLP JSON format.
149    #[deftly(tor_config(default))]
150    file: Option<OpentelemetryFileExporterConfig>,
151    /// Export spans via HTTP.
152    #[deftly(tor_config(default))]
153    http: Option<OpentelemetryHttpExporterConfig>,
154}
155
156/// Configuration for the OpenTelemetry HTTP exporter.
157#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
158#[derive_deftly(TorConfig)]
159#[deftly(tor_config(no_default_trait))]
160#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
161#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
162pub(crate) struct OpentelemetryHttpExporterConfig {
163    /// HTTP(S) endpoint to send spans to.
164    ///
165    /// For Jaeger, this should be something like: `http://localhost:4318/v1/traces`
166    #[deftly(tor_config(no_default))]
167    endpoint: String,
168    /// Configuration for how to batch exports.
169    #[deftly(tor_config(sub_builder))]
170    batch: OpentelemetryBatchConfig,
171    /// Timeout for sending data.
172    ///
173    /// If this is set to [`None`], it will be left at the OpenTelemetry default, which is
174    /// currently 10 seconds unless overridden with a environment variable.
175    //
176    // NOTE: there is no way to actually override this with None, so we have to say
177    // "no magic" to tell dd(TorConfig) not to worry about that.
178    #[deftly(tor_config(no_magic, default))]
179    timeout: Option<Duration>,
180    // TODO: Once opentelemetry-otlp supports more than one protocol over HTTP, add a config option
181    // to choose protocol here.
182}
183
184/// Configuration for the OpenTelemetry HTTP exporter.
185#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
186#[derive_deftly(TorConfig)]
187#[deftly(tor_config(no_default_trait))]
188#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
189#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
190pub(crate) struct OpentelemetryFileExporterConfig {
191    /// The path to write the JSON file to.
192    #[deftly(tor_config(no_default))]
193    path: CfgPath,
194    /// Configuration for how to batch writes.
195    #[deftly(tor_config(sub_builder))]
196    batch: OpentelemetryBatchConfig,
197}
198
199/// Configuration for the Opentelemetry batch exporting.
200///
201/// This is a copy of [`opentelemetry_sdk::trace::BatchConfig`].
202#[derive(Debug, Deftly, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
203#[derive_deftly(TorConfig)]
204#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
205#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
206pub(crate) struct OpentelemetryBatchConfig {
207    /// Maximum queue size. See [`opentelemetry_sdk::trace::BatchConfig::max_queue_size`].
208    #[deftly(tor_config(default))]
209    max_queue_size: Option<usize>,
210    /// Maximum export batch size. See [`opentelemetry_sdk::trace::BatchConfig::max_export_batch_size`].
211    #[deftly(tor_config(default))]
212    max_export_batch_size: Option<usize>,
213    /// Scheduled delay. See [`opentelemetry_sdk::trace::BatchConfig::scheduled_delay`].
214    #[deftly(tor_config(no_magic, default))]
215    scheduled_delay: Option<Duration>,
216}
217
218#[cfg(feature = "opentelemetry")]
219impl From<OpentelemetryBatchConfig> for opentelemetry_sdk::trace::BatchConfig {
220    fn from(config: OpentelemetryBatchConfig) -> opentelemetry_sdk::trace::BatchConfig {
221        let batch_config = opentelemetry_sdk::trace::BatchConfigBuilder::default();
222
223        let batch_config = if let Some(max_queue_size) = config.max_queue_size {
224            batch_config.with_max_queue_size(max_queue_size)
225        } else {
226            batch_config
227        };
228
229        let batch_config = if let Some(max_export_batch_size) = config.max_export_batch_size {
230            batch_config.with_max_export_batch_size(max_export_batch_size)
231        } else {
232            batch_config
233        };
234
235        let batch_config = if let Some(scheduled_delay) = config.scheduled_delay {
236            batch_config.with_scheduled_delay(scheduled_delay)
237        } else {
238            batch_config
239        };
240
241        batch_config.build()
242    }
243}
244
245/// Configuration for logging to the tokio console.
246#[derive(Debug, Deftly, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
247#[derive_deftly(TorConfig)]
248#[cfg(feature = "tokio-console")]
249#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
250#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
251pub(crate) struct TokioConsoleConfig {
252    /// If true, the tokio console subscriber should be enabled.
253    ///
254    /// This requires that tokio (and hence arti) is built with `--cfg tokio_unstable`
255    /// in RUSTFLAGS.
256    #[deftly(tor_config(default))]
257    enabled: bool,
258}
259
260/// Placeholder for unused tokio console config.
261#[cfg(not(feature = "tokio-console"))]
262type TokioConsoleConfig = ();
263
264/// As [`Targets::from_str`], but wrapped in an [`anyhow::Result`].
265//
266// (Note that we have to use `Targets`, not `EnvFilter`: see comment in
267// `setup_logging()`.)
268fn filt_from_str_verbose(s: &str, source: &str) -> Result<Targets> {
269    Targets::from_str(s).with_context(|| format!("in {}", source))
270}
271
272/// As filt_from_str_verbose, but treat an absent filter (or an empty string) as
273/// None.
274fn filt_from_opt_str(s: &Option<String>, source: &str) -> Result<Option<Targets>> {
275    Ok(match s {
276        Some(s) if !s.is_empty() => Some(filt_from_str_verbose(s, source)?),
277        _ => None,
278    })
279}
280
281/// Try to construct a tracing [`Layer`] for logging to stderr.
282fn console_layer<S>(config: &LoggingConfig, cli: Option<&str>) -> Result<impl Layer<S> + use<S>>
283where
284    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
285{
286    let timer = time::new_formatter(config.time_granularity);
287    let filter = cli
288        .map(|s| filt_from_str_verbose(s, "--log-level command line parameter"))
289        .or_else(|| filt_from_opt_str(&config.console, "logging.console").transpose())
290        .unwrap_or_else(|| Ok(Targets::from_str("debug").expect("bad default")))?;
291    let use_color = std::io::stderr().is_terminal();
292    // We used to suppress safe-logging on the console, but we removed that
293    // feature: we cannot be certain that the console really is volatile. Even
294    // if isatty() returns true on the console, we can't be sure that the
295    // terminal isn't saving backlog to disk or something like that.
296    Ok(fmt::Layer::default()
297        // we apply custom field formatting so that error fields are listed last
298        .fmt_fields(fields::ErrorsLastFieldFormatter)
299        .with_ansi(use_color)
300        .with_timer(timer)
301        .with_writer(std::io::stderr) // we make this explicit, to match with use_color.
302        .with_filter(filter))
303}
304
305/// Try to construct a tracing [`Layer`] for logging to journald, if one is
306/// configured.
307#[cfg(feature = "journald")]
308fn journald_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
309where
310    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
311{
312    if let Some(filter) = filt_from_opt_str(&config.journald, "logging.journald")? {
313        Ok(Some(tracing_journald::layer()?.with_filter(filter)))
314    } else {
315        // Fortunately, Option<Layer> implements Layer, so we can just return None here.
316        Ok(None)
317    }
318}
319
320/// Try to construct a tracing [`Layer`] for exporting spans via OpenTelemetry.
321///
322/// This doesn't allow for filtering, since most of our spans are exported at the trace level
323/// anyways, and filtering can easily be done when viewing the data.
324#[cfg(feature = "opentelemetry")]
325fn otel_layer<S>(config: &LoggingConfig, path_resolver: &CfgPathResolver) -> Result<impl Layer<S>>
326where
327    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
328{
329    use opentelemetry::trace::TracerProvider;
330    use opentelemetry_otlp::WithExportConfig;
331
332    if config.opentelemetry.file.is_some() && config.opentelemetry.http.is_some() {
333        return Err(ConfigBuildError::Invalid {
334            field: "logging.opentelemetry".into(),
335            problem: "Only one OpenTelemetry exporter can be enabled at once.".into(),
336        }
337        .into());
338    }
339
340    let resource = opentelemetry_sdk::Resource::builder()
341        .with_service_name("arti")
342        .build();
343
344    let span_processor = if let Some(otel_file_config) = &config.opentelemetry.file {
345        let file = std::fs::File::options()
346            .create(true)
347            .append(true)
348            .open(otel_file_config.path.path(path_resolver)?)?;
349
350        let exporter = otlp_file_exporter::FileExporter::new(file, resource.clone());
351
352        opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
353            .with_batch_config(otel_file_config.batch.into())
354            .build()
355    } else if let Some(otel_http_config) = &config.opentelemetry.http {
356        if otel_http_config.endpoint.starts_with("http://")
357            && !(otel_http_config.endpoint.starts_with("http://localhost")
358                || otel_http_config.endpoint.starts_with("http://127.0.0.1"))
359        {
360            return Err(ConfigBuildError::Invalid {
361                field: "logging.opentelemetry.http.endpoint".into(),
362                problem: "OpenTelemetry endpoint is set to HTTP on a non-localhost address! For security reasons, this is not supported.".into(),
363            }
364            .into());
365        }
366        let exporter = opentelemetry_otlp::SpanExporter::builder()
367            .with_http()
368            .with_endpoint(otel_http_config.endpoint.clone());
369
370        let exporter = if let Some(timeout) = otel_http_config.timeout {
371            exporter.with_timeout(timeout)
372        } else {
373            exporter
374        };
375
376        let exporter = exporter.build()?;
377
378        opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
379            .with_batch_config(otel_http_config.batch.into())
380            .build()
381    } else {
382        return Ok(None);
383    };
384
385    let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
386        .with_resource(resource.clone())
387        .with_span_processor(span_processor)
388        .build();
389
390    let tracer = tracer_provider.tracer("otel_file_tracer");
391
392    Ok(Some(tracing_opentelemetry::layer().with_tracer(tracer)))
393}
394
395/// Try to construct a non-blocking tracing [`Layer`] for writing data to an
396/// optionally rotating logfile.
397///
398/// On success, return that layer, along with a WorkerGuard that needs to be
399/// dropped when the program exits, to flush buffered messages.
400fn logfile_layer<S>(
401    config: &LogfileConfig,
402    granularity: std::time::Duration,
403    mistrust: &Mistrust,
404    path_resolver: &CfgPathResolver,
405) -> Result<(impl Layer<S> + Send + Sync + Sized + use<S>, WorkerGuard)>
406where
407    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
408{
409    use tracing_appender::{
410        non_blocking,
411        rolling::{RollingFileAppender, Rotation},
412    };
413    let timer = time::new_formatter(granularity);
414
415    let filter = filt_from_str_verbose(&config.filter, "logging.files.filter")?;
416    let rotation = match config.rotate {
417        LogRotation::Daily => Rotation::DAILY,
418        LogRotation::Hourly => Rotation::HOURLY,
419        _ => Rotation::NEVER,
420    };
421    let path = config.path.path(path_resolver)?;
422
423    let directory = match path.parent() {
424        None => {
425            return Err(anyhow!(
426                "Logfile path \"{}\" did not have a parent directory",
427                path.display_lossy()
428            ));
429        }
430        Some(p) if p == Path::new("") => Path::new("."),
431        Some(d) => d,
432    };
433    mistrust.make_directory(directory).with_context(|| {
434        format!(
435            "Unable to create parent directory for logfile \"{}\"",
436            path.display_lossy()
437        )
438    })?;
439    let fname = path
440        .file_name()
441        .ok_or_else(|| anyhow!("No path for log file"))
442        .map(Path::new)?;
443
444    let appender = RollingFileAppender::new(rotation, directory, fname);
445    let (nonblocking, guard) = non_blocking(appender);
446    let layer = fmt::layer()
447        // we apply custom field formatting so that error fields are listed last
448        .fmt_fields(fields::ErrorsLastFieldFormatter)
449        .with_ansi(false)
450        .with_writer(nonblocking)
451        .with_timer(timer)
452        .with_filter(filter);
453    Ok((layer, guard))
454}
455
456/// Try to construct a tracing [`Layer`] for all of the configured logfiles.
457///
458/// On success, return that layer along with a list of [`WorkerGuard`]s that
459/// need to be dropped when the program exits.
460fn logfile_layers<S>(
461    config: &LoggingConfig,
462    mistrust: &Mistrust,
463    path_resolver: &CfgPathResolver,
464) -> Result<(impl Layer<S> + use<S>, Vec<WorkerGuard>)>
465where
466    S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
467{
468    let mut guards = Vec::new();
469    if config.files.is_empty() {
470        // As above, we have Option<Layer> implements Layer, so we can return
471        // None in this case.
472        return Ok((None, guards));
473    }
474
475    let (layer, guard) = logfile_layer(
476        &config.files[0],
477        config.time_granularity,
478        mistrust,
479        path_resolver,
480    )?;
481    guards.push(guard);
482
483    // We have to use a dyn pointer here so we can build up linked list of
484    // arbitrary depth.
485    let mut layer: Box<dyn Layer<S> + Send + Sync + 'static> = Box::new(layer);
486
487    for logfile in &config.files[1..] {
488        let (new_layer, guard) =
489            logfile_layer(logfile, config.time_granularity, mistrust, path_resolver)?;
490        layer = Box::new(layer.and_then(new_layer));
491        guards.push(guard);
492    }
493
494    Ok((Some(layer), guards))
495}
496
497/// Configure a panic handler to send everything to tracing, in addition to our
498/// default panic behavior.
499fn install_panic_handler() {
500    // TODO library support: There's a library called `tracing-panic` that
501    // provides a hook we could use instead, but that doesn't have backtrace
502    // support.  We should consider using it if it gets backtrace support in the
503    // future.  We should also keep an eye on `tracing` to see if it learns how
504    // to do this for us.
505    let default_handler = std::panic::take_hook();
506    std::panic::set_hook(Box::new(move |panic_info| {
507        // Note that if we were ever to _not_ call this handler,
508        // we would want to abort on nested panics and !can_unwind cases.
509        default_handler(panic_info);
510
511        // This statement is copied from stdlib.
512        let msg = match panic_info.payload().downcast_ref::<&'static str>() {
513            Some(s) => *s,
514            None => match panic_info.payload().downcast_ref::<String>() {
515                Some(s) => &s[..],
516                None => "Box<dyn Any>",
517            },
518        };
519
520        let backtrace = std::backtrace::Backtrace::force_capture();
521        match panic_info.location() {
522            Some(location) => error!("Panic at {}: {}\n{}", location, msg, backtrace),
523            None => error!("Panic at ???: {}\n{}", msg, backtrace),
524        };
525    }));
526}
527
528/// Opaque structure that gets dropped when the program is shutting down,
529/// after logs are no longer needed.  The `Drop` impl flushes buffered messages.
530#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
531pub(crate) struct LogGuards {
532    /// The actual list of guards we're returning.
533    #[allow(unused)]
534    guards: Vec<WorkerGuard>,
535
536    /// A safelog guard, for use if we have decided to disable safe logging.
537    #[allow(unused)]
538    safelog_guard: Option<safelog::Guard>,
539}
540
541/// Set up logging.
542///
543/// Note that the returned LogGuard must be dropped precisely when the program
544/// quits; they're used to ensure that all the log messages are flushed.
545#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
546#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
547pub(crate) fn setup_logging(
548    config: &LoggingConfig,
549    mistrust: &Mistrust,
550    path_resolver: &CfgPathResolver,
551    cli: Option<&str>,
552) -> Result<LogGuards> {
553    // Important: We have to make sure that the individual layers we add here
554    // are not filters themselves.  That means, for example, that we can't add
555    // an `EnvFilter` layer unless we want it to apply globally to _all_ layers.
556    //
557    // For a bit of discussion on the difference between per-layer filters and filters
558    // that apply to the entire registry, see
559    // https://docs.rs/tracing-subscriber/0.3.5/tracing_subscriber/layer/index.html#global-filtering
560
561    let registry = registry().with(console_layer(config, cli)?);
562
563    #[cfg(feature = "journald")]
564    let registry = registry.with(journald_layer(config)?);
565
566    #[cfg(feature = "opentelemetry")]
567    let registry = registry.with(otel_layer(config, path_resolver)?);
568
569    #[cfg(feature = "tokio-console")]
570    let registry = {
571        // Note 1: We can't enable console_subscriber unconditionally when the `tokio-console`
572        // feature is enabled, since it panics unless tokio is built with  `--cfg tokio_unstable`,
573        // but we want arti to work with --all-features without any special --cfg.
574        //
575        // Note 2: We have to use an `Option` here, since the type of the registry changes
576        // with whatever you add to it.
577        let tokio_layer = if config.tokio_console.enabled {
578            Some(console_subscriber::spawn())
579        } else {
580            None
581        };
582        registry.with(tokio_layer)
583    };
584
585    let (layer, guards) = logfile_layers(config, mistrust, path_resolver)?;
586    let registry = registry.with(layer);
587
588    registry.init();
589
590    let safelog_guard = if config.log_sensitive_information {
591        match safelog::disable_safe_logging() {
592            Ok(guard) => Some(guard),
593            Err(e) => {
594                // We don't need to propagate this error; it isn't the end of
595                // the world if we were unable to disable safe logging.
596                warn_report!(e, "Unable to disable safe logging");
597                None
598            }
599        }
600    } else {
601        None
602    };
603
604    let mode = if config.protocol_warnings {
605        tor_error::tracing::ProtocolWarningMode::Warn
606    } else {
607        tor_error::tracing::ProtocolWarningMode::Off
608    };
609    tor_error::tracing::set_protocol_warning_mode(mode);
610
611    install_panic_handler();
612
613    Ok(LogGuards {
614        guards,
615        safelog_guard,
616    })
617}