Skip to main content
Ironclad uses the tracing crate for structured, async-aware logging that provides powerful observability for your application.

Logging Setup

Basic Configuration

src/main.rs
use tracing_subscriber;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Configure logging
    tracing_subscriber::fmt()
        .without_time()
        .with_target(false)
        .with_thread_ids(false)
        .with_thread_names(false)
        .with_file(false)
        .with_line_number(false)
        .with_env_filter(
            tracing_subscriber::EnvFilter::new("info,actix_server=warn,ironclads=debug")
        )
        .init();

    tracing::info!("╔════════════════════════════════════════════════════╗");
    tracing::info!("║    🚀 Rust Ironclad Framework (DDD Architecture)  ║");
    tracing::info!("╚════════════════════════════════════════════════════╝");
    tracing::info!("📍 Server: http://{}:{}", app_config.server.host, app_config.server.port);

    // ... rest of setup
}

Configuration Options

without_time()
method
Removes timestamps from log output (useful for systemd or Docker logs that add their own timestamps)
with_target(false)
method
Hides the module path in logs (e.g., ironclad::auth::service)
with_thread_ids(false)
method
Hides thread IDs from log output
with_file(false)
method
Hides file names from log output
with_line_number(false)
method
Hides line numbers from log output
with_env_filter()
method
Sets log level filtering based on environment variables or configuration

Log Levels

Available Levels

tracing::error!("Critical error occurred: {}", error);   // Level 1 - Errors
tracing::warn!("Warning: potential issue detected");      // Level 2 - Warnings
tracing::info!("Application started successfully");       // Level 3 - Info
tracing::debug!("User ID: {}, processing request", id);  // Level 4 - Debug
tracing::trace!("Detailed trace information");            // Level 5 - Trace

Log Level Hierarchy

  • error → Shows only errors
  • warn → Shows errors + warnings
  • info → Shows errors + warnings + info (default)
  • debug → Shows errors + warnings + info + debug
  • trace → Shows everything

Environment-Based Configuration

Using RUST_LOG Environment Variable

.env
# Development - verbose logging
RUST_LOG=debug

# Production - minimal logging
RUST_LOG=warn

# Custom filtering
RUST_LOG=info,actix_server=warn,ironclads=debug

Multiple Module Filtering

tracing_subscriber::EnvFilter::new("info,actix_server=warn,ironclads=debug")
This configuration:
  • Sets default level to info
  • Reduces actix_server noise to warn level
  • Increases your app (ironclads) to debug level

Running with Custom Log Levels

# Debug mode
RUST_LOG=debug cargo run

# Specific module debugging
RUST_LOG=ironclads::auth=trace cargo run

# Multiple modules
RUST_LOG=ironclads::auth=debug,ironclads::db=trace cargo run

# Production mode
RUST_LOG=error cargo run --release

Structured Logging

Basic Logging

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

// Simple message
info!("User logged in");

// With variables
info!("User {} logged in", username);

// With structured fields
info!(
    user_id = %user.id,
    username = %user.username,
    "User logged in successfully"
);

Structured Fields

// Display format (%)
info!(user_id = %uuid, "Processing request");

// Debug format (?)
info!(user_data = ?user, "User details");

// Raw value
info!(count = items.len(), "Items processed");

// Multiple fields
info!(
    user_id = %user.id,
    email = %user.email,
    action = "login",
    ip_address = %req.connection_info().peer_addr(),
    "User authentication"
);

Logging in Different Layers

Controller Layer

use tracing::{info, error};
use actix_web::{web, HttpResponse};

pub async fn login_handler(
    credentials: web::Json<LoginDto>,
) -> Result<HttpResponse, ApiError> {
    info!(
        email = %credentials.email,
        "Login attempt"
    );

    match auth_service.login(credentials.into_inner()).await {
        Ok(response) => {
            info!(
                user_id = %response.user.id,
                "Login successful"
            );
            Ok(HttpResponse::Ok().json(response))
        }
        Err(e) => {
            error!(
                email = %credentials.email,
                error = %e,
                "Login failed"
            );
            Err(e)
        }
    }
}

Service Layer

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

