Purpose of This Chapter
For development, debugging, and understanding how our application behaves in different scenarios, logging is invaluable. This chapter will introduce basic logging capabilities to our password generator using the env_logger crate, allowing us to output debug information that can be toggled via environment variables without cluttering normal user output.
Concepts Explained
Logging Frameworks: Libraries like log provide a common interface for logging (e.g., info!, debug!, error!). These are typically paired with a “logger backend” (like env_logger) that actually handles how and where those log messages are displayed.
Log Levels: Logging allows categorizing messages by severity (e.g., ERROR, WARN, INFO, DEBUG, TRACE). This enables filtering, so you only see the level of detail you need.
Environment Variable Configuration: env_logger is particularly useful for CLI tools because it allows users to control the verbosity of logs through a RUST_LOG environment variable, making debugging easy without recompilation.
Standard Error for Logs: Log output, especially debug information, usually goes to stderr, similar to error messages, to keep stdout clean for the primary application output (the generated passwords).
Step-by-Step Tasks
1. Add log and env_logger Crates
First, we need to add the log facade and the env_logger backend as dependencies.
Open rpassword-gen/Cargo.toml and add these lines:
[package]
name = "rpassword-gen"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
rand = "0.8"
log = "0.4" # Add this line
env_logger = "0.10" # Add this line
Save Cargo.toml.
2. Initialize env_logger and Add Log Messages
Now, we’ll initialize env_logger early in our main function and sprinkle some debug! and info! messages throughout our run and build_char_pool functions.
Update src/main.rs:
use clap::Parser;
use rand::seq::SliceRandom;
use rand::Rng;
use std::fmt;
use std::process;
// Import log macros
use log::{debug, info, error};
// Define character sets as constants
const LOWERCASE_CHARS: &str = "abcdefghijklmnopqrstuvwxyz";
const UPPERCASE_CHARS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const NUMERIC_CHARS: &str = "0123456789";
const SYMBOL_CHARS: &str = "!@#$%^&*()-_+=[]{}|;:,.<>/?";
/// Custom error types for our password generator.
#[derive(Debug)]
enum PasswordGenError {
ZeroLength,
ZeroCount,
NoCharTypesSelected,
}
// Implement the Display trait for user-friendly error messages
impl fmt::Display for PasswordGenError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PasswordGenError::ZeroLength => write!(f, "Password length cannot be zero. Please specify a length greater than 0."),
PasswordGenError::ZeroCount => write!(f, "Password count cannot be zero. Please specify a count greater than 0."),
PasswordGenError::NoCharTypesSelected => write!(f, "No character types available for password generation. Please specify at least one character type (e.g., -U, -L, -n, -s) or run without character flags to use all defaults."),
}
}
}
/// A powerful and customizable command-line password generator written in Rust.
#[derive(Parser, Debug)]
#[command(author, version, about = "Generate strong, customizable passwords.", long_about = None)]
struct Args {
/// The length of the password to generate.
#[arg(short, long, default_value_t = 16)]
length: usize,
/// The number of passwords to generate.
#[arg(short, long, default_value_t = 1)]
count: usize,
/// Include uppercase letters (A-Z) in the password.
#[arg(short = 'U', long, default_value_t = false)]
uppercase: bool,
/// Include lowercase letters (a-z) in the password.
#[arg(short = 'L', long, default_value_t = false)]
lowercase: bool,
/// Include numbers (0-9) in the password.
#[arg(short, long, default_value_t = false)]
numbers: bool,
/// Include special characters (!@#$%^&*) in the password.
#[arg(short, long, default_value_t = false)]
symbols: bool,
}
// Function to build the character pool based on Args
fn build_char_pool(args: &Args) -> String {
debug!("Building character pool with args: {:?}", args);
let mut char_pool = String::new();
let mut selected_any = false;
if args.uppercase {
char_pool.push_str(UPPERCASE_CHARS);
selected_any = true;
debug!("Including uppercase characters.");
}
if args.lowercase {
char_pool.push_str(LOWERCASE_CHARS);
selected_any = true;
debug!("Including lowercase characters.");
}
if args.numbers {
char_pool.push_str(NUMERIC_CHARS);
selected_any = true;
debug!("Including numeric characters.");
}
if args.symbols {
char_pool.push_str(SYMBOL_CHARS);
selected_any = true;
debug!("Including symbol characters.");
}
if !selected_any {
char_pool.push_str(LOWERCASE_CHARS);
char_pool.push_str(UPPERCASE_CHARS);
char_pool.push_str(NUMERIC_CHARS);
char_pool.push_str(SYMBOL_CHARS);
info!("No character types specified, defaulting to all character sets.");
}
debug!("Final character pool: {}", char_pool);
char_pool
}
// New function to generate a single password, returning a Result
fn generate_single_password(args: &Args, pool_chars: &[char]) -> String {
debug!("Generating a password of length {} from pool size {}", args.length, pool_chars.len());
let mut password = String::new();
let mut rng = rand::thread_rng();
for _ in 0..args.length {
let random_char = pool_chars.choose(&mut rng).expect("Character pool should not be empty, checked upstream.");
password.push(*random_char);
}
debug!("Generated password segment.");
password
}
// Our main logic now lives in `run`, returning a Result
fn run() -> Result<(), PasswordGenError> {
let args = Args::parse();
info!("Application started with parsed arguments: {:?}", args);
if args.length == 0 {
error!("Attempted to generate password with zero length.");
return Err(PasswordGenError::ZeroLength);
}
if args.count == 0 {
error!("Attempted to generate zero passwords.");
return Err(PasswordGenError::ZeroCount);
}
let char_pool = build_char_pool(&args);
if char_pool.is_empty() {
error!("Character pool ended up empty after construction.");
return Err(PasswordGenError::NoCharTypesSelected);
}
let pool_chars: Vec<char> = char_pool.chars().collect();
// No need to initialize rng here as it's done within generate_single_password,
// which is suitable for a small number of generations.
// If generating millions of passwords, moving RNG out of the inner loop
// could be considered but generate_single_password only uses one per call.
for i in 0..args.count {
debug!("Generating password {}/{}", i + 1, args.count);
let password = generate_single_password(&args, &pool_chars);
println!("{}", password);
}
Ok(()) // Indicate success
}
fn main() {
// Initialize env_logger before any logging calls
env_logger::init();
info!("Logger initialized.");
// Call the run function and handle any errors
if let Err(e) = run() {
// Error messages are already printed by the Display impl of PasswordGenError
// to stderr, so we just use error! macro for consistency or additional context.
error!("Application encountered an error: {}", e);
process::exit(1);
}
info!("Application finished successfully.");
}
Key changes:
use log::{debug, info, error};: Imports the logging macros.env_logger::init();inmain(): This initializes theenv_logger. It reads theRUST_LOGenvironment variable and configures logging accordingly. It should be called once at the very beginning ofmain.- Log messages:
info!: Used for general operational messages, like application start/end, or when default behavior is triggered.debug!: Used for more detailed internal information, such as parsing arguments, building character pools, or entering/exiting functions. These are typically only seen when debugging.error!: Used in the error handling path, alongside oureprintln!to ensure errors are recorded through the logging system too.
3. Test Logging
Save src/main.rs.
Run normally (no logs visible):
cargo run -- -l 10
Expected: Only the generated password on stdout.
Run with INFO level logs:
RUST_LOG=info cargo run -- -l 10
Expected output (to stderr, followed by password to stdout):
INFO rpassword_gen: Logger initialized.
INFO rpassword_gen: Application started with parsed arguments: Args { length: 10, count: 1, uppercase: false, lowercase: false, numbers: false, symbols: false }
INFO rpassword_gen: No character types specified, defaulting to all character sets.
INFO rpassword_gen: Application finished successfully.
<generated_password>
Run with DEBUG level logs:
RUST_LOG=debug cargo run -- -l 10 -U -n
Expected output (to stderr, followed by password to stdout):
DEBUG rpassword_gen: Building character pool with args: Args { length: 10, count: 1, uppercase: true, lowercase: false, numbers: true, symbols: false }
DEBUG rpassword_gen: Including uppercase characters.
DEBUG rpassword_gen: Including numeric characters.
DEBUG rpassword_gen: Final character pool: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
DEBUG rpassword_gen: Generating a password of length 10 from pool size 36
DEBUG rpassword_gen: Generated password segment.
INFO rpassword_gen: Application started with parsed arguments: Args { length: 10, count: 1, uppercase: true, lowercase: false, numbers: true, symbols: false }
INFO rpassword_gen: Logger initialized.
INFO rpassword_gen: Application finished successfully.
<generated_password>
(Note: The order of log messages might vary slightly in stderr depending on internal buffering, but typically env_logger::init() logs first).
Run with ERROR level logs on an error condition:
RUST_LOG=error cargo run -- -l 0
Expected output (to stderr):
ERROR rpassword_gen: Attempted to generate password with zero length.
ERROR rpassword_gen: Application encountered an error: Password length cannot be zero. Please specify a length greater than 0.
Tips/Challenges/Errors
- Log Level Granularity: You can specify
RUST_LOGfor specific modules (e.g.,RUST_LOG=rpassword_gen::build_char_pool=debug). For a small app,RUST_LOG=debugis usually sufficient. env_loggerFeatures:env_loggerhas more features, likedefault_filter_orif you want to set a default log level ifRUST_LOGisn’t set. For this guide, explicitRUST_LOGis simple and effective.- Performance Impact: Logging can have a performance impact, especially at
TRACElevel. In a CLI, this is usually negligible unless performing extremely performance-sensitive operations.
Summary/Key Takeaways
In this chapter, you successfully:
- Added the
logandenv_loggercrates to your project. - Initialized
env_loggerto enable log output configurable via theRUST_LOGenvironment variable. - Integrated
info!,debug!, anderror!log messages throughout your application’s logic. - Verified that logs appear on
stderrat different verbosity levels.
With logging in place, our tool is now much easier to understand and debug. Next, we’ll add basic unit tests to ensure our core logic remains correct as we evolve the application.