Skip to content

Colored Integration Guide

RAprogramm edited this page Oct 20, 2025 · 1 revision

Integration Guide

Using colored errors with tracing, log, and web frameworks

This guide shows how to integrate masterror's colored terminal output with popular Rust logging, tracing, and web frameworks.

Table of Contents

Overview

Masterror's colored output integrates seamlessly with Rust's error handling ecosystem. The colors are automatically applied when errors are formatted using Display, making integration straightforward.

Key integration points:

  • tracing::error!("{}", err) - Logs colored errors to tracing subscribers
  • log::error!("{}", err) - Logs colored errors to log backends
  • HTTP response conversion - Colors appear in server logs, not HTTP bodies
  • Custom error handlers - Full control over when colors are applied

With tracing

The tracing crate is the modern standard for structured logging in async Rust applications.

Basic Integration

use masterror::AppError;
use tracing::{error, info};

#[tokio::main]
async fn main() {
    // Initialize tracing subscriber
    tracing_subscriber::fmt()
        .with_ansi(true)  // Enable ANSI colors
        .with_target(false)
        .init();

    match fetch_user(42).await {
        Ok(user) => info!("Fetched user: {}", user.name),
        Err(err) => error!("Failed to fetch user: {}", err),
        //                                               ^^^
        //                           Colored output appears in terminal
    }
}

async fn fetch_user(id: u64) -> Result<User, AppError> {
    database::query("SELECT * FROM users WHERE id = ?")
        .bind(id)
        .fetch_one()
        .await
        .map_err(|e| AppError::database_with_message("User query failed")
            .with_context(e)
            .with_field(field::u64("user_id", id)))
}

Terminal output:

2025-10-20T15:04:23.123Z ERROR Failed to fetch user: Error: Database error
Code: DATABASE
Message: User query failed
...

Structured Fields with Colored Errors

use masterror::AppError;
use tracing::error;

let err = AppError::database_with_message("Connection pool exhausted")
    .with_field(field::u64("pool_size", 20))
    .with_field(field::u64("active_connections", 20));

// Log with structured fields
error!(
    error = %err,              // Display formatting (colored)
    error_code = %err.code(),  // Extract specific fields
    "Database operation failed"
);

Output:

2025-10-20T15:04:23.123Z ERROR Database operation failed error="Error: Database error\nCode: DATABASE\n..." error_code="DATABASE"

Tracing Spans with Errors

use masterror::AppError;
use tracing::{error, instrument};

#[instrument(skip(db))]
async fn process_payment(
    db: &Database,
    payment_id: u64,
    amount: u64
) -> Result<Receipt, AppError> {
    let span = tracing::info_span!("process_payment", payment_id, amount);
    let _enter = span.enter();

    let result = db.execute_transaction(payment_id, amount).await;

    match result {
        Ok(receipt) => Ok(receipt),
        Err(e) => {
            let err = AppError::database_with_message("Payment transaction failed")
                .with_context(e)
                .with_field(field::u64("payment_id", payment_id))
                .with_field(field::u64("amount", amount));

            error!("Transaction failed: {}", err);
            Err(err)
        }
    }
}

Custom Subscriber for File Logging

When logging to files, you typically want to disable colors:

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

fn init_logging() {
    let file_layer = fmt::layer()
        .with_writer(std::fs::File::create("app.log").unwrap())
        .with_ansi(false);  // Disable colors for file output

    let stdout_layer = fmt::layer()
        .with_writer(std::io::stdout)
        .with_ansi(true);   // Enable colors for terminal

    tracing_subscriber::registry()
        .with(file_layer)
        .with(stdout_layer)
        .init();
}

With this setup:

  • Terminal: Colored output for developer visibility
  • File: Plain text for log processing tools

With log

The log crate provides a simpler facade for logging.

Basic Integration

use log::{error, info};
use masterror::AppError;

