use anyhow::{Context, Result, anyhow};
use derive_deftly::Deftly;
use fs_mistrust::Mistrust;
use serde::{Deserialize, Serialize};
use std::io::IsTerminal as _;
use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
use tor_basic_utils::PathExt as _;
use tor_config::ConfigBuildError;
use tor_config::derive::prelude::*;
use tor_config_path::{CfgPath, CfgPathResolver};
use tor_error::warn_report;
use tracing::{Subscriber, error};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{Layer, filter::Targets, fmt, registry};
mod fields;
#[cfg(feature = "opentelemetry")]
mod otlp_file_exporter;
mod time;
#[derive(Debug, Clone, Deftly, Eq, PartialEq)]
#[derive_deftly(TorConfig)]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct LoggingConfig {
#[deftly(tor_config(default = "default_console_filter()"))]
console: Option<String>,
#[deftly(tor_config(
build = r#"|this: &Self| tor_config::resolve_option(&this.journald, || None)"#
))]
journald: Option<String>,
#[deftly(tor_config(
sub_builder,
cfg = r#" feature = "opentelemetry" "#,
cfg_desc = "with opentelemetry support"
))]
opentelemetry: OpentelemetryConfig,
#[deftly(tor_config(
sub_builder,
cfg = r#" feature = "tokio-console" "#,
cfg_desc = "with tokio-console support"
))]
tokio_console: TokioConsoleConfig,
#[deftly(tor_config(list(element(build), listtype = "LogfileList"), default = "vec![]"))]
files: Vec<LogfileConfig>,
#[deftly(tor_config(default))]
log_sensitive_information: bool,
#[deftly(tor_config(default = "std::time::Duration::new(1,0)"))]
time_granularity: std::time::Duration,
}
#[allow(clippy::unnecessary_wraps)]
fn default_console_filter() -> Option<String> {
Some("info".to_owned())
}
#[derive(Debug, Deftly, Clone, Eq, PartialEq)]
#[derive_deftly(TorConfig)]
#[deftly(tor_config(no_default_trait))]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct LogfileConfig {
#[deftly(tor_config(default))]
rotate: LogRotation,
#[deftly(tor_config(no_default))]
path: CfgPath,
#[deftly(tor_config(no_default))]
filter: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, Copy, Eq, PartialEq)]
#[non_exhaustive]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) enum LogRotation {
Daily,
Hourly,
#[default]
Never,
}
#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[derive_deftly(TorConfig)]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct OpentelemetryConfig {
#[deftly(tor_config(default))]
file: Option<OpentelemetryFileExporterConfig>,
#[deftly(tor_config(default))]
http: Option<OpentelemetryHttpExporterConfig>,
}
#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[derive_deftly(TorConfig)]
#[deftly(tor_config(no_default_trait))]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct OpentelemetryHttpExporterConfig {
#[deftly(tor_config(no_default))]
endpoint: String,
#[deftly(tor_config(sub_builder))]
batch: OpentelemetryBatchConfig,
#[deftly(tor_config(no_magic, default))]
timeout: Option<Duration>,
}
#[derive(Debug, Deftly, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[derive_deftly(TorConfig)]
#[deftly(tor_config(no_default_trait))]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct OpentelemetryFileExporterConfig {
#[deftly(tor_config(no_default))]
path: CfgPath,
#[deftly(tor_config(sub_builder))]
batch: OpentelemetryBatchConfig,
}
#[derive(Debug, Deftly, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[derive_deftly(TorConfig)]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct OpentelemetryBatchConfig {
#[deftly(tor_config(default))]
max_queue_size: Option<usize>,
#[deftly(tor_config(default))]
max_export_batch_size: Option<usize>,
#[deftly(tor_config(no_magic, default))]
scheduled_delay: Option<Duration>,
}
#[cfg(feature = "opentelemetry")]
impl From<OpentelemetryBatchConfig> for opentelemetry_sdk::trace::BatchConfig {
fn from(config: OpentelemetryBatchConfig) -> opentelemetry_sdk::trace::BatchConfig {
let batch_config = opentelemetry_sdk::trace::BatchConfigBuilder::default();
let batch_config = if let Some(max_queue_size) = config.max_queue_size {
batch_config.with_max_queue_size(max_queue_size)
} else {
batch_config
};
let batch_config = if let Some(max_export_batch_size) = config.max_export_batch_size {
batch_config.with_max_export_batch_size(max_export_batch_size)
} else {
batch_config
};
let batch_config = if let Some(scheduled_delay) = config.scheduled_delay {
batch_config.with_scheduled_delay(scheduled_delay)
} else {
batch_config
};
batch_config.build()
}
}
#[derive(Debug, Deftly, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[derive_deftly(TorConfig)]
#[cfg(feature = "tokio-console")]
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(feature = "experimental-api", deftly(tor_config(vis = "pub")))]
pub(crate) struct TokioConsoleConfig {
#[deftly(tor_config(default))]
enabled: bool,
}
#[cfg(not(feature = "tokio-console"))]
type TokioConsoleConfig = ();
fn filt_from_str_verbose(s: &str, source: &str) -> Result<Targets> {
Targets::from_str(s).with_context(|| format!("in {}", source))
}
fn filt_from_opt_str(s: &Option<String>, source: &str) -> Result<Option<Targets>> {
Ok(match s {
Some(s) if !s.is_empty() => Some(filt_from_str_verbose(s, source)?),
_ => None,
})
}
fn console_layer<S>(config: &LoggingConfig, cli: Option<&str>) -> Result<impl Layer<S> + use<S>>
where
S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
let timer = time::new_formatter(config.time_granularity);
let filter = cli
.map(|s| filt_from_str_verbose(s, "--log-level command line parameter"))
.or_else(|| filt_from_opt_str(&config.console, "logging.console").transpose())
.unwrap_or_else(|| Ok(Targets::from_str("debug").expect("bad default")))?;
let use_color = std::io::stderr().is_terminal();
Ok(fmt::Layer::default()
.fmt_fields(fields::ErrorsLastFieldFormatter)
.with_ansi(use_color)
.with_timer(timer)
.with_writer(std::io::stderr) .with_filter(filter))
}
#[cfg(feature = "journald")]
fn journald_layer<S>(config: &LoggingConfig) -> Result<impl Layer<S>>
where
S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
if let Some(filter) = filt_from_opt_str(&config.journald, "logging.journald")? {
Ok(Some(tracing_journald::layer()?.with_filter(filter)))
} else {
Ok(None)
}
}
#[cfg(feature = "opentelemetry")]
fn otel_layer<S>(config: &LoggingConfig, path_resolver: &CfgPathResolver) -> Result<impl Layer<S>>
where
S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
use opentelemetry::trace::TracerProvider;
use opentelemetry_otlp::WithExportConfig;
if config.opentelemetry.file.is_some() && config.opentelemetry.http.is_some() {
return Err(ConfigBuildError::Invalid {
field: "logging.opentelemetry".into(),
problem: "Only one OpenTelemetry exporter can be enabled at once.".into(),
}
.into());
}
let resource = opentelemetry_sdk::Resource::builder()
.with_service_name("arti")
.build();
let span_processor = if let Some(otel_file_config) = &config.opentelemetry.file {
let file = std::fs::File::options()
.create(true)
.append(true)
.open(otel_file_config.path.path(path_resolver)?)?;
let exporter = otlp_file_exporter::FileExporter::new(file, resource.clone());
opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
.with_batch_config(otel_file_config.batch.into())
.build()
} else if let Some(otel_http_config) = &config.opentelemetry.http {
if otel_http_config.endpoint.starts_with("http://")
&& !(otel_http_config.endpoint.starts_with("http://localhost")
|| otel_http_config.endpoint.starts_with("http://127.0.0.1"))
{
return Err(ConfigBuildError::Invalid {
field: "logging.opentelemetry.http.endpoint".into(),
problem: "OpenTelemetry endpoint is set to HTTP on a non-localhost address! For security reasons, this is not supported.".into(),
}
.into());
}
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(otel_http_config.endpoint.clone());
let exporter = if let Some(timeout) = otel_http_config.timeout {
exporter.with_timeout(timeout)
} else {
exporter
};
let exporter = exporter.build()?;
opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
.with_batch_config(otel_http_config.batch.into())
.build()
} else {
return Ok(None);
};
let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_resource(resource.clone())
.with_span_processor(span_processor)
.build();
let tracer = tracer_provider.tracer("otel_file_tracer");
Ok(Some(tracing_opentelemetry::layer().with_tracer(tracer)))
}
fn logfile_layer<S>(
config: &LogfileConfig,
granularity: std::time::Duration,
mistrust: &Mistrust,
path_resolver: &CfgPathResolver,
) -> Result<(impl Layer<S> + Send + Sync + Sized + use<S>, WorkerGuard)>
where
S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
{
use tracing_appender::{
non_blocking,
rolling::{RollingFileAppender, Rotation},
};
let timer = time::new_formatter(granularity);
let filter = filt_from_str_verbose(&config.filter, "logging.files.filter")?;
let rotation = match config.rotate {
LogRotation::Daily => Rotation::DAILY,
LogRotation::Hourly => Rotation::HOURLY,
_ => Rotation::NEVER,
};
let path = config.path.path(path_resolver)?;
let directory = match path.parent() {
None => {
return Err(anyhow!(
"Logfile path \"{}\" did not have a parent directory",
path.display_lossy()
));
}
Some(p) if p == Path::new("") => Path::new("."),
Some(d) => d,
};
mistrust.make_directory(directory).with_context(|| {
format!(
"Unable to create parent directory for logfile \"{}\"",
path.display_lossy()
)
})?;
let fname = path
.file_name()
.ok_or_else(|| anyhow!("No path for log file"))
.map(Path::new)?;
let appender = RollingFileAppender::new(rotation, directory, fname);
let (nonblocking, guard) = non_blocking(appender);
let layer = fmt::layer()
.fmt_fields(fields::ErrorsLastFieldFormatter)
.with_ansi(false)
.with_writer(nonblocking)
.with_timer(timer)
.with_filter(filter);
Ok((layer, guard))
}
fn logfile_layers<S>(
config: &LoggingConfig,
mistrust: &Mistrust,
path_resolver: &CfgPathResolver,
) -> Result<(impl Layer<S> + use<S>, Vec<WorkerGuard>)>
where
S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span> + Send + Sync,
{
let mut guards = Vec::new();
if config.files.is_empty() {
return Ok((None, guards));
}
let (layer, guard) = logfile_layer(
&config.files[0],
config.time_granularity,
mistrust,
path_resolver,
)?;
guards.push(guard);
let mut layer: Box<dyn Layer<S> + Send + Sync + 'static> = Box::new(layer);
for logfile in &config.files[1..] {
let (new_layer, guard) =
logfile_layer(logfile, config.time_granularity, mistrust, path_resolver)?;
layer = Box::new(layer.and_then(new_layer));
guards.push(guard);
}
Ok((Some(layer), guards))
}
fn install_panic_handler() {
let default_handler = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
default_handler(panic_info);
let msg = match panic_info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => match panic_info.payload().downcast_ref::<String>() {
Some(s) => &s[..],
None => "Box<dyn Any>",
},
};
let backtrace = std::backtrace::Backtrace::force_capture();
match panic_info.location() {
Some(location) => error!("Panic at {}: {}\n{}", location, msg, backtrace),
None => error!("Panic at ???: {}\n{}", msg, backtrace),
};
}));
}
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) struct LogGuards {
#[allow(unused)]
guards: Vec<WorkerGuard>,
#[allow(unused)]
safelog_guard: Option<safelog::Guard>,
}
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
pub(crate) fn setup_logging(
config: &LoggingConfig,
mistrust: &Mistrust,
path_resolver: &CfgPathResolver,
cli: Option<&str>,
) -> Result<LogGuards> {
let registry = registry().with(console_layer(config, cli)?);
#[cfg(feature = "journald")]
let registry = registry.with(journald_layer(config)?);
#[cfg(feature = "opentelemetry")]
let registry = registry.with(otel_layer(config, path_resolver)?);
#[cfg(feature = "tokio-console")]
let registry = {
let tokio_layer = if config.tokio_console.enabled {
Some(console_subscriber::spawn())
} else {
None
};
registry.with(tokio_layer)
};
let (layer, guards) = logfile_layers(config, mistrust, path_resolver)?;
let registry = registry.with(layer);
registry.init();
let safelog_guard = if config.log_sensitive_information {
match safelog::disable_safe_logging() {
Ok(guard) => Some(guard),
Err(e) => {
warn_report!(e, "Unable to disable safe logging");
None
}
}
} else {
None
};
install_panic_handler();
Ok(LogGuards {
guards,
safelog_guard,
})
}