Logger Setup

Agent Air uses the tracing ecosystem for structured logging. This page documents how the logger is initialized, where logs are stored, and how to customize logging behavior.

Overview

The logger is initialized during AgentAir::new() and writes structured logs to daily log files:

let logger = Logger::new(config.log_prefix())?;
// Creates: logs/myagent-2025-01-26.log

Logger Struct

The Logger struct holds the tracing guard that ensures logs are flushed:

pub struct Logger {
    _guard: WorkerGuard,
}

The WorkerGuard keeps the non-blocking writer thread alive. When the guard is dropped, remaining logs are flushed to disk.

Initialization

impl Logger {
    pub fn new(prefix: &str) -> io::Result<Self> {
        // Step 1: Create logs directory
        let log_dir = Path::new("logs");
        if !log_dir.exists() {
            fs::create_dir_all(log_dir)?;
        }

        // Step 2: Generate log filename with date
        let date = Local::now().format("%Y-%m-%d");
        let log_file_name = format!("{}/{}-{}.log", "logs", prefix, date);

        // Step 3: Create log file
        let file = File::create(&log_file_name)?;

        // Step 4: Setup non-blocking writer
        let (non_blocking, guard) = tracing_appender::non_blocking(file);

        // Step 5: Configure environment filter
        let env_filter = EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| EnvFilter::new("debug"));

        // Step 6: Build and install subscriber
        tracing_subscriber::registry()
            .with(env_filter)
            .with(
                fmt::layer()
                    .with_writer(non_blocking)
                    .with_ansi(false)
                    .with_target(true)
                    .with_thread_ids(false)
                    .with_file(true)
                    .with_line_number(true)
            )
            .init();

        Ok(Self { _guard: guard })
    }
}

Log File Location

Logs are written to daily files in the logs/ directory:

logs/
├── myagent-2025-01-24.log
├── myagent-2025-01-25.log
└── myagent-2025-01-26.log

The filename pattern is: {prefix}-{YYYY-MM-DD}.log

  • prefix: From AgentConfig::log_prefix()
  • Date: Local system date when the agent starts

Log Format

Log entries include contextual information:

2025-01-26T14:30:45.123456Z DEBUG agent_air::controller::llm_controller src/controller/llm_controller.rs:245 Received user input for session 1
2025-01-26T14:30:45.234567Z INFO  agent_air::controller::session::session src/controller/session/session.rs:312 Sending message to Claude API
2025-01-26T14:30:46.345678Z DEBUG agent_air::controller::tools::executor src/controller/tools/executor.rs:89 Executing tool: web_search

Format components:

  • Timestamp: ISO 8601 with microseconds
  • Level: TRACE, DEBUG, INFO, WARN, ERROR
  • Target: Module path (e.g., agent_air::controller)
  • File: Source file path
  • Line: Line number in source
  • Message: Log message

Log Levels

The default log level is debug. This can be overridden via the RUST_LOG environment variable:

# Show only warnings and errors
RUST_LOG=warn ./myagent

# Show info and above
RUST_LOG=info ./myagent

# Enable trace for specific module
RUST_LOG=agent_air::controller=trace ./myagent

# Multiple filters
RUST_LOG=warn,agent_air::tui=debug ./myagent

Level Guidelines

LevelUse For
errorUnrecoverable errors, failures
warnRecoverable issues, deprecation
infoSignificant events (session created, tool executed)
debugDetailed flow information
traceVery verbose, per-message details

Non-Blocking Writes

The logger uses non-blocking writes to avoid impacting performance:

let (non_blocking, guard) = tracing_appender::non_blocking(file);

This creates a background thread that handles actual file writes. Log calls return immediately, with messages queued for the writer thread.

Benefits:

  • No blocking on disk I/O
  • Consistent performance regardless of disk speed
  • Automatic batching of writes

ANSI Colors

ANSI color codes are disabled in log files:

.with_ansi(false)

This ensures logs are readable in text editors and log aggregation tools.

Thread IDs

Thread IDs are suppressed to reduce noise:

.with_thread_ids(false)

Most operations occur on Tokio worker threads, making thread IDs less useful.

Using the Logger

Once initialized, use standard tracing macros:

use tracing::{debug, info, warn, error, trace};

// Simple messages
info!("Starting session");
debug!("Processing input");
warn!("Rate limit approaching");
error!("API call failed");

// With fields
info!(session_id = 1, model = "claude-3", "Session created");
debug!(tool = "web_search", input = ?params, "Executing tool");

// With spans
let span = tracing::info_span!("handle_request", session_id = 1);
let _guard = span.enter();
// All logs within this scope include session_id

Structured Fields

Use structured fields for machine-parseable logs:

// Recommended: structured fields
tracing::info!(
    session_id = session.id(),
    model = session.model(),
    tokens = usage.total(),
    "Request completed"
);

// Avoid: string interpolation
tracing::info!("Request completed for session {} using {}", id, model);

Log File Retention

Log files are not automatically rotated or deleted. Implement your own retention policy:

fn cleanup_old_logs(keep_days: u64) -> io::Result<()> {
    let cutoff = SystemTime::now() - Duration::from_secs(keep_days * 24 * 60 * 60);

    for entry in fs::read_dir("logs")? {
        let entry = entry?;
        let metadata = entry.metadata()?;

        if metadata.modified()? < cutoff {
            fs::remove_file(entry.path())?;
        }
    }
    Ok(())
}

Flush on Shutdown

The WorkerGuard ensures logs are flushed when the Logger is dropped:

// In AgentAir
pub struct AgentAir {
    logger: Logger,  // Dropped last, after other fields
    // ...
}

pub fn shutdown(&self) {
    tracing::info!("Shutting down");
    // ... cleanup
}
// Logger dropped here, flushing remaining logs

Custom Log Destinations

For custom log destinations (e.g., remote logging), create the logger manually:

use tracing_subscriber::prelude::*;

// Custom layer for remote logging
let remote_layer = RemoteLoggingLayer::new(endpoint);

tracing_subscriber::registry()
    .with(env_filter)
    .with(file_layer)
    .with(remote_layer)
    .init();

Performance Considerations

Avoid Expensive Operations

// Bad: expensive operation always evaluated
debug!("User data: {:?}", fetch_all_users().await);

// Good: check level first
if tracing::enabled!(tracing::Level::DEBUG) {
    debug!("User data: {:?}", fetch_all_users().await);
}

Use Static Strings

// Good: static string
info!("Processing request");

// Avoid: format allocation on every call
info!("{}", format!("Processing request"));

Troubleshooting

Logs Not Appearing

  1. Check RUST_LOG environment variable
  2. Verify logs/ directory exists and is writable
  3. Ensure Logger is not dropped prematurely

Missing Final Logs

The WorkerGuard must remain alive until shutdown:

// Bad: guard dropped immediately
let _ = Logger::new("myagent")?;

// Good: guard stored in struct
self.logger = Logger::new("myagent")?;

Large Log Files

Consider:

  • Increasing log level in production
  • Implementing log rotation
  • Using log aggregation service

Next Steps