-
Notifications
You must be signed in to change notification settings - Fork 17
Holo Guide ‐ [error.rs] how to structure our errors.
error.rs
is a file that does not depend on any other file inside the holo-vrrp
and thus is the most independent file on the module. It just describes the errors we will be using through the module, how they are structured and how we will be tracing them.
In Rust, errors are handled using the Result
and Option
types. The Result<T, E>
type is used for operations that can succeed(Ok(T)
) or fail (Err(E)
), while Option is for values that might exist(Some(T)
) or not exist(None
). Rust encourages handling errors explicitly, avoiding exceptions. By implementing the std::error::Error trait, custom error types can be created and propagated throughout the program, ensuring robust error management.
Errors are key, and as your module gets bigger you may introduce new custom errors or remove some unused error types depending on what exactly you will be needing.
For VRRP, we will define the key errors based on what we have defined in our VRRP yang model.
There are various errors that we aim to maintain across our different modules. Notably, these include IoError
and InterfaceError
, which are global errors that exist independently of the specific module or layer in which we are operating.
After these are general errors are described, we shift our focus to our protocol specific errors, which in this case are vrrp specific errors. These are defined within our VRRP YANG model. Here is a yang description of an error from line 213 to line 242, where the vrrp global
errors are defined.
Among these errors we define the following checksum-error, ip-ttl-error, version-error and vrid-error; all defined under vrrp-error-global. These errors help ensure consistency with the VRRP standard, as defined in our YANG model.
Let us see how we describe this vrrp-error-global
and its child errors inside our error.rs
.
// error.rs
use std::fmt::Debug;
#[derive(Debug)]
pub enum GlobalError {
ChecksumError,
IpTtlError,
VersionError,
VridError,
}
impl std::fmt::Display for GlobalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GlobalError::ChecksumError => {
write!(f, "incorrect checksum received")
}
GlobalError::IpTtlError => {
write!(f, "invalid ttl received. IP ttl for vrrp should always be 255")
}
GlobalError::VersionError => {
write!(
f,
"invalid VRRP version received. only version 2 accepted"
)
}
GlobalError::VridError => {
write!(f, "vrid received is not amongst the configured VRIDs")
}
}
}
}
This is a solid start for defining how we structure our errors. The GlobalError
enum encapsulates the VRRP-specific errors, and by implementing both Debug and Display
, we ensure that these errors can be displayed effectively.
However, it is important to note that, despite its name, GlobalError
refers to global errors within the VRRP module and not the whole of holo
module. We also have general errors, which apply across multiple modules. For instance, the IoError
enum represents errors that are not specific to VRRP but apply to various networking operations.
Here is how the IoError
is defined:
// error.rs
use std::fmt::Debug;
use std::net::IpAddr;
use tracing::{warn, warn_span};
#[derive(Debug)]
pub enum IoError {
SocketError(std::io::Error),
MulticastJoinError(IpAddr, std::io::Error),
MulticastLeaveError(IpAddr, std::io::Error),
RecvError(std::io::Error),
RecvMissingSourceAddr,
SendError(std::io::Error),
}
impl std::fmt::Display for IoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IoError::SocketError(..) => {
write!(f, "failed to create raw IP socket")
}
IoError::MulticastJoinError(..) => {
write!(f, "failed to join multicast group")
}
IoError::MulticastLeaveError(..) => {
write!(f, "failed to leave multicast group")
}
IoError::RecvError(..) => {
write!(f, "failed to receive IP packet")
}
IoError::RecvMissingSourceAddr => {
write!(
f,
"failed to retrieve source address from received packet"
)
}
IoError::SendError(..) => {
write!(f, "failed to send IP packet")
}
}
}
}
So far, things are looking good—we’ve defined one group of VRRP-specific errors (GlobalError
) and another for general errors (IoError
). Both of these error groups have implemented the std::fmt::Display
trait, allowing us to print out the errors as needed.
Next, we need to unify these under a single struct, which we’ll call Error
. This struct will encapsulate both the general errors and the VRRP-specific errors, providing a cohesive way to handle all errors across the module. Here's how the Error
struct looks like so far:
// error.rs
use std::fmt::Debug;
use std::net::IpAddr;
use tracing::{warn, warn_span};
#[derive(Debug)]
pub enum Error {
// I/O errors
IoError(IoError),
// vrrp-ietf-yang-2018-03-13 specific errors
GlobalError(GlobalError),
}
#[derive(Debug)]
pub enum IoError {
// defined above...
}
#[derive(Debug)]
pub enum GlobalError {
// defined above...
}
With this setup, whenever an error occurs, we will call something like Error::GlobalError(/*GlobalError instance*/)
, depending on the type of error encountered. This ensures that both general and protocol-specific errors are handled in a unified manner.
Additionally, while we’ve focused on VRRP-specific errors based on the YANG model, you can extend this approach by adding custom errors that are not defined in the YANG model. However, it’s important to proceed with caution when doing so, ensuring that these custom errors are well-defined and fit logically within your protocol's overall structure.
We've defined our error structures, but one of the main reasons for having errors in the first place is to log them. As engineers, we rely on these logs to troubleshoot and trace what went wrong during execution.
For logging, we will define a method named log(&self)
in the Error
implementation. This method will enable structured logging using the tracing
crate. If you're unfamiliar with the difference between logging and tracing, this article explains it in detail.
Here’s how we define the log(&self) method inside our Error struct:
//error.rs
impl Error {
pub(crate) fn log(&self) {
match self {
Error::IoError(error) => {
error.log();
}
Error::GlobalError(error) => {
match error {
GlobalError::ChecksumError => {
warn_span!("global_error").in_scope(|| { warn!("invalid checksum received") })
},
GlobalError::IpTtlError => {
warn_span!("global_error").in_scope(|| { warn!("TTL for IP packet is not 255.") })
},
GlobalError::VersionError => {
warn_span!("global_error").in_scope(|| { warn!("invalid version received. only version 2 accepted.") })
},
GlobalError::VridError => {
warn_span!("global_error").in_scope(|| { warn!("vrid is not locally configured. ") })
},
}
},
}
}
}
impl IoError {
pub(crate) fn log(&self) {
match self {
IoError::SocketError(error) => {
warn!(error = %with_source(error), "{}", self);
}
IoError::MulticastJoinError(addr, error)
| IoError::MulticastLeaveError(addr, error) => {
warn!(?addr, error = %with_source(error), "{}", self);
}
IoError::RecvError(error) | IoError::SendError(error) => {
warn!(error = %with_source(error), "{}", self);
}
IoError::RecvMissingSourceAddr => {
warn!("{}", self);
}
}
}
}
Above, we have defined a log()
method inside IoError, while we have had GlobalError
manually define it's various types and their logging inside the impl Error
's log(&self)
method. You can choose either one of these, or a mixture of these to describe your logs
So, what have we achieved in terms of error handling so far?
-
First, we made a clear distinction between two error types: general errors found across multiple modules and protocol-specific errors.
-
Second, we explored our VRRP YANG module and found the
vrrp-global-error
group, which represents VRRP-specific errors. We created an enum for them (GlobalError
), implemented theDisplay
trait to allow proper error printing, and defined another enum (IoError
) for general errors that span across protocol implementations inholo
. Both enums were structured to print errors effectively for troubleshooting. -
Third, we unified these two error groups under a single
Error
enum. AlthoughIoError
andGlobalError
are just a starting point, they serve as a blueprint for how to define and handle errors in future modules. -
Finally, we implemented a
log
method to log bothGlobalError
andIoError
using thetracing
crate. Inholo
, all logs are saved to/var/log/holod.log
, ensuring that errors are tracked and logged in a structured and consistent manner.
We have so far created two main error groups, which is GlobalError
and IoError
, then placed them inside the Error
enum.
However, we have more error groups, such as VirtualRouterError
which have not yet been described here.
In the end, our Error
enum should look like the following:
// error.rs
#[derive(Debug)]
pub enum Error {
// I/O errors
IoError(IoError),
InterfaceError(String),
// vrrp-ietf-yang-2018-03-13 specific errors
GlobalError(GlobalError),
VirtualRouterError(VirtualRouterError),
}
This is the link to the whole error.rs file.
The file describes a lot more details that what we have covered in this guide so far. Although the file is lengthy, if you did go through this guide and understood the basics of how we build our errors, you should be comfortable to go through the file without much trouble. Copy and paste the whole file into your error.rs
and take your time to go through it. It is a lengthy file, but not too complicated; taking your time to go through it will definitely be helpful to you in the long run.
Let us test to make sure everything is running successfully. From our project's root directory, let us run:
cargo +nightly build
If this runs successfully, then everything so far is fine, and we haven't broken anything. We can create a commit showing what we have done so far:
git add .
git commit
And write the following commit message:
vrrp: create and describe errors inside error.rs
We have created error definitions inside the
error.rs file. Two main types of errors created:
- general errors which are used across most protocols
- protocol specific errors, mostly described inside the
protocols YANG model.
The error definitions have had `Display` trait implemented
allowing for them to be printed out and `log()` method has
been implemented in the main `Error` enum allowing for
logging when the service is already running. Both will be
used for troubleshooting.
Signed-off-by: My Name <[email protected]>
Save that commit, and now we have properly defined errors which we can use in future.
Note: You may have noticed a pattern with how our commits are structured:
[protocol/issue]: [brief-description]
[detailed description]
Signed-off-by: [Full Name] <[email]>
This is the general way we structure the commit messages inside holo
, and we would urge for you to structure the same way, for uniformity purposes.
With our errors defined, let us look at our packet.rs
next.
- Architecture
- Management Interfaces
- Developer's Documentation
- Example Topology
- Paul's Practical Guide