-
-
Notifications
You must be signed in to change notification settings - Fork 0
Colored 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.
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
The tracing crate is the modern standard for structured logging in async Rust applications.
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
...
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"
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)
}
}
}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
The log crate provides a simpler facade for logging.
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
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);
}Axum is a modern web framework for Rust. Masterror includes built-in Axum integration via the axum feature.
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.
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"
}
}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));Actix-Web is a mature, high-performance web framework.
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))
}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.
// 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!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();
}// 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);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))
}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()
}
}#[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:
- Colored Terminal Output - Main colored output guide
- Color Scheme Reference - Complete color mapping
- Troubleshooting - Common issues
Previous: Color Scheme Reference | Next: Troubleshooting →