Files
firezone/rust/logging/src/file.rs
Thomas Eizinger bed625a312 chore(rust): make logging more ergonomic (#6237)
Setting up a logger is something that pretty much every entrypoint needs
to do, be it a test, a shared library embedded in another app or a
standalone application. Thus, it makes sense to introduce a dedicated
crate that allows us to bundle all the things together, how we want to
do logging.

This allows us to introduce convenience functions like
`firezone_logging::test` which allow you to construct a logger for a
test as a one-liner.

Crucially though, introducing `firezone-logging` gives us a place to
store a default log directive that silences very noisy crates. When
looking into a problem, it is common to start by simply setting the
log-filter to `debug`. Without further action, this floods the output
with logs from crates like `netlink_proto` on Linux. It is very unlikely
that those are the logs that you want to see. Without a preset filter,
the only alternative here is to explicitly turn off the log filter for
`netlink_proto` by typing something like
`RUST_LOG=netlink_proto=off,debug`. Especially when debugging issues
with customers, this is annoying.

Log filters can be overridden, i.e. a 2nd filter that matches the exact
same scope overrides a previous one. Thus, with this design it is still
possible to activate certain logs at runtime, even if they have silenced
by default.

I'd expect `firezone-logging` to attract more functionality in the
future. For example, we want to support re-loading of log-filters on
other platforms. Additionally, where logs get stored could also be
defined in this crate.

---------

Signed-off-by: Thomas Eizinger <thomas@eizinger.io>
Co-authored-by: Reactor Scram <ReactorScram@users.noreply.github.com>
2024-08-10 05:17:03 +00:00

166 lines
5.3 KiB
Rust

//! Connlib File Logger
//!
//! This module implements a file-based logger for connlib using tracing-appender.
//!
//! The log files are never rotated for the duration of the process; this prevents
//! tracing_appender from trying to prune old log files which triggers privacy
//! alerts in Apple app store submissions.
//!
//! Since these will be leaving the user's device, these logs should contain *only*
//! the necessary debugging information, and **not** any sensitive information,
//! including but not limited to:
//! - WiFi SSIDs
//! - Location information
//! - Device names
//! - Device serials
//! - MAC addresses
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{fs, io};
use time::OffsetDateTime;
use tracing::Subscriber;
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_subscriber::Layer;
const LOG_FILE_BASE_NAME: &str = "connlib";
pub const TIME_FORMAT: &str = "[year]-[month]-[day]-[hour]-[minute]-[second]";
/// Create a new file logger layer.
pub fn layer<T>(log_dir: &Path) -> (Box<dyn Layer<T> + Send + Sync + 'static>, Handle)
where
T: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
let (appender_json, handle_json) = new_appender(log_dir.to_path_buf(), "jsonl");
let layer_json = tracing_stackdriver::layer()
.with_writer(appender_json)
.boxed();
let (appender_fmt, handle_fmt) = new_appender(log_dir.to_path_buf(), "log");
let layer_fmt = tracing_subscriber::fmt::layer()
.with_writer(appender_fmt)
.boxed();
let handle = Handle {
_guard_json: Arc::new(handle_json),
_guard_fmt: Arc::new(handle_fmt),
};
// Return the guard so that the caller maintains a handle to it. Otherwise,
// we have to wait for tracing_appender to flush the logs before exiting.
// See https://docs.rs/tracing-appender/latest/tracing_appender/non_blocking/struct.WorkerGuard.html
(vec![layer_json, layer_fmt].boxed(), handle)
}
fn new_appender(directory: PathBuf, file_extension: &'static str) -> (NonBlocking, WorkerGuard) {
let appender = Appender {
directory,
current: None,
file_extension,
};
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
(non_blocking, guard)
}
/// A handle to our file-logger.
///
/// This handle houses the [`WorkerGuard`]s of the underlying non-blocking appenders.
/// Thus, you MUST NOT drop this handle for as long as you want messages to arrive at the log files.
#[must_use]
#[derive(Clone, Debug)]
pub struct Handle {
_guard_json: Arc<WorkerGuard>,
_guard_fmt: Arc<WorkerGuard>,
}
#[derive(Debug)]
struct Appender {
directory: PathBuf,
file_extension: &'static str,
// Leaving this so that I/O errors come up through `write` instead of panicking
// in `layer`
current: Option<(fs::File, String)>,
}
impl Appender {
fn with_current_file<R>(
&mut self,
cb: impl Fn(&mut fs::File) -> io::Result<R>,
) -> io::Result<R> {
match self.current.as_mut() {
None => {
let (mut file, name) = self.create_new_writer()?;
let ret = cb(&mut file);
self.current = Some((file, name));
ret
}
Some((file, _)) => cb(file),
}
}
// Inspired from `tracing-appender/src/rolling.rs`.
fn create_new_writer(&self) -> io::Result<(fs::File, String)> {
let format = time::format_description::parse(TIME_FORMAT)
.expect("static format description should always be parsable");
let date = OffsetDateTime::now_utc()
.format(&format)
.expect("formatting a timestamp should always be possible");
let filename = format!("{LOG_FILE_BASE_NAME}.{date}.{}", self.file_extension);
let path = self.directory.join(&filename);
let mut open_options = fs::OpenOptions::new();
open_options.append(true).create(true);
let new_file = open_options.open(path.as_path());
if new_file.is_err() {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
let file = open_options.open(path)?;
return Ok((file, filename));
}
}
let file = new_file?;
Self::set_permissions(&file)?;
Ok((file, filename))
}
/// Make the logs group-readable so that the GUI, running as a user in the `firezone`
/// group, can zip them up when exporting logs.
#[cfg(target_os = "linux")]
fn set_permissions(f: &fs::File) -> io::Result<()> {
// I would put this at the top of the file, but it only exists on Linux
use std::os::unix::fs::PermissionsExt;
// user read/write, group read-only, others nothing
let perms = fs::Permissions::from_mode(0o640);
f.set_permissions(perms)?;
Ok(())
}
/// Does nothing on non-Linux systems
#[cfg(not(target_os = "linux"))]
#[allow(clippy::unnecessary_wraps)]
fn set_permissions(_f: &fs::File) -> io::Result<()> {
Ok(())
}
}
impl io::Write for Appender {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.with_current_file(|f| f.write(buf))
}
fn flush(&mut self) -> io::Result<()> {
self.with_current_file(|f| f.flush())
}
}