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: FromAgentConfig::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
| Level | Use For |
|---|---|
error | Unrecoverable errors, failures |
warn | Recoverable issues, deprecation |
info | Significant events (session created, tool executed) |
debug | Detailed flow information |
trace | Very 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
- Check
RUST_LOGenvironment variable - Verify
logs/directory exists and is writable - 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
- Agent Lifecycle - When logging is initialized
- Controller Errors - Error logging patterns
- AgentConfig Internals - Log prefix configuration
