How Clash Nyanpasu Structures Its Logging System, Error Handling, and Panic Hooks

Clash Nyanpasu uses the tracing ecosystem for structured logging with rotating file appenders, implements dynamic log level reloading via channels, and registers a global panic hook that captures backtraces, logs fatal errors to disk, and displays native system dialogs to users.

The logging system and error handling architecture in Clash Nyanpasu (the Tauri-based desktop proxy client) is designed for production reliability and developer observability. According to the libnyanpasu/clash-nyanpasu source code, the backend combines Rust's tracing library with custom file rotation logic, while the frontend uses TypeScript utilities for development-time console output.

Logging Architecture Overview

The Tracing Ecosystem Setup

The core initialization happens in backend/tauri/src/utils/init/logging.rs. The init() function establishes a global subscriber that combines multiple layers:

  • File Layer: A JSON-formatted appender that writes to daily-rotating log files (clash-nyanpasu-*.log) with configurable retention
  • Console Layer (debug builds only): Pretty, ANSI-colored output to stdout/stderr using tracing_subscriber::fmt::layer()
  • EnvFilter: Parses RUST_LOG or LOG_LEVEL environment variables to control verbosity per module

The function first ensures the logs directory exists using dirs::app_logs_dir(), then constructs the subscriber with tracing::subscriber::set_global_default().

File Rotation and Appender Configuration

Log rotation is handled by get_file_appender(max_files: usize) in the same module. This creates a non-blocking writer that:

  1. Generates filenames with daily timestamps (clash-nyanpasu-YYYY-MM-DD.log)
  2. Maintains a maximum of max_files historical log files, deleting older entries automatically
  3. Uses tracing_appender::rolling::Rotation::DAILY for the underlying rotation logic

The appender is wrapped with tracing_subscriber::fmt::layer().json() to ensure structured logging output suitable for debugging production issues.

Dynamic Log Level Reloading

Clash Nyanpasu supports changing log levels at runtime without restarting the application. This is implemented via a global mpsc::channel<ReloadSignal>:

pub struct ReloadSignal {
    pub level: Option<LoggingLevel>,
    pub max_files: Option<usize>,
}

When refresh_logger((Some(new_level), Some(new_max_files))) is called—typically after the user changes settings in the UI—the function sends a signal through the channel. A background thread spawned during initialization receives this signal and updates the EnvFilter layer and file appender accordingly.

Error Handling and Panic Hooks

Global Panic Hook Registration

In backend/tauri/src/lib.rs, the application installs a custom panic hook immediately upon startup using std::panic::set_hook():

std::panic::set_hook(Box::new(move |panic_info| {
    // Capture payload and location
    let payload = panic_info.payload();
    let location = panic_info.location().map(|l| l.to_string());
    
    // Log the panic via tracing
    tracing::event!(
        tracing::Level::ERROR,
        panic.payload = ?payload,
        panic.location = ?location,
        panic.backtrace = backtrace.as_ref().map(tracing::field::display),
        "A panic occurred"
    );
    
    // Show user-facing dialog
    utils::dialog::panic_dialog(&msg);
}));

This hook intercepts all unrecoverable errors before the application terminates.

Structured Panic Logging

When a panic occurs, the hook captures:

  • Payload: The panic message (often an anyhow::Error or string slice)
  • Location: File path and line number where the panic originated
  • Backtrace: Full stack trace when RUST_BACKTRACE=1 is set in the environment

These fields are emitted as a structured tracing::event! at the ERROR level, ensuring they are written to the rotating log file with JSON formatting. This allows developers to correlate user-reported crashes with specific code locations and stack traces.

User Notification via Native Dialogs

After logging the panic, the hook calls utils::dialog::panic_dialog(msg) defined in backend/tauri/src/utils/dialog.rs. This function uses Tauri's MessageDialog API to display a platform-native error dialog containing:

  • The panic message
  • A localized title fetched via t!("dialog.panic") (i18n support)
  • Standard system dialog buttons (typically "OK")