impl AuthService {
    pub async fn register(&self, dto: RegisterDto) -> ApiResult<AuthResponse> {
        debug!(
            email = %dto.email,
            username = %dto.username,
            "Starting user registration"
        );

        // Check if user exists
        if self.user_exists(&dto.email).await? {
            warn!(
                email = %dto.email,
                "Registration attempt with existing email"
            );
            return Err(ApiError::Conflict(
                format!("User with email {} already exists", dto.email)
            ));
        }

        // Create user
        let user = self.create_user(dto).await?;
        
        info!(
            user_id = %user.id,
            email = %user.email,
            "User registered successfully"
        );

        Ok(AuthResponse {
            user,
            token: self.generate_token(&user)?,
        })
    }
}

Repository Layer

use tracing::{debug, error};

impl UserRepository {
    pub async fn find_by_email(&self, email: &str) -> Result<Option<User>, sqlx::Error> {
        debug!(email = %email, "Querying user by email");

        match sqlx::query_as!(User, "SELECT * FROM users WHERE email = $1", email)
            .fetch_optional(&self.pool)
            .await
        {
            Ok(user) => {
                if user.is_some() {
                    debug!(email = %email, "User found");
                } else {
                    debug!(email = %email, "User not found");
                }
                Ok(user)
            }
            Err(e) => {
                error!(
                    email = %email,
                    error = %e,
                    "Database query failed"
                );
                Err(e)
            }
        }
    }
}

HTTP Request Logging

TracingLogger Middleware

src/main.rs
use tracing_actix_web::TracingLogger;

HttpServer::new(move || {
    App::new()
        .wrap(MaintenanceMode)
        .wrap(Cors::default())
        .wrap(TracingLogger::default())  // Automatic HTTP request/response logging
        .configure(routes::configure)
})

Output Example

INFO HTTP request
  method: GET
  uri: /api/users/550e8400-e29b-41d4-a716-446655440000
  version: HTTP/1.1

INFO HTTP response
  status: 200
  latency_ms: 45

Performance Monitoring

Measuring Function Duration

use tracing::{info, instrument};
use std::time::Instant;

pub async fn expensive_operation() -> Result<(), ApiError> {
    let start = Instant::now();
    
    // Perform operation
    process_data().await?;
    
    let duration = start.elapsed();
    info!(
        duration_ms = duration.as_millis(),
        "Expensive operation completed"
    );
    
    Ok(())
}

Automatic Instrumentation

use tracing::instrument;

#[instrument(skip(self))]  // Don't log 'self'
pub async fn get_user(&self, user_id: Uuid) -> ApiResult<User> {
    // Function entry/exit automatically logged
    // with all parameters
    self.repository.find_by_id(user_id).await
}

#[instrument(skip(pool), fields(user_id = %user_id))]
pub async fn find_user(pool: &PgPool, user_id: Uuid) -> Result<User, sqlx::Error> {
    sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
        .fetch_one(pool)
        .await
}

Error Logging

Comprehensive Error Logging

use tracing::error;

pub async fn process_payment(amount: f64) -> ApiResult<Payment> {
    match payment_gateway.charge(amount).await {
        Ok(payment) => {
            info!(
                payment_id = %payment.id,
                amount = amount,
                "Payment processed successfully"
            );
            Ok(payment)
        }
        Err(e) => {
            error!(
                amount = amount,
                error = %e,
                error_type = ?std::any::type_name_of_val(&e),
                "Payment processing failed"
            );
            Err(ApiError::InternalServerError(
                "Payment processing failed".to_string()
            ))
        }
    }
}

Logging with Context

use tracing::{error, warn};

pub async fn update_user(
    user_id: Uuid,
    update_data: UpdateUserDto,
) -> ApiResult<User> {
    match repository.update(user_id, update_data).await {
        Ok(user) => Ok(user),
        Err(e) => {
            if let Some(db_err) = e.as_database_error() {
                if db_err.is_unique_violation() {
                    warn!(
                        user_id = %user_id,
                        constraint = db_err.constraint(),
                        "Unique constraint violation"
                    );
                    return Err(ApiError::Conflict("Email already exists".to_string()));
                }
            }
            
            error!(
                user_id = %user_id,
                error = %e,
                "Failed to update user"
            );
            Err(ApiError::DatabaseError(e.to_string()))
        }
    }
}

Log File Output

Writing to File

use tracing_appender::{non_blocking, rolling};
use tracing_subscriber::layer::SubscriberExt;

