As web developers, we have to deal with errors, warnings, bugs, and events that occur in our software, so it’s critical to understand the structure and behavior of our software through the monitoring and logging of important events to effectively gather sufficient data that can help us find root causes faster.
Sometimes, the terms tracing and logging are used interchangeably, but they are not exactly the same. We’ll learn how you can integrate logging with the Rust log crate and tracing with the tracing crate in your Rust application, the difference between them, and why they are an integral part of every software.
Both logging and tracing have similar use cases and a common purpose — to help you find the root causes of problems in your application.
Imagine you design an application used by over 50,000 users and suddenly it stops working in production 🙄 . You’ve tested it, of course, but it’s almost impossible to have 100% true test coverage, and, even if your test coverage is 100%, it doesn’t mean your code is perfect. Now, the entire team turns into a fire brigade.
How do you know what happened? If you had placed a piece of code in your app to log errors or warnings as they happened, you’d have easily referred to that data (log file) and figured out what message was recorded before the app stopped working or experienced a downtime. This would have helped you find the cause of the problem faster.
In the Rust ecosystem, the de-facto tool for integrating logging is the log crate. It provides a single API that abstracts over the actual logging implementation from other libraries. It’s designed to be used by other libraries to implement logging in whatever ways they desire, using the standard logging levels that are implemented as macros in Rust.
Here are the standard macros for different log levels:
use log::{ info, warn, error, debug, }; debug!("Something weird occured: {}", someDebugVariable); error!("{}", "And error occured"); info!("{:?}", "Take note"); warn!("{:#?}", "This is important");
Let’s use one of its implementations (you can find more in the docs ), the env_logger
. The env logger is a minimal configuration logger. It primarily allows you to configure your log using environment variables, like so:
RUST_LOG=info cargo run
Let’s explore how to use log with env_logger. Ensure you have Rust and its toolchain installed.
First, initialize a Rust app with Cargo by running cargo init
on your terminal, then add env_logger
and log
to your Cargo.toml
file as dependencies.
[dependencies] env_logger = "0.9.0" log = "0.4.16"
Log requires all libraries implementing it to add log as a dependency.
Next, import log
and its log
level macros in the file you intend to add logging to. For this example, we’ll have it in the main.rs
file.
Then, initialize env_logger
before using the macros, as shown below:
use log::{ info, error, debug, warn }; fn main() { env_logger::init(); error!("{}", "And error occured"); warn!("{:#?}", "This is important"); info!("{:?}", "Take note"); debug!("Something weird occured: {}", "Error"); }
If any of the log levels are used before env_logger
initialization, there will be no impact. It’s important to note that if you run this code with cargo run
, only the error message will be printed on the console.
This is because the error log level is the default log level and it’s the highest in the log levels hierarchy. You should see the docs for more configuration options and other implementation options that might be suitable for your project.
“In software engineering, tracing involves a specialized use of logging to record information about a program’s execution.” – Wikipedia
Tracing involves monitoring the flow of your code logic from start to finish during the execution process. It’s easy to find how relevant this can be for you, especially if you are designing a large application where too many things could go wrong and debugging could be a pain; tracing gives you a systematic overview of the activities in your code.
The Rust tracing library leverages tracing and provides devs with a full-scale framework that allows you to collect structured, event-based diagnostic information from your Rust program.
Code tracing involves three different stages:
Just like log, the Rust tracing library provides a robust API that allows library developers to implement the functionalities necessary to collect the needed trace data.
Several libraries have been written to work with tracing. You can find them in the tracing documentation.
Let’s take tracing-subscriber as an example to see how you can integrate tracing into your Rust app. Add tracing-subscriber
to your list of dependencies. Make sure to add tracing
as a dependency as well.
Your /Cargo.toml
file should look like this:
[package] name = "tracing_examples" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] tracing = "0.1.34" tracing-subscriber = "0.3.11"
Next, in your main.rs
file, import tracing, and its log levels.
use tracing::{info, error, Level}; use tracing_subscriber::FmtSubscriber; fn main() { let subscriber = FmtSubscriber::builder() .with_max_level(Level::TRACE) .finish(); tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let number_of_teams: i32 = 3; info!(number_of_teams, "We've got {} teams!", number_of_teams); }
In the code above, we’ve built a subscriber that logs formatted representations of tracing
events and sets the level to TRACE
, which captures all the details about the behavior of the application and enables error, warn, info, and debug levels.
The result should look like this:
2022-05-04T23:30:02.401492Z INFO tracing_examples: We've got 3 teams! number_of_teams=3
You can easily integrate with OpenTelemetry using this tracing telemetry crate. There is a lot more you can do with tracing, too. Check out the docs for more information and examples.
Slog, just like log, is an extensible logging library for Rust. It intends to be a superset for the standard log crate. However, unlike the log crate, it’s a little more difficult to work with. Here is an example of how you can log to the terminal; code is adapted from the docs.
#[macro_use] extern crate slog; extern crate slog_term; extern crate slog_async; use slog::Drain; fn main() { let decorator = slog_term::TermDecorator::new().build(); let drain = slog_term::FullFormat::new(decorator).build().fuse(); let drain = slog_async::Async::new(drain).build().fuse(); let _log = slog::Logger::root(drain, o!()); }
As you can see, this isn’t as straightforward as the standard log crate because there is no publicly available tracing alternative in the Rust ecosystem. If you know of anyone, you could share with us in the comment section.
No software is perfect. No one writes code and lets it run with the intention that everything will just work for eternity, and you should expect some unexpected things to happen. You should set up logging and tracing to help you monitor your application, track down bugs, and fix them before your users come yelling at you.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.