How Target-Based Log Filtering Works in tui-logger: A Complete Configuration Guide

tui-logger uses a three-layered filtering system—capture, recording, and display—that operates independently on the target string supplied to log macros, allowing both programmatic and interactive control over which log levels appear in the buffer and the UI.

The tui-logger crate provides a specialized logging frontend for terminal user interfaces (TUIs) built with Rust. Unlike standard loggers, it implements target-based log filtering across three distinct axes: a hot-path capture filter that decides whether to copy records from the hot buffer, a recording filter that governs persistent storage, and a display filter that controls the UI visibility. This architecture enables sub-millisecond log filtering while still permitting runtime adjustments through the TUI itself.

The Three Axes of Target-Based Log Filtering

tui-logger organizes log filtering into three independent layers keyed by the target string (e.g., "my_crate::module::submodule"):

  • Capture filter – Determines if a record moves from the hot circular buffer into the widget’s main buffer. Stored in HotSelect::hashtable (HashMap<u64, LevelFilter>) in src/logger/inner.rs.
  • Recording filter – Defines the maximum level actually recorded for a target when writing to files or the main buffer. Stored in TuiLoggerInner::targets (LevelConfig) in src/logger/inner.rs.
  • Display filter – Controls which levels appear in the target selector widget. Stored in TuiWidgetInnerState::config in src/widget/inner.rs.

These layers work together to ensure that high-frequency logging does not degrade performance while still allowing granular control over output visibility.

How Capture Filtering Works on the Hot Path

When the log! macro fires, tui_logger::raw_log pushes an ExtLogRecord into the hot buffer (see src/logger/inner.rs lines 94-98). Before storage, the Log::enabled method performs a fast hash lookup:

fn enabled(&self, metadata: &Metadata) -> bool {
    let h = fast_str_hash(metadata.target());
    let hs = self.hot_select.lock();
    if let Some(&levelfilter) = hs.hashtable.get(&h) {
        metadata.level() <= levelfilter
    } else if let Some(envfilter) = hs.filter.as_ref() {
        envfilter.enabled(metadata)
    } else {
        metadata.level() <= hs.default
    }
}

The logic follows a strict precedence:

  1. If the target exists in hashtable, compare the record’s level against the stored LevelFilter.
  2. If absent, consult the optional env-filter (configured via env_filter::Filter).
  3. Fall back to the global default level (hs.default).

The first time a new target passes the env-filter check, the move_events function (lines 55-86 in src/logger/inner.rs) extracts the most permissive valid level and writes it to both hashtable (for fast hot-path checks) and the persistent targets table.

Recording Levels and the Configuration API

To change filtering behavior programmatically, use the public API defined in src/logger/api.rs. The function set_level_for_target synchronizes both the hot-path and persistent storage:

pub fn set_level_for_target(target: &str, levelfilter: LevelFilter) {
    let h = fast_str_hash(target);
    TUI_LOGGER.inner.lock().targets.set(target, levelfilter);
    let mut hs = TUI_LOGGER.hot_select.lock();
    hs.hashtable.insert(h, levelfilter);
}

Key configuration functions include:

  • tui_logger::init_logger(max_level) – Initializes the background thread that moves events from the hot buffer to the main buffer. Call once at startup.
  • tui_logger::set_default_level(LevelFilter) – Sets the global default for unknown targets, stored in HotSelect::default.
  • tui_logger::set_level_for_target(target, level) – Overrides the level for a specific target, updating both inner.targets and hot_select.hashtable.
  • tui_logger::set_env_filter_from_string(filter) – Parses an env-filter string (e.g., "info,my_module=debug") and populates the filter structures.
  • tui_logger::set_env_filter_from_env(Some("MY_LOG")) – Reads an environment variable (defaults to RUST_LOG) to build the env-filter.

Display Filtering in the TUI Widget

The target selector widget (src/widget/target.rs) renders each target with symbols E W I D T (error, warn, info, debug, trace). During rendering (lines 61-84), it compares two values:

  1. hot_level_filter – The recording filter from hot_targets.
  2. level_filter – The display filter from the widget’s private state.config.

A symbol appears in show style only when both filters permit that level; otherwise, it renders in hide style.

Interactive controls function as follows:

  • Left / Right arrow keys – Adjust the display filter via state.config.set (affects only UI visibility).
  • + / - keys – Adjust the recording filter by calling set_level_for_target (affects what gets stored).

