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.
Differences between logging and tracing in Rust
Both logging and tracing have similar use cases and a common purpose — to help you find the root causes of problems in your application.
Logging in Rust
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.
Tracing in Rust
“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:
- Instrumentation: this is where you add tracing code to your application source code
- Actual tracing: at this point during execution, the activities are written to the target platform for analysis
- Analysis: the stage where you analyze and evaluate the information your tracing system has gathered to find and understand problems in the application. This is also where you can plug in tools like LogRocket, Sentry, or Grafana to allow you to visualize your entire system workflow, performance, errors, and things you could improve
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.
Alternatives to Rust log and tracing libraries
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.
Conclusion
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.
The post Comparing logging and tracing in Rust appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/Y6vwhQl
via Read more