fn main() {
    // Initialize env_logger with colors
    env_logger::Builder::from_default_env()
        .format_timestamp_millis()
        .init();

    match run_server() {
        Ok(_) => info!("Server stopped gracefully"),
        Err(err) => error!("Server error: {}", err),
    }
}

fn run_server() -> Result<(), AppError> {
    Err(AppError::config("Missing PORT environment variable"))
}

Terminal output:

[2025-10-20T15:04:23.123Z ERROR app] Server error: Error: Configuration error
Code: CONFIG
Message: Missing PORT environment variable

Custom Logger with Selective Coloring

use log::{Level, Metadata, Record};

struct CustomLogger;

impl log::Log for CustomLogger {
    fn enabled(&self, metadata: &Metadata) -> bool {
        metadata.level() <= Level::Info
    }

    fn log(&self, record: &Record) {
        if self.enabled(record.metadata()) {
            // Colors are automatically applied when errors are formatted
            eprintln!("[{}] {}", record.level(), record.args());
        }
    }

    fn flush(&self) {}
}

static LOGGER: CustomLogger = CustomLogger;

fn main() {
    log::set_logger(&LOGGER).unwrap();
    log::set_max_level(log::LevelFilter::Info);
}

With Axum

Axum is a modern web framework for Rust. Masterror includes built-in Axum integration via the axum feature.

Basic Setup

use axum::{
    Router,
    routing::get,
    extract::Path,
    response::IntoResponse,
};
use masterror::AppError;
use tracing::{error, info_span, Instrument};

#[tokio::main]
async fn main() {
    // Initialize tracing
    tracing_subscriber::fmt()
        .with_ansi(true)
        .init();

    let app = Router::new()
        .route("/users/:id", get(get_user));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    axum::serve(listener, app).await.unwrap();
}

async fn get_user(Path(id): Path<u64>) -> Result<String, AppError> {
    let span = info_span!("get_user", user_id = id);

    fetch_user(id)
        .instrument(span)
        .await
        .map(|user| format!("User: {}", user.name))
}

async fn fetch_user(id: u64) -> Result<User, AppError> {
    database::get_user(id)
        .await
        .ok_or_else(|| AppError::not_found("User not found")
            .with_field(field::u64("user_id", id)))
}

Important: Colors appear in server logs (stderr), not in HTTP response bodies. The IntoResponse implementation converts errors to JSON without ANSI codes.

Custom Error Handler with Logging

use axum::{
    Router,
    response::{IntoResponse, Response},
    http::StatusCode,
    Json,
};
use masterror::AppError;
use serde_json::json;
use tracing::error;

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        // Log colored error to terminal
        error!("Request error: {}", self);

        // Return clean JSON to client
        let status = StatusCode::from_u16(self.kind().http_status())
            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);

        let body = Json(json!({
            "error": {
                "code": self.code().to_string(),
                "message": self.message().unwrap_or("An error occurred"),
            }
        }));

        (status, body).into_response()
    }
}

Result:

  • Server logs: Colored terminal output with full error context
  • HTTP response: Clean JSON without ANSI codes
{
  "error": {
    "code": "NOT_FOUND",
    "message": "User not found"
  }
}

Middleware Error Logging

use axum::{
    middleware::{self, Next},
    response::Response,
    http::Request,
};
use tracing::error;

async fn log_errors<B>(
    req: Request<B>,
    next: Next<B>,
) -> Response {
    let response = next.run(req).await;

    // Log errors with colors if status >= 500
    if response.status().is_server_error() {
        error!(
            status = %response.status(),
            "Server error occurred"
        );
    }

    response
}

let app = Router::new()
    .route("/users/:id", get(get_user))
    .layer(middleware::from_fn(log_errors));

With Actix-Web

Actix-Web is a mature, high-performance web framework.

Basic Integration

use actix_web::{
    web, App, HttpResponse, HttpServer,
    error::ResponseError,
    http::StatusCode,
};
use masterror::AppError;
use tracing::error;