This ensures users are immediately informed of fatal errors rather than experiencing a silent crash, while the detailed technical information is preserved in the log files for debugging.

Integration with Configuration System

The logging system integrates tightly with Clash Nyanpasu's configuration management. During the initialization sequence in logging::init(), a dedicated thread spawns to synchronize logger settings with the user's configuration:

std::thread::spawn(move || {
    let cfg = Config::verge();
    let level = cfg.latest().get_log_level();
    let max_files = cfg.latest().max_log_files;
    let _ = refresh_logger((Some(level), max_files));
});

This ensures that:

  • The log level respects the user's preference from the previous session immediately upon startup
  • The number of retained log files matches the configured rotation policy
  • Changes made in the settings UI trigger refresh_logger() to update the active logger without requiring an application restart

Code Examples

Initializing the Logger

To start the logging system in a Tauri application context:

use backend::tauri::utils::init::logging;

fn main() -> anyhow::Result<()> {
    // Initialize global tracing subscriber with file rotation
    logging::init()?;
    
    // Now tracing macros work throughout the application
    tracing::info!("Application started successfully");
    
    Ok(())
}

Refreshing Log Configuration

When implementing a settings panel that allows users to change log verbosity:

use backend::tauri::utils::init::logging::{refresh_logger, LoggingLevel};

fn on_settings_changed(new_level: LoggingLevel, max_retention: usize) {
    // Send reload signal through the global channel
    let result = refresh_logger((Some(new_level), Some(max_retention)));
    
    match result {
        Ok(_) => tracing::info!("Log configuration updated successfully"),
        Err(e) => tracing::error!("Failed to refresh logger: {}", e),
    }
}

Testing Panic Handling

To verify the panic hook captures and logs correctly during development:

fn test_panic_recovery() {
    // This will trigger the global hook
    std::panic::set_hook(Box::new(|info| {
        tracing::error!("Test panic captured: {:?}", info);
    }));
    
    // Intentional panic to test the hook
    panic!("simulated fatal error");
}

When executed with RUST_BACKTRACE=1, this produces a structured log entry containing the backtrace and displays the native error dialog.

Summary

  • Structured Logging: Clash Nyanpasu uses tracing with JSON formatting and daily file rotation, configured in backend/tauri/src/utils/init/logging.rs.
  • Dynamic Reloading: A global mpsc channel enables runtime updates to log levels and retention policies without restarting the application.
  • Panic Hooks: The global panic hook in backend/tauri/src/lib.rs captures fatal errors, logs structured panic information including backtraces, and invokes utils::dialog::panic_dialog() to notify users via native system dialogs.
  • Configuration Integration: Logger settings synchronize automatically with the user's configuration from Config::verge(), ensuring preferences persist across sessions.

Frequently Asked Questions

How does Clash Nyanpasu handle log file rotation?

The application creates a daily-rotating file appender via get_file_appender(max_files) in backend/tauri/src/utils/init/logging.rs. This generates files named clash-nyanpasu-YYYY-MM-DD.log and automatically deletes older files when the count exceeds the configured max_files limit. The rotation uses tracing_appender::rolling::Rotation::DAILY to ensure non-blocking I/O operations.

Can I change the log level without restarting Clash Nyanpasu?

Yes. The logging system supports dynamic reloading through the refresh_logger() function. When you modify settings in the UI, the application sends a ReloadSignal through a global mpsc channel to a background thread. This thread updates the EnvFilter layer with the new log level and adjusts the file appender's retention settings immediately, without requiring an application restart.

What happens when Clash Nyanpasu encounters a fatal panic?

When a panic occurs, the custom hook installed in backend/tauri/src/lib.rs intercepts the error before termination. It captures the panic payload, source location, and optional backtrace (when RUST_BACKTRACE=1 is set), then emits a structured tracing::event! at the ERROR level to ensure the crash is recorded in the rotating log file. Finally, it calls utils::dialog::panic_dialog() to display a native system dialog informing the user of the unexpected error, preventing silent crashes while preserving diagnostic data for developers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →