1use anyhow::{Context, Result, anyhow};
4use derive_builder::Builder;
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::impl_standard_builder;
14use tor_config::{define_list_builder_accessors, define_list_builder_helper};
15use tor_config_path::{CfgPath, CfgPathResolver};
16use tor_error::warn_report;
17use tracing::{Subscriber, error};
18use tracing_appender::non_blocking::WorkerGuard;
19use tracing_subscriber::layer::SubscriberExt;
20use tracing_subscriber::prelude::*;
21use tracing_subscriber::{Layer, filter::Targets, fmt, registry};
22
23mod fields;
24#[cfg(feature = "opentelemetry")]
25mod otlp_file_exporter;
26mod time;
27
28#[derive(Debug, Clone, Builder, Eq, PartialEq)]
30#[non_exhaustive] #[builder(build_fn(private, name = "build_unvalidated", error = "ConfigBuildError"))]
32#[builder(derive(Debug, Serialize, Deserialize))]
33#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
34#[cfg_attr(feature = "experimental-api", builder(public))]
35pub(crate) struct LoggingConfig {
36 #[builder(default = "default_console_filter()", setter(into, strip_option))]
43 console: Option<String>,
44
45 #[builder(
49 setter(into),
50 field(build = r#"tor_config::resolve_option(&self.journald, || None)"#)
51 )]
52 journald: Option<String>,
53
54 #[cfg(feature = "opentelemetry")]
56 #[builder_field_attr(serde(default))]
57 #[builder(default)]
58 opentelemetry: OpentelemetryConfig,
59
60 #[cfg(not(feature = "opentelemetry"))]
64 #[builder_field_attr(serde(default))]
65 #[builder(field(type = "Option<toml::Value>", build = "()"), private)]
66 opentelemetry: (),
67
68 #[cfg(feature = "tokio-console")]
70 #[builder(sub_builder(fn_name = "build"))]
71 #[builder_field_attr(serde(default))]
72 tokio_console: TokioConsoleConfig,
73
74 #[cfg(not(feature = "tokio-console"))]
78 #[builder_field_attr(serde(default))]
79 #[builder(field(type = "Option<toml::Value>", build = "()"), private)]
80 tokio_console: (),
81
82 #[builder_field_attr(serde(default))]
86 #[builder(sub_builder(fn_name = "build"), setter(custom))]
87 files: LogfileListConfig,
88
89 #[builder_field_attr(serde(default))]
101 #[builder(default)]
102 log_sensitive_information: bool,
103
104 #[builder(default = "std::time::Duration::new(1,0)")]
114 #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
115 time_granularity: std::time::Duration,
116}
117impl_standard_builder! { LoggingConfig }
118
119impl LoggingConfigBuilder {
120 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
122 pub(crate) fn build(&self) -> Result<LoggingConfig, ConfigBuildError> {
123 let config = self.build_unvalidated()?;
124
125 #[cfg(not(feature = "tokio-console"))]
126 if self.tokio_console.is_some() {
127 tracing::warn!(
128 "tokio-console options were set, but Arti was built without support for tokio-console."
129 );
130 }
131
132 #[cfg(not(feature = "opentelemetry"))]
133 if self.opentelemetry.is_some() {
134 tracing::warn!(
135 "opentelemetry options were set, but Arti was built without support for opentelemetry."
136 );
137 }
138
139 Ok(config)
140 }
141}
142
143#[allow(clippy::unnecessary_wraps)]
145fn default_console_filter() -> Option<String> {
146 Some("info".to_owned())
147}
148
149type LogfileListConfig = Vec<LogfileConfig>;
151
152define_list_builder_helper! {
153 struct LogfileListConfigBuilder {
154 files: [LogfileConfigBuilder],
155 }
156 built: LogfileListConfig = files;
157 default = vec![];
158}
159
160define_list_builder_accessors! {
161 struct LoggingConfigBuilder {
162 pub files: [LogfileConfigBuilder],
163 }
164}
165
166#[derive(Debug, Builder, Clone, Eq, PartialEq)]
168#[builder(derive(Debug, Serialize, Deserialize))]
169#[builder(build_fn(error = "ConfigBuildError"))]
170#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
171#[cfg_attr(feature = "experimental-api", builder(public))]
172pub(crate) struct LogfileConfig {
173 #[builder(default)]
175 rotate: LogRotation,
176 path: CfgPath,
178 filter: String,
180}
181
182impl_standard_builder! { LogfileConfig: !Default }
183
184#[derive(Debug, Default, Clone, Serialize, Deserialize, Copy, Eq, PartialEq)]
186#[non_exhaustive]
187#[serde(rename_all = "lowercase")]
188#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
189pub(crate) enum LogRotation {
190 Daily,
192 Hourly,
194 #[default]
196 Never,
197}
198
199#[derive(Debug, Builder, Clone, Eq, PartialEq, Serialize, Deserialize)]
201#[builder(derive(Debug, Serialize, Deserialize))]
202#[builder(build_fn(error = "ConfigBuildError"))]
203#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
204#[cfg_attr(feature = "experimental-api", builder(public))]
205pub(crate) struct OpentelemetryConfig {
206 #[builder(default)]
208 file: Option<OpentelemetryFileExporterConfig>,
209 #[builder(default)]
211 http: Option<OpentelemetryHttpExporterConfig>,
212}
213impl_standard_builder! { OpentelemetryConfig }
214
215#[derive(Debug, Builder, Clone, Eq, PartialEq, Serialize, Deserialize)]
217#[builder(derive(Debug, Serialize, Deserialize))]
218#[builder(build_fn(error = "ConfigBuildError"))]
219#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
220#[cfg_attr(feature = "experimental-api", builder(public))]
221pub(crate) struct OpentelemetryHttpExporterConfig {
222 endpoint: String,
226 batch: Option<OpentelemetryBatchConfig>,
229 #[serde(default)]
234 #[serde(with = "humantime_serde")]
235 timeout: Option<Duration>,
236 }
239impl_standard_builder! { OpentelemetryHttpExporterConfig: !Default }
240
241#[derive(Debug, Builder, Clone, Eq, PartialEq, Serialize, Deserialize)]
243#[builder(derive(Debug, Serialize, Deserialize))]
244#[builder(build_fn(error = "ConfigBuildError"))]
245#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
246#[cfg_attr(feature = "experimental-api", builder(public))]
247pub(crate) struct OpentelemetryFileExporterConfig {
248 path: CfgPath,
250 batch: Option<OpentelemetryBatchConfig>,
253}
254impl_standard_builder! { OpentelemetryFileExporterConfig: !Default }
255
256#[derive(Debug, Builder, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
260#[builder(derive(Debug, Serialize, Deserialize))]
261#[builder(build_fn(error = "ConfigBuildError"))]
262#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
263#[cfg_attr(feature = "experimental-api", builder(public))]
264pub(crate) struct OpentelemetryBatchConfig {
265 #[builder(default)]
267 max_queue_size: Option<usize>,
268 #[builder(default)]
270 max_export_batch_size: Option<usize>,
271 #[builder(default)]
273 #[serde(with = "humantime_serde")]
274 scheduled_delay: Option<Duration>,
275}
276impl_standard_builder! { OpentelemetryBatchConfig }
277
278#[cfg(feature = "opentelemetry")]
279impl From<OpentelemetryBatchConfig> for opentelemetry_sdk::trace::BatchConfig {
280 fn from(config: OpentelemetryBatchConfig) -> opentelemetry_sdk::trace::BatchConfig {
281 let batch_config = opentelemetry_sdk::trace::BatchConfigBuilder::default();
282
283 let batch_config = if let Some(max_queue_size) = config.max_queue_size {
284 batch_config.with_max_queue_size(max_queue_size)
285 } else {
286 batch_config
287 };
288
289 let batch_config = if let Some(max_export_batch_size) = config.max_export_batch_size {
290 batch_config.with_max_export_batch_size(max_export_batch_size)
291 } else {
292 batch_config
293 };
294
295 let batch_config = if let Some(scheduled_delay) = config.scheduled_delay {
296 batch_config.with_scheduled_delay(scheduled_delay)
297 } else {
298 batch_config
299 };
300
301 batch_config.build()
302 }
303}
304
305#[derive(Debug, Builder, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
307#[builder(derive(Debug, Serialize, Deserialize))]
308#[builder(build_fn(error = "ConfigBuildError"))]
309#[cfg(feature = "tokio-console")]
310#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
311#[cfg_attr(feature = "experimental-api", builder(public))]
312pub(crate) struct TokioConsoleConfig {
313 #[builder(default)]
318 enabled: bool,
319}
320
321fn filt_from_str_verbose(s: &str, source: &str) -> Result<Targets> {
326 Targets::from_str(s).with_context(|| format!("in {}", source))
327}
328
329fn filt_from_opt_str(s: &Option<String>, source: &str) -> Result<Option<Targets>> {
332 Ok(match s {
333 Some(s) if !s.is_empty() => Some(filt_from_str_verbose(s, source)?),
334 _ => None,
335 })
336}
337
338fn console_layer<S>(config: &LoggingConfig, cli: Option<&str>) -> Result<impl Layer<S> + use<S>>
340where
341 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
342{
343 let timer = time::new_formatter(config.time_granularity);
344 let filter = cli
345 .map(|s| filt_from_str_verbose(s, "--log-level command line parameter"))
346 .or_else(|| filt_from_opt_str(&config.console, "logging.console").transpose())
347 .unwrap_or_else(|| Ok(Targets::from_str("debug").expect("bad default")))?;
348 let use_color = std::io::stderr().is_terminal();
349 Ok(fmt::Layer::default()
354 .fmt_fields(fields::ErrorsLastFieldFormatter)
356 .with_ansi(use_color)
357 .with_timer(timer)
358 .with_writer(std::io::stderr) .with_filter(filter))
360}
361
362#[cfg(feature = "journald")]
365fn journald_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
366where
367 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
368{
369 if let Some(filter) = filt_from_opt_str(&config.journald, "logging.journald")? {
370 Ok(Some(tracing_journald::layer()?.with_filter(filter)))
371 } else {
372 Ok(None)
374 }
375}
376
377#[cfg(feature = "opentelemetry")]
382fn otel_layer<S>(config: &LoggingConfig, path_resolver: &CfgPathResolver) -> Result<impl Layer<S>>
383where
384 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
385{
386 use opentelemetry::trace::TracerProvider;
387 use opentelemetry_otlp::WithExportConfig;
388
389 if config.opentelemetry.file.is_some() && config.opentelemetry.http.is_some() {
390 return Err(ConfigBuildError::Invalid {
391 field: "logging.opentelemetry".into(),
392 problem: "Only one OpenTelemetry exporter can be enabled at once.".into(),
393 }
394 .into());
395 }
396
397 let resource = opentelemetry_sdk::Resource::builder()
398 .with_service_name("arti")
399 .build();
400
401 let span_processor = if let Some(otel_file_config) = &config.opentelemetry.file {
402 let file = std::fs::File::options()
403 .create(true)
404 .append(true)
405 .open(otel_file_config.path.path(path_resolver)?)?;
406
407 let exporter = otlp_file_exporter::FileExporter::new(file, resource.clone());
408
409 opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
410 .with_batch_config(otel_file_config.batch.unwrap_or_default().into())
411 .build()
412 } else if let Some(otel_http_config) = &config.opentelemetry.http {
413 if otel_http_config.endpoint.starts_with("http://")
414 && !(otel_http_config.endpoint.starts_with("http://localhost")
415 || otel_http_config.endpoint.starts_with("http://127.0.0.1"))
416 {
417 return Err(ConfigBuildError::Invalid {
418 field: "logging.opentelemetry.http.endpoint".into(),
419 problem: "OpenTelemetry endpoint is set to HTTP on a non-localhost address! For security reasons, this is not supported.".into(),
420 }
421 .into());
422 }
423 let exporter = opentelemetry_otlp::SpanExporter::builder()
424 .with_http()
425 .with_endpoint(otel_http_config.endpoint.clone());
426
427 let exporter = if let Some(timeout) = otel_http_config.timeout {
428 exporter.with_timeout(timeout)
429 } else {
430 exporter
431 };
432
433 let exporter = exporter.build()?;
434
435 opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
436 .with_batch_config(otel_http_config.batch.unwrap_or_default().into())
437 .build()
438 } else {
439 return Ok(None);
440 };
441
442 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
443 .with_resource(resource.clone())
444 .with_span_processor(span_processor)
445 .build();
446
447 let tracer = tracer_provider.tracer("otel_file_tracer");
448
449 Ok(Some(tracing_opentelemetry::layer().with_tracer(tracer)))
450}
451
452fn logfile_layer<S>(
458 config: &LogfileConfig,
459 granularity: std::time::Duration,
460 mistrust: &Mistrust,
461 path_resolver: &CfgPathResolver,
462) -> Result<(impl Layer<S> + Send + Sync + Sized + use<S>, WorkerGuard)>
463where
464 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
465{
466 use tracing_appender::{
467 non_blocking,
468 rolling::{RollingFileAppender, Rotation},
469 };
470 let timer = time::new_formatter(granularity);
471
472 let filter = filt_from_str_verbose(&config.filter, "logging.files.filter")?;
473 let rotation = match config.rotate {
474 LogRotation::Daily => Rotation::DAILY,
475 LogRotation::Hourly => Rotation::HOURLY,
476 _ => Rotation::NEVER,
477 };
478 let path = config.path.path(path_resolver)?;
479
480 let directory = match path.parent() {
481 None => {
482 return Err(anyhow!(
483 "Logfile path \"{}\" did not have a parent directory",
484 path.display_lossy()
485 ));
486 }
487 Some(p) if p == Path::new("") => Path::new("."),
488 Some(d) => d,
489 };
490 mistrust.make_directory(directory).with_context(|| {
491 format!(
492 "Unable to create parent directory for logfile \"{}\"",
493 path.display_lossy()
494 )
495 })?;
496 let fname = path
497 .file_name()
498 .ok_or_else(|| anyhow!("No path for log file"))
499 .map(Path::new)?;
500
501 let appender = RollingFileAppender::new(rotation, directory, fname);
502 let (nonblocking, guard) = non_blocking(appender);
503 let layer = fmt::layer()
504 .fmt_fields(fields::ErrorsLastFieldFormatter)
506 .with_ansi(false)
507 .with_writer(nonblocking)
508 .with_timer(timer)
509 .with_filter(filter);
510 Ok((layer, guard))
511}
512
513fn logfile_layers<S>(
518 config: &LoggingConfig,
519 mistrust: &Mistrust,
520 path_resolver: &CfgPathResolver,
521) -> Result<(impl Layer<S> + use<S>, Vec<WorkerGuard>)>
522where
523 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
524{
525 let mut guards = Vec::new();
526 if config.files.is_empty() {
527 return Ok((None, guards));
530 }
531
532 let (layer, guard) = logfile_layer(
533 &config.files[0],
534 config.time_granularity,
535 mistrust,
536 path_resolver,
537 )?;
538 guards.push(guard);
539
540 let mut layer: Box<dyn Layer<S> + Send + Sync + 'static> = Box::new(layer);
543
544 for logfile in &config.files[1..] {
545 let (new_layer, guard) =
546 logfile_layer(logfile, config.time_granularity, mistrust, path_resolver)?;
547 layer = Box::new(layer.and_then(new_layer));
548 guards.push(guard);
549 }
550
551 Ok((Some(layer), guards))
552}
553
554fn install_panic_handler() {
557 let default_handler = std::panic::take_hook();
563 std::panic::set_hook(Box::new(move |panic_info| {
564 default_handler(panic_info);
567
568 let msg = match panic_info.payload().downcast_ref::<&'static str>() {
570 Some(s) => *s,
571 None => match panic_info.payload().downcast_ref::<String>() {
572 Some(s) => &s[..],
573 None => "Box<dyn Any>",
574 },
575 };
576
577 let backtrace = std::backtrace::Backtrace::force_capture();
578 match panic_info.location() {
579 Some(location) => error!("Panic at {}: {}\n{}", location, msg, backtrace),
580 None => error!("Panic at ???: {}\n{}", msg, backtrace),
581 };
582 }));
583}
584
585#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
588pub(crate) struct LogGuards {
589 #[allow(unused)]
591 guards: Vec<WorkerGuard>,
592
593 #[allow(unused)]
595 safelog_guard: Option<safelog::Guard>,
596}
597
598#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
603#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
604pub(crate) fn setup_logging(
605 config: &LoggingConfig,
606 mistrust: &Mistrust,
607 path_resolver: &CfgPathResolver,
608 cli: Option<&str>,
609) -> Result<LogGuards> {
610 let registry = registry().with(console_layer(config, cli)?);
619
620 #[cfg(feature = "journald")]
621 let registry = registry.with(journald_layer(config)?);
622
623 #[cfg(feature = "opentelemetry")]
624 let registry = registry.with(otel_layer(config, path_resolver)?);
625
626 #[cfg(feature = "tokio-console")]
627 let registry = {
628 let tokio_layer = if config.tokio_console.enabled {
635 Some(console_subscriber::spawn())
636 } else {
637 None
638 };
639 registry.with(tokio_layer)
640 };
641
642 let (layer, guards) = logfile_layers(config, mistrust, path_resolver)?;
643 let registry = registry.with(layer);
644
645 registry.init();
646
647 let safelog_guard = if config.log_sensitive_information {
648 match safelog::disable_safe_logging() {
649 Ok(guard) => Some(guard),
650 Err(e) => {
651 warn_report!(e, "Unable to disable safe logging");
654 None
655 }
656 }
657 } else {
658 None
659 };
660
661 install_panic_handler();
662
663 Ok(LogGuards {
664 guards,
665 safelog_guard,
666 })
667}