impl ResponseError for AppError {
    fn status_code(&self) -> StatusCode {
        StatusCode::from_u16(self.kind().http_status())
            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
    }

    fn error_response(&self) -> HttpResponse {
        // Log colored error
        error!("Request error: {}", self);

        // Return JSON response
        HttpResponse::build(self.status_code())
            .json(serde_json::json!({
                "error": {
                    "code": self.code().to_string(),
                    "message": self.message().unwrap_or("An error occurred"),
                }
            }))
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    tracing_subscriber::fmt()
        .with_ansi(true)
        .init();

    HttpServer::new(|| {
        App::new()
            .route("/users/{id}", web::get().to(get_user))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

async fn get_user(path: web::Path<u64>) -> Result<String, AppError> {
    let id = path.into_inner();

    fetch_user(id)
        .await
        .map(|user| format!("User: {}", user.name))
}

With tokio-console

For async task debugging, tokio-console benefits from colored error output.

use masterror::AppError;
use tracing::error;

#[tokio::main]
async fn main() {
    console_subscriber::init();

    let handle = tokio::spawn(async move {
        match risky_operation().await {
            Ok(_) => {}
            Err(err) => error!("Task failed: {}", err),
        }
    });

    handle.await.unwrap();
}

async fn risky_operation() -> Result<(), AppError> {
    Err(AppError::timeout("Operation exceeded deadline")
        .with_field(field::u64("timeout_ms", 5000)))
}

When viewing in tokio-console, the colored output helps identify error patterns across tasks.

Best Practices

1. Separate Logging from HTTP Responses

// Good: Log colored errors, return clean JSON
error!("Database error: {}", err);  // Terminal: colored
return Json(ErrorResponse { code: err.code() });  // HTTP: clean JSON

// Bad: Sending colored errors to HTTP clients
return HttpResponse::build(status).body(format!("{}", err));  // ANSI codes in HTTP!

2. Conditional Coloring Based on Environment

use std::env;

fn init_logging() {
    let use_colors = env::var("NO_COLOR").is_err()
        && env::var("CI").is_err();

    tracing_subscriber::fmt()
        .with_ansi(use_colors)
        .init();
}

3. Structured Logging with Errors

// Good: Extract fields for structured logging
error!(
    error_kind = ?err.kind(),
    error_code = %err.code(),
    user_id = %user_id,
    "User operation failed: {}",
    err
);

// Less ideal: Only log the formatted error
error!("Error: {}", err);

4. Use Spans for Context

use tracing::instrument;

#[instrument(skip(db))]
async fn create_user(
    db: &Database,
    email: String
) -> Result<User, AppError> {
    // Span automatically captures function args
    db.insert_user(email).await
        .map_err(|e| AppError::database_with_message("Failed to create user")
            .with_context(e))
}

5. Centralized Error Logging

use axum::{
    response::{IntoResponse, Response},
    http::StatusCode,
};
use tracing::{error, warn};

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        // Centralized logging logic
        match self.kind().http_status() {
            status if status >= 500 => {
                error!("Server error: {}", self);
            }
            status if status >= 400 => {
                warn!("Client error: {}", self);
            }
            _ => {}
        }

        let status = StatusCode::from_u16(self.kind().http_status())
            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);

        (status, self.to_string()).into_response()
    }
}

6. Testing with NO_COLOR

#[cfg(test)]
mod tests {
    use masterror::AppError;

    #[test]
    fn error_display_is_clean_in_tests() {
        // CI environments typically set NO_COLOR
        std::env::set_var("NO_COLOR", "1");

        let err = AppError::internal("Test error");
        let output = format!("{}", err);

        // No ANSI codes in test output
        assert!(!output.contains("\x1b["));
        assert!(output.contains("Internal server error"));
    }
}

Related Pages:


Previous: Color Scheme Reference | Next: Troubleshooting

Clone this wiki locally