use crate::cli::{CatalogAction, Command, SessionAction};
fn wants_cli_diagnostics(command: &Option<Command>) -> bool {
matches!(
command,
None | Some(Command::Doctor { .. })
| Some(Command::Catalog {
action: CatalogAction::Apply { .. }
})
| Some(Command::Sessions {
action: SessionAction::Instructions { .. } | SessionAction::Start { .. }
})
| Some(Command::Session {
action: SessionAction::Instructions { .. } | SessionAction::Start { .. }
})
| Some(Command::Install { .. })
)
}
pub(crate) fn init_cli_diagnostics_if_wanted(command: &Option<Command>) {
if wants_cli_diagnostics(command) {
init_stderr_only("warn");
}
}
pub(crate) fn init_stderr_only(default: &str) {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| default.into()),
)
.with_writer(std::io::stderr)
.with_target(false)
.try_init();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StderrSink {
Register,
Skip,
}
pub(crate) fn stderr_sink_decision(
file_layer_configured: bool,
supervision: &trusty_common::supervision::LaunchdSupervision,
) -> StderrSink {
use trusty_common::supervision::LaunchdSupervision;
match (file_layer_configured, supervision) {
(true, LaunchdSupervision::Supervised(_)) => StderrSink::Skip,
_ => StderrSink::Register,
}
}
#[cfg(feature = "daemon")]
pub(crate) fn init_daemon_tracing() -> anyhow::Result<(
tracing_appender::non_blocking::WorkerGuard,
trusty_common::error_capture::ErrorStore,
)> {
use tracing_subscriber::Layer as _;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
let log_dir = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("cannot resolve home directory"))?
.join(".trusty-mpm")
.join("logs");
std::fs::create_dir_all(&log_dir)?;
let file_appender = tracing_appender::rolling::daily(&log_dir, "trusty-mpm.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let (capture_layer, store) = trusty_common::error_capture::bug_capture_layer(
"trusty-mpm",
trusty_common::error_capture::DEFAULT_CAPTURE_CAPACITY,
env!("CARGO_PKG_VERSION"),
);
let supervision = trusty_common::supervision::launchd_supervision();
let stderr_sink = stderr_sink_decision(true, &supervision);
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false)
.with_filter(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
);
let stderr_layer = (stderr_sink == StderrSink::Register).then(|| {
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
});
tracing_subscriber::registry()
.with(stderr_layer)
.with(file_layer)
.with(capture_layer)
.init();
tracing::info!(
log_dir = %log_dir.display(),
stderr_sink = ?stderr_sink,
supervision = %supervision.describe(),
"tracing initialised for a long-running mode (#6569)"
);
Ok((guard, store))
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use crate::cli::Cli;
fn command_for(argv: &[&str]) -> Option<crate::cli::Command> {
Cli::try_parse_from(argv)
.unwrap_or_else(|e| panic!("argv {argv:?} must parse: {e}"))
.command
}
#[test]
fn wants_cli_diagnostics_covers_every_in_process_deploy_path() {
for argv in [
vec!["tm"],
vec!["tm", "doctor"],
vec!["tm", "doctor", "--fix-skills"],
vec!["tm", "catalog", "apply"],
vec!["tm", "catalog", "apply", "--force", "--prune"],
vec!["tm", "sessions", "instructions"],
vec!["tm", "session", "instructions"],
vec!["tm", "sessions", "start"],
vec!["tm", "sessions", "start", "--dir", "/tmp/project"],
vec!["tm", "session", "start"],
vec!["tm", "install"],
vec!["tm", "install", "--force"],
vec!["tm", "install", "--reconcile-skills"],
] {
assert!(
wants_cli_diagnostics(&command_for(&argv)),
"{argv:?} runs an in-process deploy and must get a subscriber"
);
}
}
#[test]
fn wants_cli_diagnostics_skips_paths_that_own_stdout() {
for argv in [
vec!["tm", "serve", "--stdio"],
vec!["tm", "status"],
vec!["tm", "catalog", "ls"],
] {
assert!(
!wants_cli_diagnostics(&command_for(&argv)),
"{argv:?} must not register the CLI diagnostics subscriber"
);
}
}
#[test]
fn init_stderr_only_is_idempotent() {
init_stderr_only("warn");
init_stderr_only("warn");
}
#[test]
fn stderr_sink_matrix() {
use trusty_common::supervision::LaunchdSupervision;
let supervised = LaunchdSupervision::Supervised("com.trusty.mpm".into());
let not = LaunchdSupervision::NotSupervised;
let unknown = LaunchdSupervision::Unknown("launchctl not on PATH".into());
assert_eq!(
stderr_sink_decision(true, &supervised),
StderrSink::Skip,
"a launchd-supervised process with a file sink must not also write stderr"
);
assert_eq!(stderr_sink_decision(true, ¬), StderrSink::Register);
assert_eq!(
stderr_sink_decision(true, &unknown),
StderrSink::Register,
"an unreachable launchd must never cost the operator their logs"
);
for supervision in [&supervised, ¬, &unknown] {
assert_eq!(
stderr_sink_decision(false, supervision),
StderrSink::Register,
"{supervision:?} must still keep stderr when nothing else is configured"
);
}
}
#[test]
fn stderr_sink_never_skips_without_a_file_layer() {
use trusty_common::supervision::LaunchdSupervision;
for supervision in [
LaunchdSupervision::Supervised("com.trusty.mpm".into()),
LaunchdSupervision::NotSupervised,
LaunchdSupervision::Unknown("timed out".into()),
] {
assert_ne!(stderr_sink_decision(false, &supervision), StderrSink::Skip);
}
}
}