The TargetWidget::render method (lines 49-60) merges the widget’s private configuration with the global targets map, ensuring new targets appear in the selector even before they exist in the hot-path hash table.

Configuration Examples

Basic Per-Target Filtering

Initialize the logger and restrict a noisy module while keeping the rest at Info:

use log::{info, error, warn, LevelFilter};
use tui_logger::{self, set_level_for_target, set_default_level};

fn main() -> Result<(), tui_logger::TuiLoggerError> {
    // Initialize with global maximum level
    tui_logger::init_logger(LevelFilter::Trace)?;

    // Default for unknown targets
    set_default_level(LevelFilter::Info);

    // Tighten filter for a specific module
    set_level_for_target("my::noisy_module", LevelFilter::Warn);

    // This log is filtered out (Info < Warn)
    info!(target: "my::noisy_module", "this will be filtered out");

    // This log is captured (Warn >= Warn)
    warn!(target: "my::noisy_module", "visible warning");

    Ok(())
}

Using Environment-Filter Syntax

Apply complex filters without recompiling:

use tui_logger::{init_logger, set_env_filter_from_string};
use log::LevelFilter;

fn main() -> Result<(), tui_logger::TuiLoggerError> {
    init_logger(LevelFilter::Trace)?;

    // Syntax: global level, then target-specific overrides
    set_env_filter_from_string("info,db=warn,net=off");

    // Filtered: level Info is below the db target's Warn threshold
    log::info!(target: "db", "database connected");

    // Shown: Warn meets the threshold
    log::warn!(target: "db", "slow query detected");

    // Filtered: net target is disabled (off)
    log::error!(target: "net", "network unreachable");

    Ok(())
}

Controlling the Widget Display State

Configure the UI to hide debug messages by default while still recording them:

use tui_logger::{TuiLoggerWidget, TuiWidgetState, LevelFilter, TuiWidgetEvent};
use ratatui::widgets::Block;

let state = TuiWidgetState::new()
    .set_default_display_level(LevelFilter::Warn)
    .set_level_for_target("app::renderer", LevelFilter::Debug);

let widget = TuiLoggerWidget::default()
    .block(Block::default().title("Logs"))
    .state(&state);

// In your event loop, handle key inputs:
// state.transition(TuiWidgetEvent::RightKey); // Increases display level for selected target
// state.transition(TuiWidgetEvent::PlusKey);  // Increases recording level (calls set_level_for_target)

Summary

  • tui-logger implements target-based log filtering through three layers: capture (hot-path hash table), recording (persistent LevelConfig), and display (widget state).
  • The Log::enabled implementation in src/logger/inner.rs uses a HashMap<u64, LevelFilter> keyed by fast_str_hash(target) to achieve O(1) lookup during log emission.
  • Use set_level_for_target to programmatically adjust both capture and recording filters simultaneously.
  • Use set_env_filter_from_string or set_env_filter_from_env to configure filters at runtime using standard env-filter syntax.
  • The target selector widget maintains a separate display filter (state.config) that interacts with recording levels in src/widget/target.rs, allowing users to hide levels in the UI without losing data.

Frequently Asked Questions

How do I completely disable logging for a specific target in tui-logger?

Set the target’s level filter to Off. Call tui_logger::set_level_for_target("target_name", log::LevelFilter::Off). This updates the hashtable in HotSelect (defined in src/logger/inner.rs) to reject all records from that target before they reach the main buffer.

Why are my log messages appearing in the buffer but not in the TUI widget?

The widget’s display filter (stored in TuiWidgetInnerState::config in src/widget/inner.rs) likely excludes your message’s level. Use the Left/Right arrow keys in the target selector to adjust the display threshold, or call state.set_level_for_target() on the TuiWidgetState to change the UI visibility without affecting what gets recorded.

What is the difference between set_default_level and set_env_filter_from_string?

set_default_level sets a static global fallback used when no per-target rule exists, stored in HotSelect::default. In contrast, set_env_filter_from_string parses a complex filter string (e.g., "crate=debug,other=off") and installs an env_filter::Filter that dynamically evaluates targets; the first time a target passes this filter, its level is cached in the hot-path hash table for performance.

How does tui-logger maintain performance with thousands of log events per second?

The crate separates concerns into a hot buffer and a main buffer. The hot-path check in Log::enabled (lines 71-80 of src/logger/inner.rs) uses a lock-protected hash map (HotSelect::hashtable) to avoid string comparisons during logging, achieving O(1) filtering via 64-bit hash keys generated by fast_str_hash.

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 →