Files
firezone/rust/logging/src/lib.rs
Thomas Eizinger 490e9c1dde refactor(gui-client): tidy up app startup (#9468)
As part of investigating #9400, I refactored the startup code of the GUI
client to play around with the initialization order of the runtime and
other parts. After finding the root cause (fixed in #9469), I figured it
would still be nice to land these improvements.

This refactors the app startup:

- Only initialize what is absolutely necessary outside of `try_main`:
The runtime and the telemetry instance.
- Settings are loaded earlier
- Telemetry is initializer earlier
- Add a bootstrap logger that logs to stdout whilst we are booting
- Re-order some code inside `gui::run` to bundle the initialization of
Tauri into a single command chain

---------

Signed-off-by: Thomas Eizinger <thomas@eizinger.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-08 08:04:15 +00:00

308 lines
9.7 KiB
Rust

#![cfg_attr(test, allow(clippy::unwrap_used))]
pub mod file;
mod format;
#[macro_use]
mod unwrap_or;
mod ansi;
mod capturing_writer;
mod err_with_sources;
mod event_message_contains_filter;
use std::sync::Arc;
use anyhow::{Context, Result};
use event_message_contains_filter::EventMessageContains;
use sentry_tracing::EventFilter;
use tracing::{Subscriber, subscriber::DefaultGuard};
use tracing_log::LogTracer;
use tracing_subscriber::{
EnvFilter, Layer, Registry,
filter::{FilterExt, ParseError},
fmt,
layer::SubscriberExt as _,
registry::LookupSpan,
reload,
util::SubscriberInitExt,
};
pub use ansi::stdout_supports_ansi;
pub use capturing_writer::CapturingWriter;
pub use err_with_sources::{ErrorWithSources, err_with_src};
pub use format::Format;
/// Registers a global subscriber with stdout logging and `additional_layer`
pub fn setup_global_subscriber<L>(additional_layer: L) -> Result<FilterReloadHandle>
where
L: Layer<Registry> + Send + Sync,
{
if let Err(error) = output_vt100::try_init() {
tracing::debug!("Failed to init terminal colors: {error}");
}
let directives = std::env::var("RUST_LOG").unwrap_or_default();
let (filter1, reload_handle1) =
try_filter(&directives).context("Failed to parse directives")?;
let (filter2, reload_handle2) =
try_filter(&directives).context("Failed to parse directives")?;
let subscriber = Registry::default()
.with(additional_layer.with_filter(filter1))
.with(sentry_layer())
.with(
fmt::layer()
.with_ansi(stdout_supports_ansi())
.event_format(Format::new())
.with_filter(filter2),
);
init(subscriber)?;
Ok(reload_handle1.merge(reload_handle2))
}
/// Sets up a bootstrap logger.
pub fn setup_bootstrap() -> Result<DefaultGuard> {
let directives = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
let (filter, _) = try_filter(&directives).context("failed to parse directives")?;
let layer = tracing_subscriber::fmt::layer()
.event_format(Format::new())
.with_filter(filter);
let subscriber = Registry::default().with(layer);
Ok(tracing::dispatcher::set_default(&subscriber.into()))
}
#[expect(
clippy::disallowed_methods,
reason = "This is the alternative function."
)]
pub fn init(subscriber: impl Subscriber + Send + Sync + 'static) -> Result<()> {
tracing::subscriber::set_global_default(subscriber).context("Could not set global default")?;
LogTracer::init().context("Failed to init LogTracer")?;
Ok(())
}
/// Constructs an opinionated [`EnvFilter`] with some crates already silenced.
pub fn try_filter<S>(
directives: &str,
) -> Result<(reload::Layer<EnvFilter, S>, FilterReloadHandle), ParseError>
where
S: 'static,
{
let env_filter = parse_filter(directives)?;
let (layer, reload_handle) = reload::Layer::new(env_filter);
let handle = FilterReloadHandle {
inner: Arc::new(reload_handle),
};
Ok((layer, handle))
}
fn parse_filter(directives: &str) -> Result<EnvFilter, ParseError> {
/// A filter directive that silences noisy crates.
///
/// For debugging, it is useful to set a catch-all log like `debug`.
/// This obviously creates a lot of logs from all kinds of crates.
/// For our usecase, logs from `netlink_proto` and other crates are very likely not what you want to see.
///
/// By prepending this directive to the active log filter, a simple directive like `debug` actually produces useful logs.
/// If necessary, you can still activate logs from these crates by restating them in your directive with a lower filter, i.e. `netlink_proto=debug`.
const IRRELEVANT_CRATES: &str =
"netlink_proto=warn,os_info=warn,rustls=warn,opentelemetry_sdk=info,opentelemetry=info";
let env_filter = if directives.is_empty() {
EnvFilter::try_new(IRRELEVANT_CRATES)?
} else {
EnvFilter::try_new(format!("{IRRELEVANT_CRATES},{directives}"))?
};
Ok(env_filter)
}
pub struct FilterReloadHandle {
inner: Arc<dyn Reload + Send + Sync>,
}
impl std::fmt::Debug for FilterReloadHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("FilterReloadHandle").finish_non_exhaustive()
}
}
impl FilterReloadHandle {
pub fn reload(&self, new_filter: &str) -> Result<()> {
self.inner.reload(new_filter)?;
Ok(())
}
pub fn merge(self, other: FilterReloadHandle) -> Self {
Self {
inner: Arc::new((self, other)),
}
}
}
trait Reload {
fn reload(&self, new_filter: &str) -> Result<()>;
}
impl<S> Reload for tracing_subscriber::reload::Handle<EnvFilter, S>
where
S: 'static,
{
fn reload(&self, new_filter: &str) -> Result<()> {
let filter = parse_filter(new_filter).context("Failed to parse new filter")?;
self.reload(filter).context("Failed to reload filter")?;
Ok(())
}
}
impl Reload for (FilterReloadHandle, FilterReloadHandle) {
fn reload(&self, new_filter: &str) -> Result<()> {
let (a, b) = self;
a.reload(new_filter)?;
b.reload(new_filter)?;
Ok(())
}
}
/// Initialises a logger to be used in tests.
pub fn test(directives: &str) -> DefaultGuard {
tracing_subscriber::fmt()
.with_test_writer()
.with_env_filter(directives)
.set_default()
}
pub fn test_global(directives: &str) {
init(
tracing_subscriber::fmt()
.with_test_writer()
.with_env_filter(directives)
.finish(),
)
.ok();
}
/// Constructs a [`tracing::Layer`](Layer) that captures events and spans and reports them to Sentry.
///
/// ## Events
///
/// - error and warn events are reported as sentry exceptions
/// - info and debug events are captured as breadcrumbs (and submitted together with warns & errors)
///
/// ## Telemetry events
///
/// This layer configuration supports a special `telemetry` event.
/// Telemetry events are events logged on the `TRACE` level for the `telemetry` target.
/// These events SHOULD be created using [`telemetry_event`] to ensure that they are sampled correctly.
/// The idea here is that some events logged via `tracing` should not necessarily end up in the users log file.
/// Yet, if they happen a lot, we still want to know about them.
/// Coupling the `telemetry` target to the `TRACE` level pretty much prevents these events from ever showing up in log files.
/// By sampling them, we prevent flooding Sentry with lots of these logs.
///
/// ## Telemetry spans
///
/// Only spans with the `telemetry` target on level `TRACE` will be submitted to Sentry.
/// Similar to telemetry events, these should be created with [`telemetry_span`] to ensure they are sampled correctly.
pub fn sentry_layer<S>() -> impl Layer<S> + Send + Sync
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
use tracing::Level;
sentry_tracing::layer()
.event_filter(move |md| match *md.level() {
Level::ERROR | Level::WARN => EventFilter::Event,
Level::INFO | Level::DEBUG => EventFilter::Breadcrumb,
Level::TRACE if md.target() == TELEMETRY_TARGET => EventFilter::Event,
_ => EventFilter::Ignore,
})
.span_filter(|md| {
matches!(
*md.level(),
Level::ERROR | Level::WARN | Level::INFO | Level::DEBUG
)
})
.enable_span_attributes()
.with_filter(parse_filter("trace").expect("static filter always parses"))
.with_filter(EventMessageContains::all(
Level::ERROR,
&[
"WinTun: Failed to create process: rundll32",
r#"RemoveInstance "SWD\WINTUN\{E9245BC1-B8C1-44CA-AB1D-C6AAD4F13B9C}""#,
"(Code 0x00000003)",
],
).not())
.with_filter(EventMessageContains::all(
Level::ERROR,
&[
r#"WinTun: Error executing worker process: "SWD\WINTUN\{E9245BC1-B8C1-44CA-AB1D-C6AAD4F13B9C}""#,
"(Code 0x00000003)",
],
).not())
.with_filter(EventMessageContains::all(
Level::ERROR,
&[
"WinTun: Failed to remove adapter when closing",
"(Code 0x00000003)",
],
).not())
.with_filter(EventMessageContains::all(
Level::ERROR,
&[
r#"WinTun: Failed to remove orphaned adapter "Firezone""#,
"(Code 0x00000003)",
],
).not())
}
#[doc(hidden)]
pub const TELEMETRY_TARGET: &str = "telemetry";
#[doc(hidden)]
pub const TELEMETRY_SAMPLE_RATE: f32 = 0.01;
/// Creates a `telemetry` span that will be active until dropped.
///
/// In order to save CPU power, `telemetry` spans are sampled at a rate of 1% at creation time.
#[macro_export]
macro_rules! telemetry_span {
($($arg:tt)*) => {
if $crate::__export::rand::random::<f32>() < $crate::TELEMETRY_SAMPLE_RATE {
$crate::__export::tracing::trace_span!(target: $crate::TELEMETRY_TARGET, $($arg)*)
} else {
$crate::__export::tracing::Span::none()
}
};
}
/// Creates a `telemetry` event.
///
/// In order to save CPU power, `telemetry` events are sampled at a rate of 1% at creation time.
/// In addition, all telemetry events are logged at the `DEBUG` level.
#[macro_export]
macro_rules! telemetry_event {
($($arg:tt)*) => {
if $crate::__export::rand::random::<f32>() < $crate::TELEMETRY_SAMPLE_RATE {
$crate::__export::tracing::trace!(target: $crate::TELEMETRY_TARGET, $($arg)*);
}
$crate::__export::tracing::debug!($($arg)*);
};
}
#[doc(hidden)]
pub mod __export {
pub use rand;
pub use tracing;
}