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))]
33pub struct LoggingConfig {
34 #[builder(default = "default_console_filter()", setter(into, strip_option))]
41 console: Option<String>,
42
43 #[builder(
47 setter(into),
48 field(build = r#"tor_config::resolve_option(&self.journald, || None)"#)
49 )]
50 journald: Option<String>,
51
52 #[cfg(feature = "opentelemetry")]
54 #[builder_field_attr(serde(default))]
55 #[builder(default)]
56 opentelemetry: OpentelemetryConfig,
57
58 #[cfg(not(feature = "opentelemetry"))]
62 #[builder_field_attr(serde(default))]
63 #[builder(field(type = "Option<toml::Value>", build = "()"), private)]
64 opentelemetry: (),
65
66 #[cfg(feature = "tokio-console")]
68 #[builder(sub_builder(fn_name = "build"))]
69 #[builder_field_attr(serde(default))]
70 tokio_console: TokioConsoleConfig,
71
72 #[cfg(not(feature = "tokio-console"))]
76 #[builder_field_attr(serde(default))]
77 #[builder(field(type = "Option<toml::Value>", build = "()"), private)]
78 tokio_console: (),
79
80 #[builder_field_attr(serde(default))]
84 #[builder(sub_builder(fn_name = "build"), setter(custom))]
85 files: LogfileListConfig,
86
87 #[builder_field_attr(serde(default))]
99 #[builder(default)]
100 log_sensitive_information: bool,
101
102 #[builder(default = "std::time::Duration::new(1,0)")]
112 #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
113 time_granularity: std::time::Duration,
114}
115impl_standard_builder! { LoggingConfig }
116
117impl LoggingConfigBuilder {
118 pub fn build(&self) -> Result<LoggingConfig, ConfigBuildError> {
120 let config = self.build_unvalidated()?;
121
122 #[cfg(not(feature = "tokio-console"))]
123 if self.tokio_console.is_some() {
124 tracing::warn!(
125 "tokio-console options were set, but Arti was built without support for tokio-console."
126 );
127 }
128
129 #[cfg(not(feature = "opentelemetry"))]
130 if self.opentelemetry.is_some() {
131 tracing::warn!(
132 "opentelemetry options were set, but Arti was built without support for opentelemetry."
133 );
134 }
135
136 Ok(config)
137 }
138}
139
140#[allow(clippy::unnecessary_wraps)]
142fn default_console_filter() -> Option<String> {
143 Some("info".to_owned())
144}
145
146type LogfileListConfig = Vec<LogfileConfig>;
148
149define_list_builder_helper! {
150 struct LogfileListConfigBuilder {
151 files: [LogfileConfigBuilder],
152 }
153 built: LogfileListConfig = files;
154 default = vec![];
155}
156
157define_list_builder_accessors! {
158 struct LoggingConfigBuilder {
159 pub files: [LogfileConfigBuilder],
160 }
161}
162
163#[derive(Debug, Builder, Clone, Eq, PartialEq)]
165#[builder(derive(Debug, Serialize, Deserialize))]
166#[builder(build_fn(error = "ConfigBuildError"))]
167pub struct LogfileConfig {
168 #[builder(default)]
170 rotate: LogRotation,
171 path: CfgPath,
173 filter: String,
175}
176
177impl_standard_builder! { LogfileConfig: !Default }
178
179#[derive(Debug, Default, Clone, Serialize, Deserialize, Copy, Eq, PartialEq)]
181#[non_exhaustive]
182#[serde(rename_all = "lowercase")]
183pub enum LogRotation {
184 Daily,
186 Hourly,
188 #[default]
190 Never,
191}
192
193#[derive(Debug, Builder, Clone, Eq, PartialEq, Serialize, Deserialize)]
195#[builder(derive(Debug, Serialize, Deserialize))]
196#[builder(build_fn(error = "ConfigBuildError"))]
197pub struct OpentelemetryConfig {
198 #[builder(default)]
200 file: Option<OpentelemetryFileExporterConfig>,
201 #[builder(default)]
203 http: Option<OpentelemetryHttpExporterConfig>,
204}
205impl_standard_builder! { OpentelemetryConfig }
206
207#[derive(Debug, Builder, Clone, Eq, PartialEq, Serialize, Deserialize)]
209#[builder(derive(Debug, Serialize, Deserialize))]
210#[builder(build_fn(error = "ConfigBuildError"))]
211pub struct OpentelemetryHttpExporterConfig {
212 endpoint: String,
216 batch: Option<OpentelemetryBatchConfig>,
219 #[serde(default)]
224 #[serde(with = "humantime_serde")]
225 timeout: Option<Duration>,
226 }
229impl_standard_builder! { OpentelemetryHttpExporterConfig: !Default }
230
231#[derive(Debug, Builder, Clone, Eq, PartialEq, Serialize, Deserialize)]
233#[builder(derive(Debug, Serialize, Deserialize))]
234#[builder(build_fn(error = "ConfigBuildError"))]
235pub struct OpentelemetryFileExporterConfig {
236 path: CfgPath,
238 batch: Option<OpentelemetryBatchConfig>,
241}
242impl_standard_builder! { OpentelemetryFileExporterConfig: !Default }
243
244#[derive(Debug, Builder, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
248#[builder(derive(Debug, Serialize, Deserialize))]
249#[builder(build_fn(error = "ConfigBuildError"))]
250pub struct OpentelemetryBatchConfig {
251 #[builder(default)]
253 max_queue_size: Option<usize>,
254 #[builder(default)]
256 max_export_batch_size: Option<usize>,
257 #[builder(default)]
259 #[serde(with = "humantime_serde")]
260 scheduled_delay: Option<Duration>,
261}
262impl_standard_builder! { OpentelemetryBatchConfig }
263
264#[cfg(feature = "opentelemetry")]
265impl From<OpentelemetryBatchConfig> for opentelemetry_sdk::trace::BatchConfig {
266 fn from(config: OpentelemetryBatchConfig) -> opentelemetry_sdk::trace::BatchConfig {
267 let batch_config = opentelemetry_sdk::trace::BatchConfigBuilder::default();
268
269 let batch_config = if let Some(max_queue_size) = config.max_queue_size {
270 batch_config.with_max_queue_size(max_queue_size)
271 } else {
272 batch_config
273 };
274
275 let batch_config = if let Some(max_export_batch_size) = config.max_export_batch_size {
276 batch_config.with_max_export_batch_size(max_export_batch_size)
277 } else {
278 batch_config
279 };
280
281 let batch_config = if let Some(scheduled_delay) = config.scheduled_delay {
282 batch_config.with_scheduled_delay(scheduled_delay)
283 } else {
284 batch_config
285 };
286
287 batch_config.build()
288 }
289}
290
291#[derive(Debug, Builder, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
293#[builder(derive(Debug, Serialize, Deserialize))]
294#[builder(build_fn(error = "ConfigBuildError"))]
295#[cfg(feature = "tokio-console")]
296pub struct TokioConsoleConfig {
297 #[builder(default)]
302 enabled: bool,
303}
304
305fn filt_from_str_verbose(s: &str, source: &str) -> Result<Targets> {
310 Targets::from_str(s).with_context(|| format!("in {}", source))
311}
312
313fn filt_from_opt_str(s: &Option<String>, source: &str) -> Result<Option<Targets>> {
316 Ok(match s {
317 Some(s) if !s.is_empty() => Some(filt_from_str_verbose(s, source)?),
318 _ => None,
319 })
320}
321
322fn console_layer<S>(config: &LoggingConfig, cli: Option<&str>) -> Result<impl Layer<S> + use<S>>
324where
325 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
326{
327 let timer = time::new_formatter(config.time_granularity);
328 let filter = cli
329 .map(|s| filt_from_str_verbose(s, "--log-level command line parameter"))
330 .or_else(|| filt_from_opt_str(&config.console, "logging.console").transpose())
331 .unwrap_or_else(|| Ok(Targets::from_str("debug").expect("bad default")))?;
332 let use_color = std::io::stderr().is_terminal();
333 Ok(fmt::Layer::default()
338 .fmt_fields(fields::ErrorsLastFieldFormatter)
340 .with_ansi(use_color)
341 .with_timer(timer)
342 .with_writer(std::io::stderr) .with_filter(filter))
344}
345
346#[cfg(feature = "journald")]
349fn journald_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
350where
351 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
352{
353 if let Some(filter) = filt_from_opt_str(&config.journald, "logging.journald")? {
354 Ok(Some(tracing_journald::layer()?.with_filter(filter)))
355 } else {
356 Ok(None)
358 }
359}
360
361#[cfg(feature = "opentelemetry")]
366fn otel_layer<S>(config: &LoggingConfig, path_resolver: &CfgPathResolver) -> Result<impl Layer<S>>
367where
368 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
369{
370 use opentelemetry::trace::TracerProvider;
371 use opentelemetry_otlp::WithExportConfig;
372
373 if config.opentelemetry.file.is_some() && config.opentelemetry.http.is_some() {
374 return Err(ConfigBuildError::Invalid {
375 field: "logging.opentelemetry".into(),
376 problem: "Only one OpenTelemetry exporter can be enabled at once.".into(),
377 }
378 .into());
379 }
380
381 let resource = opentelemetry_sdk::Resource::builder()
382 .with_service_name("arti")
383 .build();
384
385 let span_processor = if let Some(otel_file_config) = &config.opentelemetry.file {
386 let file = std::fs::File::options()
387 .create(true)
388 .append(true)
389 .open(otel_file_config.path.path(path_resolver)?)?;
390
391 let exporter = otlp_file_exporter::FileExporter::new(file, resource.clone());
392
393 opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
394 .with_batch_config(otel_file_config.batch.unwrap_or_default().into())
395 .build()
396 } else if let Some(otel_http_config) = &config.opentelemetry.http {
397 if otel_http_config.endpoint.starts_with("http://")
398 && !(otel_http_config.endpoint.starts_with("http://localhost")
399 || otel_http_config.endpoint.starts_with("http://127.0.0.1"))
400 {
401 return Err(ConfigBuildError::Invalid {
402 field: "logging.opentelemetry.http.endpoint".into(),
403 problem: "OpenTelemetry endpoint is set to HTTP on a non-localhost address! For security reasons, this is not supported.".into(),
404 }
405 .into());
406 }
407 let exporter = opentelemetry_otlp::SpanExporter::builder()
408 .with_http()
409 .with_endpoint(otel_http_config.endpoint.clone());
410
411 let exporter = if let Some(timeout) = otel_http_config.timeout {
412 exporter.with_timeout(timeout)
413 } else {
414 exporter
415 };
416
417 let exporter = exporter.build()?;
418
419 opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
420 .with_batch_config(otel_http_config.batch.unwrap_or_default().into())
421 .build()
422 } else {
423 return Ok(None);
424 };
425
426 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
427 .with_resource(resource.clone())
428 .with_span_processor(span_processor)
429 .build();
430
431 let tracer = tracer_provider.tracer("otel_file_tracer");
432
433 Ok(Some(tracing_opentelemetry::layer().with_tracer(tracer)))
434}
435
436fn logfile_layer<S>(
442 config: &LogfileConfig,
443 granularity: std::time::Duration,
444 mistrust: &Mistrust,
445 path_resolver: &CfgPathResolver,
446) -> Result<(impl Layer<S> + Send + Sync + Sized + use<S>, WorkerGuard)>
447where
448 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
449{
450 use tracing_appender::{
451 non_blocking,
452 rolling::{RollingFileAppender, Rotation},
453 };
454 let timer = time::new_formatter(granularity);
455
456 let filter = filt_from_str_verbose(&config.filter, "logging.files.filter")?;
457 let rotation = match config.rotate {
458 LogRotation::Daily => Rotation::DAILY,
459 LogRotation::Hourly => Rotation::HOURLY,
460 _ => Rotation::NEVER,
461 };
462 let path = config.path.path(path_resolver)?;
463
464 let directory = match path.parent() {
465 None => {
466 return Err(anyhow!(
467 "Logfile path \"{}\" did not have a parent directory",
468 path.display_lossy()
469 ));
470 }
471 Some(p) if p == Path::new("") => Path::new("."),
472 Some(d) => d,
473 };
474 mistrust.make_directory(directory).with_context(|| {
475 format!(
476 "Unable to create parent directory for logfile \"{}\"",
477 path.display_lossy()
478 )
479 })?;
480 let fname = path
481 .file_name()
482 .ok_or_else(|| anyhow!("No path for log file"))
483 .map(Path::new)?;
484
485 let appender = RollingFileAppender::new(rotation, directory, fname);
486 let (nonblocking, guard) = non_blocking(appender);
487 let layer = fmt::layer()
488 .fmt_fields(fields::ErrorsLastFieldFormatter)
490 .with_ansi(false)
491 .with_writer(nonblocking)
492 .with_timer(timer)
493 .with_filter(filter);
494 Ok((layer, guard))
495}
496
497fn logfile_layers<S>(
502 config: &LoggingConfig,
503 mistrust: &Mistrust,
504 path_resolver: &CfgPathResolver,
505) -> Result<(impl Layer<S> + use<S>, Vec<WorkerGuard>)>
506where
507 S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
508{
509 let mut guards = Vec::new();
510 if config.files.is_empty() {
511 return Ok((None, guards));
514 }
515
516 let (layer, guard) = logfile_layer(
517 &config.files[0],
518 config.time_granularity,
519 mistrust,
520 path_resolver,
521 )?;
522 guards.push(guard);
523
524 let mut layer: Box<dyn Layer<S> + Send + Sync + 'static> = Box::new(layer);
527
528 for logfile in &config.files[1..] {
529 let (new_layer, guard) =
530 logfile_layer(logfile, config.time_granularity, mistrust, path_resolver)?;
531 layer = Box::new(layer.and_then(new_layer));
532 guards.push(guard);
533 }
534
535 Ok((Some(layer), guards))
536}
537
538fn install_panic_handler() {
541 let default_handler = std::panic::take_hook();
547 std::panic::set_hook(Box::new(move |panic_info| {
548 default_handler(panic_info);
551
552 let msg = match panic_info.payload().downcast_ref::<&'static str>() {
554 Some(s) => *s,
555 None => match panic_info.payload().downcast_ref::<String>() {
556 Some(s) => &s[..],
557 None => "Box<dyn Any>",
558 },
559 };
560
561 let backtrace = std::backtrace::Backtrace::force_capture();
562 match panic_info.location() {
563 Some(location) => error!("Panic at {}: {}\n{}", location, msg, backtrace),
564 None => error!("Panic at ???: {}\n{}", msg, backtrace),
565 };
566 }));
567}
568
569#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
572pub(crate) struct LogGuards {
573 #[allow(unused)]
575 guards: Vec<WorkerGuard>,
576
577 #[allow(unused)]
579 safelog_guard: Option<safelog::Guard>,
580}
581
582#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
587#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
588pub(crate) fn setup_logging(
589 config: &LoggingConfig,
590 mistrust: &Mistrust,
591 path_resolver: &CfgPathResolver,
592 cli: Option<&str>,
593) -> Result<LogGuards> {
594 let registry = registry().with(console_layer(config, cli)?);
603
604 #[cfg(feature = "journald")]
605 let registry = registry.with(journald_layer(config)?);
606
607 #[cfg(feature = "opentelemetry")]
608 let registry = registry.with(otel_layer(config, path_resolver)?);
609
610 #[cfg(feature = "tokio-console")]
611 let registry = {
612 let tokio_layer = if config.tokio_console.enabled {
619 Some(console_subscriber::spawn())
620 } else {
621 None
622 };
623 registry.with(tokio_layer)
624 };
625
626 let (layer, guards) = logfile_layers(config, mistrust, path_resolver)?;
627 let registry = registry.with(layer);
628
629 registry.init();
630
631 let safelog_guard = if config.log_sensitive_information {
632 match safelog::disable_safe_logging() {
633 Ok(guard) => Some(guard),
634 Err(e) => {
635 warn_report!(e, "Unable to disable safe logging");
638 None
639 }
640 }
641 } else {
642 None
643 };
644
645 install_panic_handler();
646
647 Ok(LogGuards {
648 guards,
649 safelog_guard,
650 })
651}