fn init_logging() {
    let file_appender = rolling::daily("storage/logs", "ironclad.log");
    let (non_blocking, _guard) = non_blocking(file_appender);

    let subscriber = tracing_subscriber::fmt()
        .with_writer(non_blocking)
        .with_ansi(false)
        .finish();

    tracing::subscriber::set_global_default(subscriber)
        .expect("Failed to set subscriber");
}

Multiple Outputs (Console + File)

use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

fn init_logging() {
    let file_appender = rolling::daily("storage/logs", "ironclad.log");
    let (file_writer, _guard) = non_blocking(file_appender);

    tracing_subscriber::registry()
        .with(tracing_subscriber::fmt::layer())
        .with(
            tracing_subscriber::fmt::layer()
                .with_writer(file_writer)
                .with_ansi(false)
        )
        .init();
}

Best Practices

What to Log
  • ✅ User authentication events
  • ✅ Database operations (especially failures)
  • ✅ External API calls
  • ✅ Business-critical operations
  • ✅ Error conditions with context
  • ✅ Performance metrics for slow operations
What NOT to Log
  • ❌ Passwords or authentication tokens
  • ❌ Credit card numbers or PII
  • ❌ Full request/response bodies (unless debugging)
  • ❌ Excessive debug logs in production
  • ❌ Sensitive business logic details

Logging Sensitive Data

// ❌ BAD - logs password
info!(password = %credentials.password, "Login attempt");

// ✅ GOOD - omits sensitive data
info!(email = %credentials.email, "Login attempt");

// ✅ GOOD - masks sensitive data
info!(
    email = %credentials.email,
    password_length = credentials.password.len(),
    "Login attempt"
);

Production Logging

let env = env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string());

let log_level = match env.as_str() {
    "production" => "warn",
    "staging" => "info",
    _ => "debug",
};

tracing_subscriber::fmt()
    .with_env_filter(tracing_subscriber::EnvFilter::new(log_level))
    .init();

Monitoring and Observability

Health Check Logging

use tracing::info;

pub async fn health_check() -> HttpResponse {
    info!("Health check requested");
    
    // Check database
    match sqlx::query("SELECT 1").execute(&pool).await {
        Ok(_) => {
            info!("Health check passed");
            HttpResponse::Ok().json(json!({
                "status": "healthy",
                "database": "connected"
            }))
        }
        Err(e) => {
            error!(error = %e, "Health check failed: database error");
            HttpResponse::ServiceUnavailable().json(json!({
                "status": "unhealthy",
                "database": "disconnected"
            }))
        }
    }
}

Startup Logging

src/main.rs
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Initialize logging first
    init_logging();

    info!("╔════════════════════════════════════════════════════╗");
    info!("║    🚀 Rust Ironclad Framework (DDD Architecture)  ║");
    info!("╚════════════════════════════════════════════════════╝");
    info!("📍 Server: http://{}:{}", app_config.server.host, app_config.server.port);

    // Database initialization
    let pg_pool = db::postgres::init_pool(&app_config.db_postgres)
        .await
        .expect("Failed to initialize PostgreSQL pool");
    info!("✅ PostgreSQL connected");

    // MongoDB (optional)
    if let Some(mongo_config) = &app_config.mongodb {
        match db::mongo::init_mongodb(mongo_config).await {
            Ok(_) => info!("✅ MongoDB connected"),
            Err(e) => warn!("⚠️  MongoDB skipped: {}", e),
        }
    }

    info!("🌐 Listening on http://{}", address);
    info!("🔗 Documentation: http://{}:{}/api/docs", 
          app_config.server.host, app_config.server.port);

    // Start server
    HttpServer::new(/* ... */)
        .bind(&address)?
        .run()
        .await
}

Troubleshooting

Problem: Logs not showing upSolution: Check RUST_LOG environment variable
# Test with debug level
RUST_LOG=debug cargo run

# Verify initialization
# Ensure tracing_subscriber::fmt().init() is called
Problem: Log volume is too highSolution: Adjust log levels
# Production
RUST_LOG=warn,ironclads=info

# Or in code
tracing_subscriber::EnvFilter::new("warn")
Problem: Structured fields not appearingSolution: Use correct format specifiers
// ❌ Wrong
info!("user_id: {}", user_id);

// ✅ Correct
info!(user_id = %user_id, "User action");

Next Steps

Error Handling

Learn about error types and logging

Deployment

Deploy with proper logging configuration

Build docs developers (and LLMs) love