feat(logging): integrate tauri-plugin-log with rotating logs and settings UI (resolves #139) #142

Merged
gronod merged 1 commits from feat/logging-139 into development 2026-08-29 17:48:23 +01:00
7 changed files with 146 additions and 4 deletions
+2
View File
@@ -23,6 +23,8 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["process", "io-util", "sync", "time", "rt-multi-thread", "macros"] }
tauri-plugin-dialog = "2.7.2"
tauri-plugin-log = "2"
log = "0.4"
base64 = "0.22"
image = { version = "0.25", default-features = false, features = ["png", "tiff"] }
+45
View File
@@ -121,6 +121,51 @@ pub fn get_default_working_dir(app: AppHandle) -> Result<String, String> {
resolve_safe_cwd(&app, "")
}
#[tauri::command]
pub fn get_log_path(app: AppHandle) -> Result<String, String> {
let log_dir = app
.path()
.app_log_dir()
.map_err(|e| format!("Failed to resolve log dir: {}", e))?;
Ok(log_dir.join("iccery.log").to_string_lossy().to_string())
}
#[tauri::command]
pub fn open_log_dir(app: AppHandle) -> Result<(), String> {
let log_dir = app
.path()
.app_log_dir()
.map_err(|e| format!("Failed to resolve log dir: {}", e))?;
std::fs::create_dir_all(&log_dir).map_err(|e| format!("Failed to create log dir: {}", e))?;
#[cfg(target_os = "windows")]
{
std::process::Command::new("explorer")
.arg(&log_dir)
.spawn()
.map_err(|e| format!("Failed to open log folder: {}", e))?;
}
#[cfg(target_os = "macos")]
{
std::process::Command::new("open")
.arg(&log_dir)
.spawn()
.map_err(|e| format!("Failed to open log folder: {}", e))?;
}
#[cfg(target_os = "linux")]
{
std::process::Command::new("xdg-open")
.arg(&log_dir)
.spawn()
.map_err(|e| format!("Failed to open log folder: {}", e))?;
}
Ok(())
}
#[tauri::command]
pub async fn select_target_file(
app: AppHandle,
+19
View File
@@ -8,12 +8,31 @@ mod settings;
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(
tauri_plugin_log::Builder::default()
.targets([
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir {
file_name: Some("iccery".into()),
}),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview),
])
.level(if cfg!(debug_assertions) {
log::LevelFilter::Debug
} else {
log::LevelFilter::Info
})
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
.build(),
)
.manage(process_manager::ProcessManager::new())
.manage(print::PrinterDevModeStore::new())
.invoke_handler(tauri::generate_handler![
commands::spawn_process,
commands::get_app_info,
commands::get_default_working_dir,
commands::get_log_path,
commands::open_log_dir,
commands::select_target_file,
commands::select_directory,
commands::send_stdin,
+6
View File
@@ -51,6 +51,7 @@ impl ProcessManager {
command.creation_flags(CREATE_NO_WINDOW);
}
log::info!(target: "subprocess", "Spawning process '{id}': {binary} {:?}", args);
match command.spawn() {
Ok(mut child) => {
let stdout = child.stdout.take().expect("Failed to open stdout");
@@ -67,6 +68,7 @@ impl ProcessManager {
let json_str = line[JSON_ROW_PREFIX.len()..].to_string();
crate::events::emit_json_row(&app_clone, &id_clone, json_str);
} else {
log::info!(target: "subprocess", "[{id_clone}] {line}");
emit_stdout(&app_clone, &id_clone, line);
}
}
@@ -77,6 +79,7 @@ impl ProcessManager {
tokio::spawn(async move {
let mut reader = BufReader::new(stderr).lines();
while let Ok(Some(line)) = reader.next_line().await {
log::warn!(target: "subprocess", "[{id_clone2}] [stderr] {line}");
emit_stderr(&app_clone2, &id_clone2, line);
}
});
@@ -112,6 +115,8 @@ impl ProcessManager {
}
};
log::info!(target: "subprocess", "Process '{id_clone_exit}' exited with code {exit_code}");
// Reap child from process manager maps upon exit
{
let mut stdins = stdins_clone.lock().await;
@@ -126,6 +131,7 @@ impl ProcessManager {
Ok(())
}
Err(e) => {
log::error!(target: "subprocess", "Failed to spawn '{id}': {e}");
emit_error(&app, &id, e.to_string());
Err(e.to_string())
}
+1
View File
@@ -24,6 +24,7 @@ pub struct ProfilingPreset {
pub struct AppSettings {
pub argyll_binary_dir: Option<String>,
pub default_instrument: Option<String>,
pub log_level: Option<String>,
#[serde(default)]
pub custom_presets: Vec<ProfilingPreset>,
}
+20
View File
@@ -489,6 +489,26 @@
<label for="default_instrument">Default Instrument Override</label>
<input type="text" id="default_instrument" placeholder="e.g. i1">
</div>
<div class="form-group" style="margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border-color, #333);">
<label style="font-weight: 600;">Diagnostics &amp; Logging</label>
<div class="input-row" style="margin-top: 8px;">
<div>
<label for="logLevelSelect" class="sub-label">Log Level</label>
<select id="logLevelSelect">
<option value="error">Error (Minimal)</option>
<option value="warn">Warn</option>
<option value="info" selected>Info (Standard)</option>
<option value="debug">Debug (Detailed)</option>
<option value="trace">Trace (Verbose)</option>
</select>
</div>
<div style="display: flex; align-items: flex-end; gap: 8px;">
<button type="button" id="btnOpenLogFolder" class="secondary" title="Open log folder in file explorer">📂 Open Log Folder</button>
<button type="button" id="btnCopyLogPath" class="secondary" title="Copy log path to clipboard">📋 Copy Path</button>
</div>
</div>
<small id="logPathDisplay" class="path-display" style="margin-top: 6px; display: block;"></small>
</div>
<div class="modal-actions">
<button id="saveSettingsBtn" class="primary">Save Settings</button>
<button id="closeSettingsBtn" class="secondary">Cancel</button>
+53 -4
View File
@@ -5,31 +5,80 @@ export async function initSettings() {
const openBtn = document.getElementById('openSettingsBtn');
const saveBtn = document.getElementById('saveSettingsBtn');
const closeBtn = document.getElementById('closeSettingsBtn');
const logLevelSelect = document.getElementById('logLevelSelect');
const btnOpenLogFolder = document.getElementById('btnOpenLogFolder');
const btnCopyLogPath = document.getElementById('btnCopyLogPath');
const logPathDisplay = document.getElementById('logPathDisplay');
if (!dialog || !openBtn) return;
async function refreshLogPath() {
if (!logPathDisplay) return;
try {
const path = await invoke('get_log_path');
logPathDisplay.textContent = path ? `Log file: ${path}` : '';
} catch (err) {
console.warn("Could not retrieve log path:", err);
}
}
openBtn.addEventListener('click', async () => {
try {
const settings = await invoke('load_settings');
document.getElementById('argyll_binary_dir').value = settings.argyll_binary_dir || '';
document.getElementById('default_instrument').value = settings.default_instrument || '';
if (logLevelSelect && settings.log_level) {
logLevelSelect.value = settings.log_level;
}
await refreshLogPath();
dialog.showModal();
} catch (e) {
console.error("Failed to load settings:", e);
}
});
if (btnOpenLogFolder) {
btnOpenLogFolder.addEventListener('click', async () => {
try {
await invoke('open_log_dir');
} catch (err) {
alert(`Failed to open log folder: ${err}`);
}
});
}
if (btnCopyLogPath) {
btnCopyLogPath.addEventListener('click', async () => {
try {
const path = await invoke('get_log_path');
if (path) {
await navigator.clipboard.writeText(path);
const originalText = btnCopyLogPath.textContent;
btnCopyLogPath.textContent = '✓ Copied!';
setTimeout(() => {
btnCopyLogPath.textContent = originalText;
}, 2000);
}
} catch (err) {
alert(`Failed to copy log path: ${err}`);
}
});
}
if (closeBtn) {
closeBtn.addEventListener('click', () => dialog.close());
}
if (saveBtn) {
saveBtn.addEventListener('click', async () => {
const settings = {
argyll_binary_dir: document.getElementById('argyll_binary_dir').value.trim() || null,
default_instrument: document.getElementById('default_instrument').value.trim() || null
};
try {
const currentSettings = await invoke('load_settings').catch(() => ({}));
const settings = {
...currentSettings,
argyll_binary_dir: document.getElementById('argyll_binary_dir').value.trim() || null,
default_instrument: document.getElementById('default_instrument').value.trim() || null,
log_level: logLevelSelect ? logLevelSelect.value : (currentSettings.log_level || 'info'),
};
await invoke('save_settings', { settings });
dialog.close();
} catch (e) {