feat: diagnostic log (tracing)
The app now writes a diagnostic log to nexus.log in the data directory (0600, rotated at 5 MiB): connection attempts and results, remote closes, reconnect scheduling, host-key/certificate checks with the policy applied, and credentials that couldn't be stored (kind only). Credentials and terminal data are never logged. RUST_LOG sets the level and also mirrors the log to stderr. The remaining eprintln! diagnostics in storage and URL handling go through it too.
This commit is contained in:
@@ -434,6 +434,7 @@ The directory holds:
|
||||
- `recordings/` — session recordings
|
||||
- `triggers.json` — trigger rules
|
||||
- `sftp-bookmarks.json` — SFTP bookmarks
|
||||
- `nexus.log` — the diagnostic log (owner-only, rotated to `nexus.log.1` at 5 MiB): connections, reconnects, host-key and certificate checks and errors. Credentials and terminal contents are never logged. Set `RUST_LOG` (for example `RUST_LOG=debug`) for more detail; it then also goes to the terminal you started Nexus Terminal from.
|
||||
|
||||
Credentials saved in the OS keychain are stored under the service name `NexusTerminal`.
|
||||
|
||||
|
||||
Generated
+55
@@ -3225,6 +3225,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
@@ -3449,6 +3458,8 @@ dependencies = [
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"winresource",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -3483,6 +3494,7 @@ dependencies = [
|
||||
"sha2 0.11.0",
|
||||
"tiny_http",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"ureq",
|
||||
"uuid",
|
||||
"webpki-roots",
|
||||
@@ -3554,6 +3566,15 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
@@ -5507,6 +5528,15 @@ dependencies = [
|
||||
"sponge-cursor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_library"
|
||||
version = "0.1.9"
|
||||
@@ -5984,6 +6014,15 @@ dependencies = [
|
||||
"syn 3.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.11.3"
|
||||
@@ -6198,6 +6237,22 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "type-map"
|
||||
version = "0.5.1"
|
||||
|
||||
@@ -14,6 +14,8 @@ eframe = { version = "0.36", default-features = false, features = ["default_font
|
||||
egui = "0.36"
|
||||
parking_lot = "0.12"
|
||||
zeroize = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "std", "ansi"] }
|
||||
egui-phosphor = "0.14"
|
||||
rfd = "0.17"
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
|
||||
@@ -114,6 +114,8 @@ fn main() -> eframe::Result {
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
init_logging(&svc.config.data_dir);
|
||||
tracing::info!(version = nexus_core::service::VERSION, "starting");
|
||||
let headless = opts.screenshot.is_some();
|
||||
let native = eframe::NativeOptions {
|
||||
persist_window: !headless,
|
||||
@@ -139,3 +141,23 @@ fn window_icon() -> egui::IconData {
|
||||
let img = image::load_from_memory(chrome::LOGO_PNG).map(|i| i.to_rgba8()).unwrap_or_default();
|
||||
egui::IconData { width: img.width(), height: img.height(), rgba: img.into_raw() }
|
||||
}
|
||||
|
||||
/// Diagnostics go to `nexus.log` in the data directory (rotated at 5 MiB), and to stderr too when
|
||||
/// RUST_LOG is set. RUST_LOG also sets the level (default: info for Nexus, warn for libraries).
|
||||
/// Connection lifecycle, host-key checks and errors are logged; credentials and terminal data never.
|
||||
fn init_logging(data_dir: &std::path::Path) {
|
||||
use tracing_subscriber::prelude::*;
|
||||
let path = data_dir.join("nexus.log");
|
||||
if std::fs::metadata(&path).is_ok_and(|m| m.len() > 5 << 20) {
|
||||
let _ = std::fs::rename(&path, data_dir.join("nexus.log.1"));
|
||||
}
|
||||
let mut opts = std::fs::OpenOptions::new();
|
||||
opts.create(true).append(true);
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::OpenOptionsExt::mode(&mut opts, 0o600);
|
||||
let file = opts.open(&path).ok().map(|f| tracing_subscriber::fmt::layer().with_ansi(false).with_writer(std::sync::Mutex::new(f)));
|
||||
let stderr = std::env::var_os("RUST_LOG").map(|_| tracing_subscriber::fmt::layer().with_writer(std::io::stderr));
|
||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn,nexus_terminal=info,nexus_core=info"));
|
||||
let _ = tracing_subscriber::registry().with(filter).with(file).with(stderr).try_init();
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ pub fn is_web_url(url: &str) -> bool {
|
||||
/// Opens `url` in the browser if it is a web link; returns false (and opens nothing) otherwise.
|
||||
pub fn open_web_url(ctx: &egui::Context, url: &str) -> bool {
|
||||
if !is_web_url(url) {
|
||||
eprintln!("refusing to open non-web URL {url:?}");
|
||||
tracing::warn!(url, "refusing to open a non-web URL");
|
||||
return false;
|
||||
}
|
||||
ctx.open_url(egui::OpenUrl::new_tab(url.trim()));
|
||||
|
||||
@@ -23,6 +23,7 @@ hmac = "0.13.0"
|
||||
sha1 = "0.11.0"
|
||||
zeroize = "1"
|
||||
parking_lot = "0.12"
|
||||
tracing = "0.1"
|
||||
portable-pty = "0.9.0"
|
||||
serialport = { version = "4", default-features = false }
|
||||
des = "0.9"
|
||||
|
||||
@@ -149,6 +149,7 @@ impl Manager {
|
||||
Err(_) => Err("host key prompt failed: host key prompt timed out waiting for user response".into()),
|
||||
}
|
||||
};
|
||||
tracing::info!(host = %k.host, port = k.port, key_type = %k.key_type, fingerprint = %k.fingerprint, ?policy, known = stored.is_some(), "checking host key");
|
||||
match (policy, stored) {
|
||||
(HostKeyPolicy::Insecure, None) | (HostKeyPolicy::AcceptNew, None) => save(),
|
||||
(HostKeyPolicy::Insecure, Some(s)) => touch(s),
|
||||
|
||||
@@ -346,6 +346,7 @@ impl Manager {
|
||||
}
|
||||
(Err(_), None) => {}
|
||||
(Err(e), Some(s)) => {
|
||||
tracing::warn!(session = id, error = %e, "connection failed");
|
||||
s.info.state = SessionState::Error;
|
||||
s.info.error = e.to_string();
|
||||
attempt_failed(&s.session_secrets, &e.to_string());
|
||||
@@ -353,6 +354,7 @@ impl Manager {
|
||||
self.emit_state(id, SessionState::Error, &e.to_string());
|
||||
}
|
||||
(Ok(client), Some(s)) => {
|
||||
tracing::info!(session = id, "connected");
|
||||
let typed = std::mem::take(&mut s.session_secrets.lock().pending);
|
||||
s.client = client.clone();
|
||||
s.info.state = SessionState::Connected;
|
||||
@@ -365,6 +367,7 @@ impl Manager {
|
||||
}
|
||||
for (key, value) in typed {
|
||||
if let Err(e) = self.secrets.set_checked(&key, &value) {
|
||||
tracing::warn!(session = id, credential = key.rsplit(':').next().unwrap_or(""), error = %e, "credential not saved");
|
||||
self.emit(Event::Warning { session: id.into(), message: format!("Credential not saved: {e}") });
|
||||
}
|
||||
}
|
||||
@@ -375,6 +378,7 @@ impl Manager {
|
||||
}
|
||||
|
||||
fn connect_protocol(self: &Arc<Self>, id: &str, epoch: u64, p: &Profile, prompt: Prompt) -> Result<Client> {
|
||||
tracing::info!(session = id, protocol = p.protocol.id(), host = %p.host, port = p.port, "connecting");
|
||||
let _ = self.with(id, |s| {
|
||||
let mut st = s.session_secrets.lock();
|
||||
st.pending.clear();
|
||||
@@ -510,6 +514,7 @@ impl Manager {
|
||||
s.profile.clone()
|
||||
};
|
||||
self.store.add_audit("disconnect", "", id, "Session disconnected");
|
||||
tracing::info!(session = id, "remote closed the session");
|
||||
if let Some(p) = profile.filter(|p| p.auto_reconnect()) {
|
||||
self.schedule_reconnect(id, epoch, p);
|
||||
return;
|
||||
@@ -525,9 +530,11 @@ impl Manager {
|
||||
let attempt = self.with(id, |s| {
|
||||
s.reconnect_count += 1;
|
||||
if s.reconnect_count > max {
|
||||
tracing::warn!(session = id, tries = max, "giving up reconnecting");
|
||||
s.info.state = SessionState::Disconnected;
|
||||
return None;
|
||||
}
|
||||
tracing::info!(session = id, attempt = s.reconnect_count, of = max, "scheduling reconnect");
|
||||
s.info.state = SessionState::Reconnecting;
|
||||
s.info.error.clear();
|
||||
if let Some(c) = s.cancel_reconnect.replace(cancel.clone()) {
|
||||
|
||||
@@ -48,7 +48,7 @@ fn json_col<T: serde::Serialize>(v: &Option<T>) -> Option<String> {
|
||||
|
||||
fn parse_col<T: serde::de::DeserializeOwned>(s: Option<String>) -> Option<T> {
|
||||
let s = s.filter(|s| !s.is_empty() && s != "null")?;
|
||||
serde_json::from_str(&s).inspect_err(|e| eprintln!("storage: ignoring unreadable profile config: {e}")).ok()
|
||||
serde_json::from_str(&s).inspect_err(|e| tracing::warn!(error = %e, "ignoring unreadable profile config")).ok()
|
||||
}
|
||||
|
||||
fn scan_profile(r: &Row) -> rusqlite::Result<Profile> {
|
||||
|
||||
Reference in New Issue
Block a user