Introduction
Flogging
provides an easy framework for logging.
Log entries can be sent to the console (stdout or stderr), file, memory log, or a custom handler. They can be formatted in various layouts: ISO8601, Simple, Unix TimeStamp, or a custom layout.
Macros and public functions are provided, with the macros being the simplest method of operation.
There are several levels for logging at:
- SEVERE,
- WARNING,
- INFO,
- CONFIG,
- FINE,
- FINER, and
- FINEST
There are even two special settings:
- ALL, and
- OFF
Overview
This guide is provided to assist you in both using and customizing FLogging
.
For those interested, I have uploaded the complete example project,
my_project
, to Github. It contains all of the code developed throughout this guide. Please note, that each example is stored under a different ‘branch’ of the repository.
Usage
To use FLogging
you have two options:
-
macros
For simple straight-forward logging.They are very simple and easy to use, with the minimum of coding required.
-
functions/methods
For when you require more flexibility in your logging regimen.As an extreme example, you could have separate log tracking for each function/method in your project. For example: separate log files. Though why such separation would be of use I have no idea.
Another example would be, if you only require logging in certain functions, with different requirements, then this could be achieved.
Customization
The available options are:
- Custom Handlers
- Custom Formatters
Each of these are independent of the other. You can use custom handlers with the built-in formatters, and custom formatters with the built-in handlers. Or, if you want to be very fancy, you can use your custom formatter with your custom handler!!! What a great idea, hey?
I have tried to make this process as simple as possible. Though there are many ways I could have gone with this, I decided that having separate modules/files for each one was the easiest and most efficient option, when it comes down to maintainability.
So, let’s get started.
Macros
Using macros is easy, simple, and straight-forward.
The following code is the main.rs
file from the my_project
example, under the
Usage_Macros
branch.
//
// File Name: main.rs
// Directory: src
// Project Name: my_project
//
// Copyright (C) 2025 Bradley Willcott
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This library (crate) is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library (crate) is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this library (crate). If not, see <https://www.gnu.org/licenses/>.
//
//!
//! # main file
//!
use flogging::*;
use std::{error::Error, result::Result};
// Setting up the module level logger.
const_logger!({
Logger::builder(module_path!())
.add_console_handler()
.remove_file("test_logs/usage.log")
.add_file_handler("test_logs/usage.log")
.set_level(Level::ALL)
.build()
});
#[logger]
fn do_something() {
entering!();
// do some work worth noting
let result = "Just something to log.";
info!("Did some work here.\n {result}");
// ...
fine!("Bit more detail.");
if let Err(e) = error_prone() {
warning!("Error: {}", e);
}
exiting!();
}
#[logger]
fn error_prone() -> Result<(), Box<dyn Error>> {
entering!();
let rtn = Err(Box::from("Bad day!"));
exiting!();
rtn
}
#[logger]
fn main() {
entering!();
info!("All logging macros accept the same parameters as `std::format!(...)`");
warning!("Those same macros (info, etc.) MUST have atleast one parameter.");
config!("This is running on Fedora Linux 42.");
do_something();
info!("Job's done.");
exiting!("Bye!");
}
Explain (Part 1)
Ok, what’s happening in this code?
Let’s look at the first part:
use flogging::*;
use std::{error::Error, result::Result};
// Setting up the module level logger.
const_logger!({ // <= [1]
Logger::builder(module_path!())
.add_console_handler()
.remove_file("test_logs/usage.log")
.add_file_handler("test_logs/usage.log")
.set_level(Level::ALL)
.build() // <= [2]
}); // <= [1] [3]
Some gotchas. Well atleast they keep getting me:
[1]
- You must have the internal braces wrapping the
{ Logger::builder(...) ... }
code.[2]
- Do not terminate the braced code with a “;
”.
In this example, after the.build()
[3]
- Don’t forget the final “;
” after the macro: “});
”
I think the first two lines are self explanatory.
So let’s dive into the const_logger({...});
macro.
The API says:
Setup module level logger access.
The basic macro syntax is:
const_logger!({/* the block of Rust code to build a Logger goes here */});
Notice there are curly braces "{}" wrapping the inner Rust code. **They are required**.
The code you put in here will depend on what configuration of Logger you want to setup.
As you can see, we are using the Logger::builder(...)
method. With this we can be very specific about the logger that we end-up with. Note that this method returns a LoggerBuilder
object and not a Logger
object. The appended methods, are all implemented under LoggerBuilder
. The final method above, build()
, returns the fully configured Logger
object.
The primary purpose of this macro, is to setup a mod/file level logger environment. This is required to be able to use the rest of the macros.
If you look into the API, you will find many possible options for your logger configuration.
Please note:
It states “module level”.
Each module or file will require its own instance of this setup. This is intentional, to keep things simple to follow and maintain. Each mod/file has a different module_path!
which can only be set within its own instance of this setup. Without this distinction, your logs would be an anonymous mess, such that you would not easily know from where a log entry came.
Let’s assume you want to setup just a “global” instance in lib.rs
, to be used by all of its “child” mods/files. This is what you might get from logging a function - do_it()
:
my_project->do_it [FINER ] Entry
or with individual instances (as currently required):
my_project::core::control->do_it [FINER ] Entry
where both examples are from the file: src/core/control.rs
.
Another reason for separate instances, is that you may want different logger configurations for each one, or atleast, for one or more of them.
One possibility, might be a ‘mod’ that uses add_pconsole_handler()
, to be able to have INFO
level log entries output to the console without any formatting, like this:
This text came from the file `src/core/mod.rs`, just to let you know.
instead of:
my_project::core::mod->do_it [INFO ] This text came from the file `src/core/mod.rs`, just to let you know.
You might set this particular file’s instance to log level Level::INFO
, and have regular types of textual output that would be a normal part of the running program.
Mix and match - Have fun!!!
Idea
How would you like an effective way of controlling all of the logging within the “lib” part of your crate? Of course, still allowing each mod/file instance to be individually set to its own level.
Let’s say you have something like this project (
my_project
) structure:
src/
lib.rs
core/
control.rs
mod.rs
You might have:
-
src/lib.rs
mod my_core; pub(crate) use flogging::*; pub use my_core::*; // // Cargo.toml // // [dependencies] // ctor = "0.5.0" use ctor::*; pub(crate) const DEBUG_LEVEL:Level = Level::ALL; // pub(crate) const DEBUG_LEVEL:Level = Level::OFF; /// /// Reset the log file each time `my_project` is loaded. /// /// This is an alternative to using `remove_file()` in /// the individual mod/file setup commands.\ /// Only useful if all child mods are using the same log file. /// #[ctor] fn reset_log(){ Logger::remove_file("test_logs/usage.log"); } #[cfg(test)] mod tests{ use super::*; #[test] fn control(){ my_core::control::do_it(); } #[test] fn my_core(){ my_core::do_it(); } }
-
src/core/control.rs
Notice the use ofDEBUG_LEVEL
:use crate::*; const_logger!({ Logger::builder(module_path!()) .add_console_handler() .add_file_handler("test_logs/usage.log") .set_level(DEBUG_LEVEL) // ^^^^^^^^^^^ .build() }); #[logger] pub fn do_it() { entering!(); info!("Hello from `Control`."); exiting!(); }
-
src/core/mod.rs
Notice the use ofDEBUG_LEVEL
, andadd_pconsole_handler()
:pub mod control; use crate::*; const_logger!({ Logger::builder(module_path!()) .add_pconsole_handler() // ^^^^^^^^ .add_file_handler("test_logs/usage.log") // .set_level(Level::INFO) .set_level(DEBUG_LEVEL) // ^^^^^^^^^^^ .build() }); #[logger] pub fn do_it() { entering!(); info!("This text came from the file `src/core/mod.rs`, just to let you know."); exiting!(); }
The possible output from running the src/lib.rs
tests is:
-
console
---- tests::control stdout ---- my_project::my_core::control->do_it [FINER ] Entry my_project::my_core::control->do_it [INFO ] Hello from `Control`. my_project::my_core::control->do_it [FINER ] Return ---- tests::my_core stdout ---- my_project::my_core->do_it [FINER ] Entry This text came from the file `src/core/mod.rs`, just to let you know. my_project::my_core->do_it [FINER ] Return
-
test_logs/usage.log
2025-08-27T10:05:48.581421788+08:00 my_project::my_core::control->do_it [FINER ] Entry 2025-08-27T10:05:48.581421917+08:00 my_project::my_core->do_it [FINER ] Entry 2025-08-27T10:05:48.581505388+08:00 my_project::my_core::control->do_it [INFO ] Hello from `Control`. 2025-08-27T10:05:48.581510841+08:00 my_project::my_core->do_it [INFO ] This text came from the file `src/core/mod.rs`, just to let you know. 2025-08-27T10:05:48.581528496+08:00 my_project::my_core::control->do_it [FINER ] Return 2025-08-27T10:05:48.581533425+08:00 my_project::my_core->do_it [FINER ] Return
Now let’s prepare for a production build.
First we change the global DEBUG_LEVEL
to OFF
, then we modify src/core/mod.rs
to be set to Level::INFO
.
-
src/lib.rs
... pub(crate) const DEBUG_LEVEL:Level = Level::OFF; ...
-
src/core/mod.rs
... .set_level(Level::INFO) ...
Now to see what comes out:
-
console
---- tests::my_core stdout ---- This text came from the file `src/core/mod.rs`, just to let you know.
-
test_logs/usage.log
2025-08-27T10:07:36.124653478+08:00 my_project::my_core->do_it [INFO ] This text came from the file `src/core/mod.rs`, just to let you know.
Next, let’s get rid of the log file (part of src/core/mod.rs
):
...
const_logger!({
Logger::builder(module_path!())
.add_pconsole_handler()
// ^^^^^^^^
// .add_file_handler("test_logs/usage.log")
// .set_level(DEBUG_LEVEL)
.set_level(Level::INFO)
// ^^^^^^^^^^^
.build()
});
...
And the final production output is:
-
console
---- tests::my_core stdout ---- This text came from the file `src/core/mod.rs`, just to let you know.
-
test_logs/usage.log
Explain (Part 2)
Now the next part of the code:
#[logger]
fn do_something() {
entering!();
// do some work worth noting
let result = "Just something to log.";
info!("Did some work here.\n {result}");
// ...
fine!("Bit more detail.");
if let Err(e) = error_prone() {
warning!("Error: {}", e);
}
exiting!();
}
Firstly, the attribute macro: logger
.
The API says:
Provides for logging within the attributed function/method.
This is required to be able to use the macros. It sets up the local variable used by
the other macros, and it also registers the function/method name used by the log
entries (if included in the formatter’s 'fmt_string').
This is what handles all of the repetitive coding needed for each of the logged functions/methods.
Wrap Up
Using the macros is simple, coding efficient and tidy. Part of this, is the ability to have a variable number of parameters. As a Java programmer, this is something that I miss! In Rust, functions/methods don’t have this option. As you will see with Methods
, this needs to be handled differently.
In this example, you’ve seen macros with:
- no parameters (
entering!()
andexiting()
), - a single
&str
(fine!("Bit more detail.")
), - a single
&str
with an interpolated variable (info!("Did some work here.\n {result}")
), - multiple parameters (
warning!("Error: {}", e)
)
I understand that this could have been interpolated, but it is shown here as an example of the ability to emulate macros likeformat!()
.
Methods
For when you need greater flexibility than having just the one logger per mod/file, you have the methods option.
For a comparison, let’s look at redoing the Macros example using the methods.
The following code is the main.rs
file from the my_project
example, under the
Usage_Methods
branch.
//
// File Name: main.rs
// Directory: src
// Project Name: my_project
//
// Copyright (C) 2025 Bradley Willcott
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This library (crate) is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library (crate) is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this library (crate). If not, see <https://www.gnu.org/licenses/>.
//
//!
//! # main file
//!
use flogging::*;
use std::{error::Error, result::Result};
pub(crate) const DEBUG_LEVEL:Level = Level::ALL;
fn do_something() {
let mut log = Logger::builder(module_path!())
.set_fn_name("do_something")
.add_econsole_handler()
.add_file_handler("test_logs/usage.log")
.set_level(DEBUG_LEVEL)
.build();
log.entering();
// do some work worth noting
let result = "Just something to log.";
log.info(&format!("Did some work here.\n {result}"));
// ...
log.fine("Bit more detail.");
if let Err(e) = error_prone() {
log.warning(&format!("Error: {}", e));
}
log.exiting();
}
fn error_prone() -> Result<(), Box<dyn Error>> {
let mut log = Logger::builder(module_path!())
.set_fn_name("error_prone")
.add_econsole_handler()
.add_file_handler("test_logs/usage.log")
.set_level(DEBUG_LEVEL)
.build();
log.entering();
let rtn = Err(Box::from("Bad day!"));
log.exiting_with(&format!("{rtn:?}"));
rtn
}
fn main() {
let mut log = Logger::builder(module_path!())
.set_fn_name("main")
.add_pconsole_handler()
.remove_file("test_logs/usage.log")
.add_file_handler("test_logs/usage.log")
.set_level(DEBUG_LEVEL)
.build();
log.entering();
log.info("All logging macros accept the same parameters as `std::format!(...)`");
log.warning("Those same macros (info, etc.) MUST have atleast one parameter.");
log.config("This is running on Fedora Linux 42.");
do_something();
log.info("Job's done.");
log.exiting();
}
Explain (Part 1)
If you compare the code under Methods
with that under Macros
, you will notice
up front that the macros has a file level ‘logger’, where as the methods has just a
global (crate) level constant (DEBUG_LEVEL
).
Now there is nothing stopping you from directly copying all of the expanded macro code into your files. I would consider that a waste of effort though, as that is what macros are for.
Now then, let’s look at the main()
function first:
fn main() {
let mut log = Logger::builder(module_path!())
.set_fn_name("main")
.add_pconsole_handler()
// ^^^^^^^^
.remove_file("test_logs/usage.log")
//^^^^^^^^^^
.add_file_handler("test_logs/usage.log")
.set_level(DEBUG_LEVEL)
.build();
log.entering();
log.info("All logging macros accept the same parameters as `std::format!(...)`");
log.warning("Those same macros (info, etc.) MUST have atleast one parameter.");
log.config("This is running on Fedora Linux 42.");
do_something();
log.info("Job's done.");
log.exiting();
}
First, notice the use of both add_pconsole_handler()
and remove_file()
, in comparison to
the other functions.
The use of pconcole instead of econsole, will have .info()
log
entries sent to the ‘stdout’ console, unformatted. This would facilitate providing normal
text messages to the user. Those messages that are part of the normal interaction with the program.
Yet still allowing other log entries to be highlighted as formatted output. And, since there is
also a file handler, of course all messages will go there, formatted.
Having the remove_file()
function here, allows you to always have just the entries from
the last run available in the log file. This is not necessary if you need to track entries
over multiple runs. It is here to keep it clean.
The code (Logger::builder...
) is basically a copy of that used in the macro version, that sets
up the file level ‘logger’. The main difference is that it also has the .set_fn_name("main")
function, which, among other things, is set by the #[logger]
macro in the macro version.
There other differences between the macro and method versions. That being the use of the log
variable
to access the logging functionality. Again we have more coding to keep track of.
Explain (Part 2)
Now let’s look at the do_something()
function.
fn do_something() {
let mut log = Logger::builder(module_path!())
.set_fn_name("do_something")
//^^^^^^^^^^
.add_econsole_handler()
// ^^^^^^^^
.add_file_handler("test_logs/usage.log")
.set_level(DEBUG_LEVEL)
.build();
log.entering();
// do some work worth noting
let result = "Just something to log.";
log.info(&format!("Did some work here.\n {result}"));
// ...
log.fine("Bit more detail.");
if let Err(e) = error_prone() {
log.warning(&format!("Error: {}", e));
}
log.exiting();
}
Notice we have the .set_fn_name("do_something")
and the .add_econsole_handler()
functions.
We need to set the function name in each function being logged, and in this case, we are sending
entries to the ‘stderr’ console. Also note, that there is no remove_file()
function.
The diversity of using different handlers in each function is one of the main reasons for using the methods over using the macros.
Now we get to one of my personal bug-bears with Rust (v1.88.0). No variable list of parameters for functions. I have been using Java for many years, and it is a major convenience being able to have that facility. So, here in this code we see the need to work around that limitation.
Let’s compare the macro version with the method version:
info!("Did some work here.\n {result}");
log.info(&format!("Did some work here.\n {result}"));
The method version requires the text to be preformatted before being passed to it. Where as, the macro version does that for you, internally.
The same goes for:
warning!("Error: {}", e);
log.warning(&format!("Error: {}", e));
Explain (Part 3)
And finally, we get to the last function.
fn error_prone() -> Result<(), Box<dyn Error>> {
let mut log = Logger::builder(module_path!())
.set_fn_name("error_prone")
.add_econsole_handler()
.add_file_handler("test_logs/usage.log")
.set_level(DEBUG_LEVEL)
.build();
log.entering();
let rtn = Err(Box::from("Bad day!"));
log.exiting_with(&format!("{rtn:?}"));
rtn
}
I expect you are noticing a pattern here. Boilerplate code everywhere.
Apart from the function name being different in each case, the rest of
the ‘logger’ code is the same as with the do_something()
function. And, I
would expect this to be the case for most situations.
We also have a case of using format!()
to preformat a message:
log.exiting_with(&format!("{rtn:?}"));
Wrap Up
The use of the methods directly, requires a lot more coding in general, hence maintenance, and the inclusion of boilerplating of the ‘logger’ setup.
The primary benefit, is the ability to provide different ‘logger’ configurations for each function, as needed.
As mentioned in the API, the methods are the backbone of the FLogging
crate. The macros
are simply convenient wrappers for them. Thereby removing the need for you to track and maintain
that code.
Hybrid
Now that I have shown you both sides of the Flogging
coin, let’s
talk practicality.
Both the macros and the methods have their benefits. The macros are convenient, and the methods are flexible. So, when needed, why not combine them?
What a great idea! Who would have thought?
So, let’s see what we can do with our example code.
The following code is the main.rs
file from the my_project
example, under the
Usage_Hybrid
branch.
//
// File Name: main.rs
// Directory: src
// Project Name: my_project
//
// Copyright (C) 2025 Bradley Willcott
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This library (crate) is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library (crate) is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this library (crate). If not, see <https://www.gnu.org/licenses/>.
//
//!
//! # main file
//!
use flogging::*;
use std::{error::Error, result::Result};
// Setting up the module level logger.
const_logger!({
Logger::builder(module_path!())
.add_console_handler()
.add_file_handler("test_logs/usage.log")
.set_level(Level::ALL)
.build()
});
#[logger]
fn do_something() {
entering!();
// do some work worth noting
let result = "Just something to log.";
info!("Did some work here.\n {result}");
// ...
fine!("Bit more detail.");
if let Err(e) = error_prone() {
warning!("Error: {}", e);
}
exiting!();
}
#[logger]
fn error_prone() -> Result<(), Box<dyn Error>> {
entering!();
let rtn = Err(Box::from("Bad day!"));
exiting!();
rtn
}
fn main() {
let mut log = Logger::builder(module_path!())
.set_fn_name("main")
.add_pconsole_handler()
.remove_file("test_logs/usage.log")
.add_file_handler("test_logs/usage.log")
.set_level(Level::ALL)
.build();
log.entering();
log.info("All logging macros accept the same parameters as `std::format!(...)`");
log.warning("Those same macros (info, etc.) MUST have atleast one parameter.");
log.config("This is running on Fedora Linux 42.");
do_something();
log.info("Job's done.");
log.exiting();
}
What have I done here?
For the bulk of the functions, in this case, do_something()
and error_prone()
, I am
using the macros. This prevents the boilerplating and other code bloat problems. Also, I
have removed the remove_file()
from the macro setup code.
However, for the main()
function, where we want to use the pconsole option, we are
using the methods directly. Notice we are using the same log file for all.
I have used main()
to be my ‘methods’ recipient here, but you could have any of your functions
being setup in a similar fashion.
Of course, there is no reason you couldn’t simply have the ‘macro’ setup, using the pconsole option,
and restricting your use of the INFO
level logging, to those messages that are expected to be
displayed during the normal use of your program.
In other words, FLogging
leaves it entirely up to you and your imagination, or your project’s
requirements, as to how it is used.
Mix and match to your hearts desire!
P.S.: See what happens when you set the ‘macro’ setup to Level::OFF
, and the main()
to Level::INFO
.
Custom Handlers
Why would you want to create your own handlers?
Well, you tell me!
OK, I suppose I can provide a few reasons. How about:
- sending log entries to:
- remote system log servers
- another program (local or remote) for live analysis, or some other processing
- storing log entries:
- in a specific file format (xml, json, csv)
- in a database
Actually, the “file format” option above may only need a custom formatter. I’ll leave that decision up to you, if you need one of them.
Step 1
Why?
Firstly, you need to know why you want/need to develop a custom handler.
For our example, we are going to develop a simple handler that combines outputting to both the ‘stdout’ console, and a log file. Note, that this would, at best, be a convenience handler, as it would replace two built-in handlers.
How?
Now you need to know how you would process each log entry, so that you could either send or store it some where, and how you would get it there.
For our example, we need to do two separate things:
-
println!("{}", self.con_fmt.format(log_entry));
-
let mut buf = self.file_fmt.format(log_entry); buf.push('\n'); self.file.as_mut().unwrap().write_all(buf.as_bytes()).expect("writeln!() failed");
Step 2
Now we need to check out the existing handlers and their code, to see which one is the closest to what we are after.
For our example, we could use either the ConsoleHandler
or the FileHandler
.
As file handling is the more complex task, we will use the FileHandler
as our template.
There are two ways to obtain the code. If you are viewing through the on-line API documentation, then at the top navigation bar, click “flogging-X.X.X”, where “X.X.X” is the version number, then under “LINKS” click “Source”. This will bring up the “Source” tab. Now we need to navigate to the required file.
- src
- handlers
- file_handler.rs
- handlers
Now select ALL of the code, from the top down, then ‘copy [ctrl/c]’. You need to include the file header (Copyright).
The other way, is to access the github repository: flogging. The directory structure is the same as above.
In your project src directory somewhere, create your new handler file, and paste this code into it.
For this example, (we’ll name it
my_project
) we’ll have the following basic layout:
- src/
lib.rs
main.rs- handlers/
confile_handler.rs
mod.rs- formatters/
csv_formatter.rs
- formatters/
- handlers/
- test_logs/
Our file will be called: confile_handler.rs
, with the module: ConfileHandler
.
First things first. We now need to do some changes:
file_handler.rs
toconfile_handler.rs
FileHandler
toConfileHandler
use crate::*;
touse flogging::*;
I have used a form of ‘diff’ to represent the changes:
unchanged code
- old line code
+ new line code
unchanged code
//
- // File Name: file_handler.rs
+ // File Name: confile_handler.rs
// Directory: src/handlers
- // Project Name: flogging
+ // Project Name: my_project
//
// Copyright (C) 2025 Bradley Willcott
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This library (crate) is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library (crate) is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this library (crate). If not, see <https://www.gnu.org/licenses/>.
//
//!
- //! # FileHandler
+ //! # ConfileHandler
//!
use std::{
fmt,
fs::{File, exists},
io::{Error, ErrorKind::InvalidInput, Write},
};
- use crate::*;
+ use flogging::*;
///
/// Publishes log entries to the file whose name was provided during
/// initialization.
///
#[derive(Debug, Default)]
- pub struct FileHandler {
+ pub struct ConfileHandler {
filename: String,
formatter: Formatter,
file: Option<File>,
writer: Option<Vec<u8>>,
}
- impl FileHandler {
+ impl ConfileHandler {
fn _create(filename: &str) -> Result<Self, Error> {
if filename.is_empty() {
return Err(Error::new(InvalidInput, "'filename' must not be empty"));
}
- let fh = FileHandler {
+ let fh = ConfileHandler {
filename: filename.to_string(),
formatter: FormatType::Iso8601.create(None),
file: {
let f = File::options().append(true).create(true).open(filename)?;
Some(f)
},
writer: None,
};
Ok(fh)
}
fn log(&self) -> String {
if let Some(w) = self.writer.to_owned() {
String::from_utf8(w).unwrap()
} else {
String::new()
}
}
}
- impl fmt::Display for FileHandler {
+ impl fmt::Display for ConfileHandler {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} : {}", self.filename, self.formatter)
}
}
- impl HandlerTrait for FileHandler {
+ impl HandlerTrait for ConfileHandler {
///
/// Create a new handler instance.
///
/// ## Parameters
/// - `name` - This the `filename` of the log file.
///
fn create(name: &str) -> Result<Self, Error> {
- FileHandler::_create(name)
+ ConfileHandler::_create(name)
}
///
/// Flushes and closes the file.\
/// Also, removes the internal buffer, if in `test_mode`.\
/// Will therefore, no longer be *in* `test_mode`.
///
fn close(&mut self) {
self.flush();
self.file = None;
}
fn flush(&mut self) {
if let Some(f) = &self.file {
f.sync_all().expect("sync_all failed");
}
}
fn get_formatter(&self) -> Formatter {
self.formatter.clone()
}
fn get_log(&self) -> String {
self.log()
}
fn is_open(&self) -> bool {
self.file.is_some()
}
fn publish(&mut self, log_entry: &LogEntry) {
if self.is_open() {
let mut buf = self.formatter.format(log_entry);
buf.push('\n');
if let Some(w) = self.writer.as_mut() {
writeln!(w, "{}", self.formatter.format(log_entry)).expect("writeln!() failed");
} else {
self.file
.as_mut()
.unwrap()
.write_all(buf.as_bytes())
.expect("write_all() failed");
}
}
}
fn set_formatter(&mut self, formatter: Formatter) {
self.formatter = formatter;
}
///
/// Sets the test mode to `state`.
///
/// If set to `true`, use `get_log()` to obtain the
/// log.
///
fn set_test_mode(&mut self, state: bool) {
if state {
// true
self.writer = Some(Vec::new());
} else {
self.writer = None;
}
}
}
#[cfg(test)]
mod tests {
use crate::*;
use std::{
fs::File,
io::{Error, Read, Result},
};
#[test]
fn file_handler() {
let mut log = Logger::file_logger(module_path!(), "test_logs/file_handler.log");
log.set_fn_name("file_handler");
let h = log.get_handler(crate::Handler::File).unwrap();
h.set_test_mode(false);
assert!(h.is_open());
assert_eq!(
h.get_formatter().to_string(),
"dt_fmt: \"%+\" - fmt_string: \"{dt:35} {mod_path}->{fn_name} [{level:7}] {message}\""
.to_string()
);
log.info("trait methods");
log.warning("The sky is falling!");
let h = log.get_handler(crate::Handler::File).unwrap();
assert_eq!(h.get_log(), "".to_string());
h.flush();
h.close();
log.exiting_with("This should get thrown away.");
}
#[test]
fn file_handler_file_test() {
let expected = "flogging::handlers::file_handler::tests->file_handler_file_test [INFO ] trait methods
flogging::handlers::file_handler::tests->file_handler_file_test [WARNING] The sky is falling!\n"
.to_string();
let mut log = Logger::builder(module_path!())
.set_fn_name("file_handler_file_test")
.remove_file("test_logs/file_handler_file_test.log")
.add_file_handler_with(
"test_logs/file_handler_file_test.log",
FormatType::Simple,
None,
)
.build();
let h = log.get_handler(crate::Handler::File).unwrap();
h.set_test_mode(false);
assert!(h.is_open());
assert_eq!(
h.get_formatter().to_string(),
"dt_fmt: \"\" - fmt_string: \"{mod_path}->{fn_name} [{level:7}] {message}\""
.to_string()
);
log.info("trait methods");
log.warning("The sky is falling!");
let h = log.get_handler(crate::Handler::File).unwrap();
assert_eq!(h.get_log(), "".to_string());
h.flush();
h.close();
assert!(!h.is_open());
log.severe("This should get thrown away.");
if let Ok(mut file) = File::open("test_logs/file_handler_file_test.log") {
let mut buf = String::new();
if let Ok(_count) = file.read_to_string(&mut buf) {
assert_eq!(expected, buf);
}
}
}
#[test]
fn file_handler_test_mode() {
let expected = "flogging::handlers::file_handler::tests->file_handler_test_mode [INFO ] trait methods
flogging::handlers::file_handler::tests->file_handler_test_mode [WARNING] The sky is falling!\n"
.to_string();
let mut log = Logger::builder(module_path!())
.set_fn_name("file_handler_test_mode")
.remove_file("test_logs/file_handler_test_mode.log")
.add_file_handler_with(
"test_logs/file_handler_test_mode.log",
FormatType::Simple,
None,
)
.build();
let h = log.get_handler(crate::Handler::File).unwrap();
h.set_test_mode(true);
assert!(h.is_open());
assert_eq!(
h.get_formatter().to_string(),
"dt_fmt: \"\" - fmt_string: \"{mod_path}->{fn_name} [{level:7}] {message}\""
.to_string()
);
log.info("trait methods");
log.warning("The sky is falling!");
let h = log.get_handler(crate::Handler::File).unwrap();
let buf = h.get_log();
assert_eq!(expected, buf);
h.flush();
h.close();
}
#[test]
#[should_panic(expected = "'filename' must not be empty")]
fn filename_empty() {
let _ = Logger::file_logger(module_path!(), "");
}
}
Step 3
With “Step 2” completed, you should be able to build your project without errors.
At this point, our new handler can output to any log file we need. However, we now need to add the console output component. For this exercise, we are going to use a different formatter for the console output, to that for the file output. To keep things simple, we will only provide a “default” formatter option. That is, there will be no ability to pass-in a different one later.
Firstly, we will need to modify: struct ConfileHandler
.
///
/// Publishes log entries to the file whose name was provided during
/// initialization.
///
#[derive(Debug, Default)]
pub struct ConfileHandler {
filename: String,
- formatter: Formatter,
+ con_fmt: Formatter,
+ file_fmt: Formatter,
file: Option<File>,
writer: Option<Vec<u8>>,
}
Now: impl ConfileHandler
.
impl ConfileHandler {
fn create(filename: &str) -> Result<Self, Error> {
if filename.is_empty() {
return Err(Error::new(InvalidInput, "'filename' must not be empty"));
}
let fh = ConfileHandler {
filename: filename.to_string(),
- formatter: FormatType::Iso8601.create(None),
+ con_fmt: FormatType::Simple.create(None),
+ file_fmt: FormatType::Iso8601.create(None),
file: {
let f = File::options().append(true).create(true).open(filename)?;
Some(f)
},
writer: None,
};
Ok(fh)
}
}
Now: impl fmt::Display for ConfileHandler
.
impl fmt::Display for ConfileHandler {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{} : {}", self.filename, self.formatter)
+ write!(
+ f,
+ "Console: {}\n{} : {}",
+ self.con_fmt, self.filename, self.file_fmt
+ )
}
}
Step 4
Ok, so far so good. Hopefully, your project still builds without errors.
Now we get to the actual working parts.
Firstly, some cleanup. We will make it possible to change the file_fmt
once we have developed our custom formatter. So let’s change get_formatter()
and set_formatter()
.
fn get_formatter(&self) -> Formatter {
- self.formatter.clone()
+ self.file_fmt.clone()
}
fn set_formatter(&mut self, formatter: Formatter) {
- self.formatter = formatter;
+ self.file_fmt = formatter;
}
Now the final changes:
fn publish(&mut self, log_entry: &LogEntry) {
if self.is_open() {
- let mut buf = self.formatter.format(log_entry);
+ let mut buf = self.file_fmt.format(log_entry);
buf.push('\n');
if let Some(w) = self.writer.as_mut() {
- writeln!(w, "{}", self.formatter.format(log_entry)).expect("writeln!() failed");
+ writeln!(w, "{}", self.con_fmt.format(log_entry)).expect("writeln!() failed");
+ writeln!(w, "{}", self.file_fmt.format(log_entry)).expect("writeln!() failed");
} else {
+ println!("{}", self.con_fmt.format(log_entry));
self.file
.as_mut()
.unwrap()
.write_all(buf.as_bytes())
.expect("write_all() failed");
}
}
}
It is done.
Step 5
Oops! Forgot the testing!!!!
Ok, what are we doing with that?
Since we are using a custom handler, we need to use the appropriate methods.
#[cfg(test)]
mod tests {
- use crate::*;
- use std::{
- fs::File,
- io::{Error, Read, Result},
- };
+ use super::*;
+ use std::io::Read;
+ use regex::Regex;
#[test]
- fn file_handler() {
+ fn confile_handler() {
- let mut log = Logger::file_logger(module_path!(), "test_logs/file_handler.log");
- log.set_fn_name("file_handler");
+ let mut log = Logger::custom_logger(
+ module_path!(),
+ "ConfileHandler",
+ Box::new(ConfileHandler::create("test_logs/confile_handler.log").unwrap()),
+ );
+
+ log.set_fn_name("confile_handler");
- let h = log.get_handler(crate::Handler::File).unwrap();
+ let h = log
+ .get_handler(Handler::Custom("ConfileHandler".to_string()))
+ .unwrap();
+
h.set_test_mode(false);
assert!(h.is_open());
assert_eq!(
h.get_formatter().to_string(),
"dt_fmt: \"%+\" - fmt_string: \"{dt:35} {mod_path}->{fn_name} [{level:7}] {message}\""
.to_string()
);
log.info("trait methods");
log.warning("The sky is falling!");
- let h = log.get_handler(crate::Handler::File).unwrap();
+ let h = log
+ .get_handler(Handler::Custom("ConfileHandler".to_string()))
+ .unwrap();
assert_eq!(h.get_log(), "".to_string());
h.flush();
h.close();
log.exiting_with("This should get thrown away.");
}
Possible output:
-
console
---- handlers::confile_handler::tests::confile_handler stdout ---- my_project::handlers::confile_handler::tests->confile_handler [INFO ] trait methods my_project::handlers::confile_handler::tests->confile_handler [WARNING] The sky is falling!
-
“test_logs/confile_handler.log”
Note: This file will continue to grow. So there maybe previous entries.2025-08-27T11:52:13.273205045+08:00 my_project::handlers::confile_handler::tests->confile_handler [INFO ] trait methods 2025-08-27T11:52:13.273294181+08:00 my_project::handlers::confile_handler::tests->confile_handler [WARNING] The sky is falling!
#[test]
- fn file_handler_file_test() {
+ fn confile_handler_file_test() {
- let expected = "flogging::handlers::file_handler::tests->file_handler_file_test [INFO ] trait methods
- flogging::handlers::file_handler::tests->file_handler_file_test [WARNING] The sky is falling!\n"
- .to_string();
+ let re_str =
+ "^(?:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}\\+\\d{2}:\\d{2}) my_project::handlers::confile_handler::tests->confile_handler_file_test \\[INFO ] trait methods
+ (?:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}\\+\\d{2}:\\d{2}) my_project::handlers::confile_handler::tests->confile_handler_file_test \\[WARNING] The sky is falling!
+ $";
+
+ let re = Regex::new(re_str).unwrap();
let mut log = Logger::builder(module_path!())
- .set_fn_name("file_handler_file_test")
- .remove_file("test_logs/file_handler_file_test.log")
- .add_file_handler_with(
- "test_logs/file_handler_file_test.log",
- FormatType::Simple,
- None,
- )
+ .set_fn_name("confile_handler_file_test")
+ .remove_file("test_logs/confile_handler_file_test.log")
+ .add_custom_handler(
+ "ConfileHandler",
+ Box::new(
+ ConfileHandler::create("test_logs/confile_handler_file_test.log").unwrap(),
+ ),
+ )
.build();
- let h = log.get_handler(crate::Handler::File).unwrap();
+ let h = log
+ .get_handler(crate::Handler::Custom("ConfileHandler".to_string()))
+ .unwrap();
+
h.set_test_mode(false);
assert!(h.is_open());
assert_eq!(
h.get_formatter().to_string(),
- "dt_fmt: \"\" - fmt_string: \"{mod_path}->{fn_name} [{level:7}] {message}\""
+ "dt_fmt: \"%+\" - fmt_string: \"{dt:35} {mod_path}->{fn_name} [{level:7}] {message}\""
.to_string()
);
log.info("trait methods");
log.warning("The sky is falling!");
- let h = log.get_handler(crate::Handler::File).unwrap();
+ let h = log
+ .get_handler(crate::Handler::Custom("ConfileHandler".to_string()))
+ .unwrap();
assert_eq!(h.get_log(), "".to_string());
h.flush();
h.close();
assert!(!h.is_open());
log.severe("This should get thrown away.");
- if let Ok(mut file) = File::open("test_logs/file_handler_file_test.log") {
+ if let Ok(mut file) = File::open("test_logs/confile_handler_file_test.log") {
let mut buf = String::new();
if let Ok(count) = file.read_to_string(&mut buf) {
- assert_eq!(expected, buf);
+ assert!(re.is_match(&buf));
}
}
}
Possible output:
-
console
---- handlers::confile_handler::tests::confile_handler_file_test stdout ---- my_project::handlers::confile_handler::tests->confile_handler_file_test [INFO ] trait methods my_project::handlers::confile_handler::tests->confile_handler_file_test [WARNING] The sky is falling!
-
“test_logs/confile_handler_file_test.log”
2025-08-27T11:54:07.165918573+08:00 my_project::handlers::confile_handler::tests->confile_handler_file_test [INFO ] trait methods 2025-08-27T11:54:07.166004387+08:00 my_project::handlers::confile_handler::tests->confile_handler_file_test [WARNING] The sky is falling!
#[test]
- fn file_handler_test_mode() {
+ fn confile_handler_test_mode() {
- let expected = "flogging::handlers::file_handler::tests->file_handler_test_mode [INFO ] trait methods
- flogging::handlers::file_handler::tests->file_handler_test_mode [WARNING] The sky is falling!\n"
- .to_string();
+ let re_str =
+ "^my_project::handlers::confile_handler::tests->confile_handler_test_mode \\[INFO ] trait methods
+ (?:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}\\+\\d{2}:\\d{2}) my_project::handlers::confile_handler::tests->confile_handler_test_mode \\[INFO ] trait methods
+ my_project::handlers::confile_handler::tests->confile_handler_test_mode \\[WARNING] The sky is falling!
+ (?:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}\\+\\d{2}:\\d{2}) my_project::handlers::confile_handler::tests->confile_handler_test_mode \\[WARNING] The sky is falling!
+ $";
+
+ let re = Regex::new(re_str).unwrap();
let mut log = Logger::builder(module_path!())
- .set_fn_name("file_handler_test_mode")
- .remove_file("test_logs/file_handler_test_mode.log")
- .add_file_handler_with(
- "test_logs/file_handler_test_mode.log",
- FormatType::Simple,
- None,
- )
+ .set_fn_name("confile_handler_test_mode")
+ .add_custom_handler(
+ "ConfileHandler",
+ Box::new(
+ // This file is never written to:
+ ConfileHandler::create("test_logs/confile_handler_test_mode.log").unwrap(),
+ ),
+ )
.build();
- let h = log.get_handler(crate::Handler::File).unwrap();
+ let h = log
+ .get_handler(crate::Handler::Custom("ConfileHandler".to_string()))
+ .unwrap();
+
+ // All log entries will be stored in the internal buffer.
h.set_test_mode(true);
assert!(h.is_open());
assert_eq!(
h.get_formatter().to_string(),
- "dt_fmt: \"\" - fmt_string: \"{mod_path}->{fn_name} [{level:7}] {message}\""
+ "dt_fmt: \"%+\" - fmt_string: \"{dt:35} {mod_path}->{fn_name} [{level:7}] {message}\""
.to_string()
);
log.info("trait methods");
log.warning("The sky is falling!");
- let h = log.get_handler(crate::Handler::File).unwrap();
+ let h = log
+ .get_handler(crate::Handler::Custom("ConfileHandler".to_string()))
+ .unwrap();
+
let buf = h.get_log();
- assert_eq!(expected, buf);
+ assert!(re.is_match(&buf));
h.flush();
h.close();
}
Test passes, but no other output.
#[test]
#[should_panic(expected = "'filename' must not be empty")]
fn filename_empty() {
- let _ = Logger::file_logger(module_path!(), "");
+ let _ = Logger::builder(module_path!())
+ .set_fn_name("confile_handler_test_mode")
+ .add_custom_handler(
+ "ConfileHandler",
+ Box::new(
+ ConfileHandler::create("").unwrap(),
+ ),
+ )
+ .build();
}
}
Test passes with output:
-
console
thread 'handlers::confile_handler::tests::filename_empty' panicked at src/handlers/confile_handler.rs:323:53: called `Result::unwrap()` on an `Err` value: Custom { kind: InvalidInput, error: "'filename' must not be empty" }
Final Code
Here is the complete source code for the custom formatter: ConfileHandler
.
//
// File Name: confile_handler.rs
// Directory: src/handlers
// Project Name: my_project
//
// Copyright (C) 2025 Bradley Willcott
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This library (crate) is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library (crate) is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this library (crate). If not, see <https://www.gnu.org/licenses/>.
//
//!
//! # ConfileHandler
//!
use std::{
fmt,
fs::{File, exists},
io::{Error, ErrorKind::InvalidInput, Write},
};
use flogging::*;
///
/// Publishes log entries to the file whose name was provided during
/// initialization.
///
#[derive(Debug, Default)]
pub struct ConfileHandler {
filename: String,
con_fmt: Formatter,
file_fmt: Formatter,
file: Option<File>,
writer: Option<Vec<u8>>,
}
impl ConfileHandler {
fn create(filename: &str) -> Result<Self, Error> {
if filename.is_empty() {
return Err(Error::new(InvalidInput, "'filename' must not be empty"));
}
let fh = ConfileHandler {
filename: filename.to_string(),
con_fmt: FormatType::Simple.create(None),
file_fmt: FormatType::Iso8601.create(None),
file: {
let f = File::options().append(true).create(true).open(filename)?;
Some(f)
},
writer: None,
};
Ok(fh)
}
fn log(&self) -> String {
if let Some(w) = self.writer.to_owned() {
String::from_utf8(w).unwrap()
} else {
String::new()
}
}
}
impl fmt::Display for ConfileHandler {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Console: {}\n{} : {}",
self.con_fmt, self.filename, self.file_fmt
)
}
}
impl HandlerTrait for ConfileHandler {
///
/// Create a new handler instance.
///
/// ## Parameters
/// - `name` - This the `filename` of the log file.
///
fn create(name: &str) -> Result<Self, Error> {
ConfileHandler::create(name)
}
///
/// Flushes and closes the file.\
/// Also, removes the internal buffer, if in `test_mode`.\
/// Will therefore, no longer be *in* `test_mode`.
///
fn close(&mut self) {
self.flush();
self.file = None;
}
fn flush(&mut self) {
if let Some(f) = &self.file {
f.sync_all().expect("sync_all() failed");
}
}
fn get_formatter(&self) -> Formatter {
self.file_fmt.clone()
}
fn get_log(&self) -> String {
self.log()
}
fn is_open(&self) -> bool {
self.file.is_some()
}
fn publish(&mut self, log_entry: &LogEntry) {
if self.is_open() {
let mut buf = self.file_fmt.format(log_entry);
buf.push('\n');
if let Some(w) = self.writer.as_mut() {
writeln!(w, "{}", self.con_fmt.format(log_entry)).expect("writeln!() failed");
writeln!(w, "{}", self.file_fmt.format(log_entry)).expect("writeln!() failed");
} else {
println!("{}", self.con_fmt.format(log_entry));
self.file.as_mut().unwrap().write_all(buf.as_bytes()).expect("writeln!() failed");
}
}
}
fn set_formatter(&mut self, formatter: Formatter) {
self.file_fmt = formatter;
}
///
/// Sets the test mode to `state`.
///
/// If set to `true`, use `get_log()` to obtain the
/// log.
///
fn set_test_mode(&mut self, state: bool) {
if state {
// true
self.writer = Some(Vec::new());
} else {
self.writer = None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
use regex::Regex;
#[test]
fn confile_handler() {
let mut log = Logger::custom_logger(
module_path!(),
"ConfileHandler",
Box::new(ConfileHandler::create("test_logs/confile_handler.log").unwrap()),
);
log.set_fn_name("confile_handler");
let h = log
.get_handler(Handler::Custom("ConfileHandler".to_string()))
.unwrap();
h.set_test_mode(false);
assert!(h.is_open());
assert_eq!(
h.get_formatter().to_string(),
"dt_fmt: \"%+\" - fmt_string: \"{dt:35} {mod_path}->{fn_name} [{level:7}] {message}\""
.to_string()
);
log.info("trait methods");
log.warning("The sky is falling!");
let handler = log
.get_handler(Handler::Custom("ConfileHandler".to_string()))
.unwrap();
assert_eq!(handler.get_log(), "".to_string());
handler.flush();
handler.close();
log.exiting_with("This should get thrown away.");
}
#[test]
fn confile_handler_file_test() {
let re_str =
"^(?:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}\\+\\d{2}:\\d{2}) my_project::handlers::confile_handler::tests->confile_handler_file_test \\[INFO ] trait methods
(?:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}\\+\\d{2}:\\d{2}) my_project::handlers::confile_handler::tests->confile_handler_file_test \\[WARNING] The sky is falling!
$";
let re = Regex::new(re_str).unwrap();
let mut log = Logger::builder(module_path!())
.set_fn_name("confile_handler_file_test")
.remove_file("test_logs/confile_handler_file_test.log")
.add_custom_handler(
"ConfileHandler",
Box::new(
ConfileHandler::create("test_logs/confile_handler_file_test.log").unwrap(),
),
)
.build();
let h = log
.get_handler(Handler::Custom("ConfileHandler".to_string()))
.unwrap();
h.set_test_mode(false);
assert!(h.is_open());
assert_eq!(
h.get_formatter().to_string(),
"dt_fmt: \"%+\" - fmt_string: \"{dt:35} {mod_path}->{fn_name} [{level:7}] {message}\""
.to_string()
);
log.info("trait methods");
log.warning("The sky is falling!");
let h = log
.get_handler(Handler::Custom("ConfileHandler".to_string()))
.unwrap();
assert_eq!(h.get_log(), "".to_string());
h.flush();
h.close();
assert!(!h.is_open());
log.severe("This should get thrown away.");
if let Ok(mut file) = File::open("test_logs/confile_handler_file_test.log") {
let mut buf = String::new();
if let Ok(_count) = file.read_to_string(&mut buf) {
assert!(re.is_match(&buf));
}
}
}
#[test]
fn confile_handler_test_mode() {
let re_str =
"^my_project::handlers::confile_handler::tests->confile_handler_test_mode \\[INFO ] trait methods
(?:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}\\+\\d{2}:\\d{2}) my_project::handlers::confile_handler::tests->confile_handler_test_mode \\[INFO ] trait methods
my_project::handlers::confile_handler::tests->confile_handler_test_mode \\[WARNING] The sky is falling!
(?:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}\\+\\d{2}:\\d{2}) my_project::handlers::confile_handler::tests->confile_handler_test_mode \\[WARNING] The sky is falling!
$";
let re = Regex::new(re_str).unwrap();
let mut log = Logger::builder(module_path!())
.set_fn_name("confile_handler_test_mode")
.add_custom_handler(
"ConfileHandler",
Box::new(
// This file is never written to:
ConfileHandler::create("test_logs/confile_handler_test_mode.log").unwrap(),
),
)
.build();
let h = log
.get_handler(Handler::Custom("ConfileHandler".to_string()))
.unwrap();
// All log entries will be stored in the internal buffer.
h.set_test_mode(true);
assert!(h.is_open());
assert_eq!(
h.get_formatter().to_string(),
"dt_fmt: \"%+\" - fmt_string: \"{dt:35} {mod_path}->{fn_name} [{level:7}] {message}\""
.to_string()
);
log.info("trait methods");
log.warning("The sky is falling!");
let h = log
.get_handler(Handler::Custom("ConfileHandler".to_string()))
.unwrap();
let buf = h.get_log();
assert!(re.is_match(&buf));
h.flush();
h.close();
}
#[test]
#[should_panic(expected = "'filename' must not be empty")]
fn filename_empty() {
let _ = Logger::builder(module_path!())
.set_fn_name("confile_handler_test_mode")
.add_custom_handler(
"ConfileHandler",
Box::new(
ConfileHandler::create("").unwrap(),
),
)
.build();
}
}
Wrap Up
Right … Now we are done. We have our (hopefully) working custom handler, that can be used in our project.
Of course, I expect that your requirements will be more complex.
However, by following this reasonably straightforward process, you should not have too many challenges in getting your custom handler working.
Remember, it’s in the publish()
method that all the real work is done. The rest is just setup and support.
Custom Formatters
A custom formatter will allow you to be very specific with the layout of the output for each log entry when it is logged via the log handlers. Though the built-in formatters provide the same options as that provided by the Java library, I expect you may have more specific requirements.
Step 1
Why?
What is the goal, here. What are you trying to achieve?
For our example, we will work towards creating a custom formatter for outputting a csv style format.
How?
What is the specific layout that is required?
Our example will aim to provide:
<date-time>,<module path>-><function name>,<level>,"<message>"
Step 2
Creating a custom formatter will require some investigation of other crates.
Crates that are used by FLogging
:
- crono - Used for the date/time functionality, with its specifiers, in
- the
dt_fmt
string of each formatter. - std::fmt - The format options used in the
fmt_string
string of each formatter. - strfmt - Used to do the actual formatting.
Now you should check out the built-in formatters to find one that is the closest to what you are after.
For our example, none of the built-in formatters are really closer than the other, so we will
choose the UnixTimestampFormatter
, simply because I am running Linux as my O/S.
To find them, use either the API documentation, or the Github repository: flogging.
As with the Custom Handlers, to obtain the code from the API documentation, at the top navigation bar, click “flogging-X.X.X”, where “X.X.X” is the version number, then under “LINKS” click “Source”. This will bring up the “Source” tab. Now we need to navigate to the required file.
- src
- handlers
- formatter
- unixtimestamp_formatter.rs
- formatter
- handlers
Now select ALL of the code, from the top down, then ‘copy [ctrl/c]’. You need to include the file header (Copyright).
In your project src directory somewhere, create your new formatter file, and paste this code into it.
For this example, (we’ll name it
my_project
) we’ll have the following basic
layout:
- src/
lib.rs
main.rs- handlers/
confile_handler.rs
mod.rs- formatters/
csv_formatter.rs
- formatters/
- handlers/
- test_logs/
Our file will be called: csv_formatter.rs
, with the module: CsvFormatter
.
First things first. We now need to do some changes:
unixtimestamp_formatter.rs
tocsv_formatter.rs
UnixTimestampFormatter
toCsvFormatter
use crate::FormatTrait;
touse flogging::*;
&crate::LogEntry
to&LogEntry
I have used a form of ‘diff’ to represent the changes:
- ’- ’ old line code
- ’+ ’ new line code
//
- // File Name: unixtimestamp_formatter.rs
+ // File Name: csv_formatter.rs
// Directory: src/handlers/formatters
- // Project Name: flogging
+ // Project Name: my_project
//
// Copyright (C) 2025 Bradley Willcott
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This library (crate) is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library (crate) is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this library (crate). If not, see <https://www.gnu.org/licenses/>.
//
//!
- //! # UnixTimeStamp Formatter
+ //! # CSV Formatter
//!
use std::fmt;
- use crate::FormatTrait;
+ use flogging::*;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
///
- /// Unix Timestamp format.
+ /// CSV format.
///
/// The first part (before the decimal point) is
/// the number of seconds since 1970-01-01 00:00 UTC.
///
/// The second part is the number of nanoseconds since
/// the last whole second.
///
/// Example:
/// ```text
/// 1752817859.157970496
/// ```
/// Template:
/// - `dt` in the template would be the datetime string, similar to the above.
/// - `mod_path`, `fn_name`, `level`, and `message` all come out of the `LogEntry`
- /// provided to the [`format()`][UnixTimestampFormatter::format] method.
+ /// provided to the [`format()`][CsvFormatter::format] method.
///
/// ```ignore
/// format!("{dt} {mod_path}->{fn_name} [{level:7}] {message}");
/// ```
/// Sample output:
/// ```text
/// 1752818461.051538870 flogging->main [SEVERE ] Hurricanes are windy!
/// ```
///
- pub struct UnixTimestampFormatter {
+ pub struct CsvFormatter {
dt_fmt: String,
fmt_string: String,
}
- impl UnixTimestampFormatter {
+ impl CsvFormatter {
///
- /// Creates a new instance of `UnixTimestampFormatter`.
+ /// Creates a new instance of `CsvFormatter`.
///
pub fn new() -> Self {
Self {
dt_fmt: "%s.%f".to_string(),
fmt_string: "{dt} {mod_path}->{fn_name} [{level:7}] {message}".to_string(),
}
}
///
/// Returns the date/time format string.
///
pub fn dt_fmt(&self) -> String {
self.dt_fmt.clone()
}
///
/// Returns the primary format string.
///
pub fn fmt_string(&self) -> String {
self.fmt_string.clone()
}
}
- impl Default for UnixTimestampFormatter {
+ impl Default for CsvFormatter {
fn default() -> Self {
Self::new()
}
}
- impl fmt::Display for UnixTimestampFormatter {
+ impl fmt::Display for CsvFormatter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"dt_fmt: \"{}\" - fmt_string: \"{}\"",
self.dt_fmt, self.fmt_string
)
}
}
- impl FormatTrait for UnixTimestampFormatter {
+ impl FormatTrait for CsvFormatter {
- fn format(&self, log_entry: &crate::LogEntry) -> String {
+ fn format(&self, log_entry: &LogEntry) -> String {
self.ft_fmt(self.dt_fmt(), self.fmt_string(), log_entry)
}
}
Step 3
At this point your project should still build without errors.
Next, let’s put together the datetime format string: dt_fmt
.
After looking into the crono
crates specifiers, we come up with this string:
"%Y-%m-%d %H:%M:%S%.6f"
which should produce something like:
2025-06-23 13:10:45.123456
Now, let’s build the main format string: fmt_string
.
First, look at the provided method: FormatTrait::ft_fmt()
. It provides the following variables that can be included in your format string via interpolation:
dt
- The datetime formatted with:dt_fmt
.mod_path
- The module path, possibly supplied via:module_path!()
.fn_name
- The name of the function/method inside which the log entry was generated. Supplied by the#[logger]
macro, or manually with theset_fn_name()
method.level
- The logLevel
for which the entry was created.message
- The text of the log entry.
Using the specifiers available in std:fmt
, we produce this:
"{dt},{mod_path}->{fn_name},{level},\"{message}\""
with a possible output of:
2025-06-23 13:10:45.123456,my_project::csv_formatter::tests->csv_format,INFO,"trait methods"
Using these format strings, we can now modify our new custom formatter as follows:
impl CsvFormatter {
///
/// Creates a new instance of `CsvFormatter`.
///
pub fn new() -> Self {
Self {
- dt_fmt: "%s.%f".to_string(),
+ dt_fmt: "%Y-%m-%d %H:%M:%S%.6f".to_string(),
- fmt_string: "{dt} {mod_path}->{fn_name} [{level:7}] {message}".to_string(),
+ fmt_string: "{dt},{mod_path}->{fn_name},{level},\"{message}\"".to_string(),
}
}
Step 4
Let’s add some testing:
#[cfg(test)]
mod tests{
use super::*;
use regex::Regex;
const_logger!({
Logger::builder(module_path!())
.add_string_handler_with(
FormatType::Custom,
Some(Box::new(CsvFormatter::new())),
)
.build()
});
#[test]
#[logger]
fn csv_format() {
entering!();
let re_str =
"^(?:\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}),my_project::handlers::formatters::csv_formatter::tests->csv_format,INFO,\"Testing a new custom formatter.\"
(?:\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}),my_project::handlers::formatters::csv_formatter::tests->csv_format,WARNING,\"Must add more testing.\"
$";
let re = Regex::new(re_str).unwrap();
info!("Testing a new custom formatter.");
warning!("Must add more testing.");
let log_str = get_handler!(Handler::String).unwrap().get_log();
println!("{log_str}");
assert!(re.is_match(&log_str));
}
}
Possible output:
---- handlers::formatters::csv_formatter::tests::csv_format stdout ----
2025-08-27 12:15:48.853979,my_project::handlers::formatters::csv_formatter::tests->csv_format,INFO,"Testing a new custom formatter."
2025-08-27 12:15:48.854052,my_project::handlers::formatters::csv_formatter::tests->csv_format,WARNING,"Must add more testing."
Now let’s do a final fixup of the API comments.
///
/// CSV format.
///
- /// The first part (before the decimal point) is
- /// the number of seconds since 1970-01-01 00:00 UTC.
- ///
- /// The second part is the number of nanoseconds since
- /// the last whole second.
+ /// The datetime format string is:
+ ///
+ /// ```text
+ /// "%Y-%m-%d %H:%M:%S%.6f"
+ /// ```
///
/// Example:
/// ```text
- /// 1752817859.157970496
+ /// 2025-08-21 19:15:04.089061
/// ```
/// Template:
/// - `dt` in the template would be the datetime string, similar to the above.
/// - `mod_path`, `fn_name`, `level`, and `message` all come out of the `LogEntry`
/// provided to the [`format()`][CsvFormatter::format] method.
///
/// ```ignore
- /// format!("{dt} {mod_path}->{fn_name} [{level:7}] {message}");
+ /// format!("{dt},{mod_path}->{fn_name},{level},\"{message}\"");
/// ```
/// Sample output:
/// ```text
- /// 1752818461.051538870 flogging->main [SEVERE ] Hurricanes are windy!
+ /// 2025-08-21 19:26:47.801287,my_project::handlers::formatters::csv_formatter::tests->csv_format,SEVERE,"Hurricanes are windy!"
/// ```
///
pub struct CsvFormatter {
dt_fmt: String,
fmt_string: String,
}
Final Code
Here is the complete source code for the custom formatter: CsvFormatter
.
//
// File Name: csv_formatter.rs
// Directory: src/handlers/formatters
// Project Name: my_project
//
// Copyright (C) 2025 Bradley Willcott
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This library (crate) is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library (crate) is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this library (crate). If not, see <https://www.gnu.org/licenses/>.
//
//!
//! # CSV Formatter
//!
use std::fmt;
use flogging::*;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
///
/// CSV format.
///
/// The datetime format string is:
///
/// ```text
/// "%Y-%m-%d %H:%M:%S%.6f"
/// ```
///
/// Example:
/// ```text
/// 2025-08-21 19:15:04.089061
/// ```
/// Template:
/// - `dt` in the template would be the datetime string, similar to the above.
/// - `mod_path`, `fn_name`, `level`, and `message` all come out of the `LogEntry`
/// provided to the [`format()`][CsvFormatter::format] method.
///
/// ```ignore
/// format!("{dt},{mod_path}->{fn_name},{level},\"{message}\"");
/// ```
/// Sample output:
/// ```text
/// 2025-08-21 19:26:47.801287,my_project::handlers::formatters::csv_formatter::tests->csv_format,SEVERE,"Hurricanes are windy!"
/// ```
///
pub struct CsvFormatter {
dt_fmt: String,
fmt_string: String,
}
impl CsvFormatter {
///
/// Creates a new instance of `CsvFormatter`.
///
pub fn new() -> Self {
Self {
dt_fmt: "%Y-%m-%d %H:%M:%S%.6f".to_string(),
fmt_string: "{dt},{mod_path}->{fn_name},{level},\"{message}\"".to_string(),
}
}
///
/// Returns the date/time format string.
///
pub fn dt_fmt(&self) -> String {
self.dt_fmt.clone()
}
///
/// Returns the primary format string.
///
pub fn fmt_string(&self) -> String {
self.fmt_string.clone()
}
}
impl Default for CsvFormatter {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for CsvFormatter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"dt_fmt: \"{}\" - fmt_string: \"{}\"",
self.dt_fmt, self.fmt_string
)
}
}
impl FormatTrait for CsvFormatter {
fn format(&self, log_entry: &LogEntry) -> String {
self.ft_fmt(self.dt_fmt(), self.fmt_string(), log_entry)
}
}
#[cfg(test)]
mod tests{
use super::*;
use regex::Regex;
const_logger!({
Logger::builder(module_path!())
.add_string_handler_with(
FormatType::Custom,
Some(Box::new(CsvFormatter::new())),
)
.build()
});
#[test]
#[logger]
fn csv_format() {
entering!();
let re_str =
"^(?:\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}),my_project::handlers::formatters::csv_formatter::tests->csv_format,INFO,\"Testing a new custom formatter.\"
(?:\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}),my_project::handlers::formatters::csv_formatter::tests->csv_format,WARNING,\"Must add more testing.\"
$";
let re = Regex::new(re_str).unwrap();
info!("Testing a new custom formatter.");
warning!("Must add more testing.");
let log_str = get_handler!(Handler::String).unwrap().get_log();
println!("{log_str}");
assert!(re.is_match(&log_str));
}
}
Wrap Up
Alright! We are done.
Hopefully, by following all these steps, your own custom formatter should build and perform its duty correctly.
In developing the example formatter, I found out that the crono
specifiers are very specific. Atleast %.3f
, etc. I tried to use: %.4f
and it did not like it. Since 3
was too small, I had to go for 6
.
Integration
Now that we have our custom handler and formatter, let’s use them.
For those interested, I have uploaded the whole example project to:
https://github.com/bewillcott/my_project/tree/Custom.
Here is the sample main.rs:
//
// File Name: main.rs
// Directory: src
// Project Name: my_project
//
// Copyright (C) 2025 Bradley Willcott
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This library (crate) is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library (crate) is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this library (crate). If not, see <https://www.gnu.org/licenses/>.
//
use my_project::*;
const_logger!({
Logger::builder(module_path!())
.remove_file("test_logs/debug.log")
.add_custom_handler_with(
"ConfileHandler",
Box::new(ConfileHandler::create("test_logs/debug.log").unwrap()),
FormatType::Custom,
Some(Box::new(CsvFormatter::new())),
)
.set_level(Level::ALL)
.build()
});
#[logger]
fn main() {
entering!();
config!("Operating system: Fedora Linux");
config!("Version: 42");
info!("This is a test of the integration of the 'FLogging crate' and the custom handler and formatter.");
println!("*** My Project ***");
exiting!();
}
Sample output:
-
console
my_project->main [FINER ] Entry my_project->main [CONFIG ] Operating system: Fedora Linux my_project->main [CONFIG ] Version: 42 my_project->main [INFO ] This is a test of the integration of the 'FLogging crate' and the custom handler and formatter. *** My Project *** my_project->main [FINER ] Return
-
test_logs/debug.log
2025-08-27 12:22:28.291102,my_project->main,FINER,"Entry" 2025-08-27 12:22:28.291180,my_project->main,CONFIG,"Operating system: Fedora Linux" 2025-08-27 12:22:28.291205,my_project->main,CONFIG,"Version: 42" 2025-08-27 12:22:28.291226,my_project->main,INFO,"This is a test of the integration of the 'FLogging crate' and the custom handler and formatter." 2025-08-27 12:22:28.291255,my_project->main,FINER,"Return"
Now let’s see what we get when we turn OFF
logging:
const_logger!({
Logger::builder(module_path!())
.remove_file("test_logs/debug.log")
.add_custom_handler_with(
"ConfileHandler",
Box::new(ConfileHandler::create("test_logs/debug.log").unwrap()),
FormatType::Custom,
Some(Box::new(CsvFormatter::new())),
)
- .set_level(Level::ALL)
+ .set_level(Level::OFF)
.build()
});
Sample output:
-
console
*** My Project ***
-
test_logs/debug.log
Expand description
§FLogging
The primary purpose of logging, is to facilitate fault diagnosis through the provision of specific information as, when, and from where, it is needed. This could be during development, testing, or even during production runs.
There is a new tutorial guide: The FLogging Guide.
§Setting up
You need to add this crate to your project:
$ cargo add flogging
or add this text to the projects Cargo.toml
file:
[dependencies]
flogging = "0.6.0"
§** Warning **
Before proceeding, please read the README.md file.
§Features
- Levels - There are nine (9) levels of message logging, with two (2) special ones.
- Choice - You can use either macros, methods, or a mix of both.
- Built-in options - A range of handlers and formatters.
- Customization - You can create your own handlers and/or formatters.
§Choice
§Macros
This crate has very easy to use macros. By using them, you remove a lot of the complexity from the process. Thus making it both simpler and less code cluttering, to use.
- There is one macro (
const_logger!()
) that is used to setup a single module/file for logging. - There is one macro (
#[logger]
) that is applied as an attribute to each function/method that you need to create log entries within. - There are nine macros that are used to actually log the messages:
- There are two helper macros:
Check out the Examples below, or The FLogging Guide, for how easy it is to get started.
§Special Note
For the macros that accept the parameter: msg
, the following is true:
- They accept parameters the same as for
std::format!
- plain text
&str
:("It's your time.")
- format
&str
with interpolated variables:("Var: {var}")
- format
&str
with supporting parameters:("Var: {}", var)
- Combination of the last two:
("Vars {var1} - {}:{}", var2, var3)
- plain text
- Additional Feature
- Just one or more variables without a supplied format string:
(var1, var2, var3)
- In this case, a default format string will be used:
"{}, {}, {}"
- The number of
"{}"
will depend on the number of parameters. - Ideal for logging concrete instances that have very good
Display
implementations, or you just need their data without further explanation.
- Just one or more variables without a supplied format string:
- Special Cases
§Methods
Now for the coding geeks! Yes I didn’t forget you lot.
Though the macros are the easiest and simplest way to use this crate, those macros are just candy
coating over the real workers, the methods. There are two main mods/structs in this crate, Logger
and LoggerBuilder
.
§Logger
Logger
is the work-horse of the crate. It has all the methods for initializing each function/method
for logging, and all of the message logging methods.
Using the “methods” option is more complex, as-in, you have to write a lot more code, and manage it.
To see how much more is involved, check-out the Logger
’s methods. There are
plenty of examples throughout.
builder()
config()
console_logger()
econsole_logger()
custom_logger()
entering()
entering_with()
exiting()
exiting_with()
file_logger()
fine()
finer()
finest()
fn_name()
get_handler()
has_handler()
info()
level()
set_fn_name()
set_level()
severe()
string_logger()
warning()
§LoggerBuilder
LoggerBuilder
is used by Logger
to provide various configuration options for setting up your logger.
The available options/methods are:
add_console_handler()
add_console_handler_with()
add_econsole_handler()
add_econsole_handler_with()
add_console_handler()
add_console_handler_with()
add_custom_handler()
add_custom_handler_with()
add_file_handler()
add_file_handler_with()
add_string_handler()
add_string_handler_with()
remove_file()
set_fn_name()
set_level()
And to finish:
These options/methods allow you a lot of flexibility in how you configure your logger. As you will typically
have a different logger for each mod/file, you have a lot of control over what is logged, how it is formatted,
and where it is stored/viewed. With the set_level()
method, you can control
this on a mod/file basis. Logging each mod/file differently, or even turning logging off when you no-longer require it.
Check out The FLogging Guide for examples and further help.
Note
As of version (0.4.0), you can only set the logging level for the logger. All handlers process every log entry that the logger accepts, based on the logger’s current log level setting. This may change in a future version, allowing each handler to have its own logging level.
§Built-in options
I have included a number of handlers to get you started:
There are also a number of formatters as well:
§Customization
Now for the fun part - “Doing it your way!!!”
Though I have provided some “standard” handlers and formatters, not everyone, or every project, will want to use them. I expect there will be a need for:
- sending log entries to remote system log servers,
- sending log entries to another program (local or remote) for live analysis, or some other processing,
- storing log entries in a specific file format (xml, json, csv),
- storing log entries in a database.
And I’m sure you’ll come-up with more requirements at some time in the future. So, you have the option to create your own custom handlers and custom formatters. Mixing them up with the built-in ones as you need to.
OK now, how do you do it. Well this is going to require some work on your part.
For a more in-depth tutorial refer to The FLogging Guide.
§Custom Handler
To create a custom handler, I would suggest looking at the source code for the built-in ones, and copying the code from the one that is closest to your requirements. Make sure that you rename as appropriate! Then make the necessary changes, adding in your own code, to get it doing what you need.
When you are ready to try-out your new custom handler, check-out these methods:
Logger::custom_logger()
LoggerBuilder::add_custom_handler()
LoggerBuilder::add_custom_handler_with()
§Custom Formatter
Now for the custom formatter. This may require a bit more investigation on your part, as to the actual formatting options that are available.
Firstly, this crate uses crono for the date/time functionality. Check
out the available specifiers. You will need to use the formatting options from this crate for the dt_fmt
string, of your custom
formatter.
Secondly, the fmt_string
uses the format options available in accordance with std::fmt. Though I am
actually using the strfmt crate to do the formatting, because it does
not require a ‘static’ string like format!()
.
Again, check-out the built-in formatters, and copy the code from the one that is closest to your
requirements. As before, renaming as necessary! Also, check-out the trait: FormatTrait
.
You will need to implement it for your custom formatter, as you will notice when you look at the built-in formatters.
Also, you will find that the ‘provided method’, ft_fmt()
, provides certain variables
that you can include, via interpolation, in your fmt_string
.
Once you have got your custom formatter set up, you can then use it with:
LoggerBuilder::add_console_handler_with()
LoggerBuilder::add_econsole_handler_with()
LoggerBuilder::add_pconsole_handler_with()
LoggerBuilder::add_custom_handler_with()
LoggerBuilder::add_file_handler_with()
LoggerBuilder::add_string_handler_with()
§Examples
This example demonstrates the use of the macros. The reason I am demoing the macros, is that I expect most people will want to use them, instead of the methods, for ease of use.
Let’s see what is required:
- At the module/file level:
use flogging::*;
const_logger!({...});
=>
- On each function/method you want to add logging to:
#[logger]
=>
- Inside each such attributed function/method:
- Any of the logging macros
use flogging::*;
use std::{error::Error, result::Result};
// Setting up the module level logger.
const_logger!({
Logger::builder(module_path!())
.add_console_handler()
.add_file_handler(“test_logs/debug.log”)
.set_level(Level::FINEST)
.build()
});
#[logger]
fn do_something() {
entering!();
<span class="comment">// do some work worth noting
</span><span class="kw">let </span>result = <span class="string">"Just something to log."</span>;
<span class="macro">info!</span>(<span class="string">"Did some work here.\n {result}"</span>);
<span class="comment">// ...
</span><span class="macro">fine!</span>(<span class="string">"Bit more detail."</span>);
<span class="kw">if let </span><span class="prelude-val">Err</span>(e) = error_prone() {
<span class="macro">warning!</span>(<span class="string">"Error: {}"</span>, e);
}
<span class="macro">exiting!</span>();
}
#[logger]
fn error_prone() -> Result<(), Box<dyn Error>> {
entering!();
let rtn = Err(Box::from(“Bad day!”));
exiting!();
rtn
}
#[logger]
fn main() {
entering!();
info!(“All logging macros accept the same parameters as std::format!(...)
”);
warning!(“Those same macros (info, etc.) MUST have atleast one parameter.”);
config!(“This is running on Fedora Linux 42.”);
do_something();
info!(“Job’s done.”);
exiting!();
}
Output:
flogging->main [FINER ] Entry
flogging->main [INFO ] All logging macros accept the same parameters as `std::format!(...)`
flogging->main [WARNING] Those same macros (info, etc.) MUST have atleast one parameter.
flogging->main [CONFIG ] This is running on Fedora Linux 42.
flogging->do_something [FINER ] Entry
flogging->do_something [INFO ] Did some work here.
Just something to log.
flogging->do_something [FINE ] Bit more detail.
flogging->error_prone [FINER ] Entry
flogging->error_prone [FINER ] Return
flogging->do_something [WARNING] Error: Bad day!
flogging->do_something [FINER ] Return
flogging->main [INFO ] Job's done.
flogging->main [FINER ] Return
Macros§
- config
- Log a CONFIG message.
- const_
logger - Setup module level logger access.
- entering
- Log entry into a function/method.
- exiting
- Log return from a function/method.
- fine
- Log a FINE message.
- finer
- Log a FINER message.
- finest
- Log a FINEST message.
- get_
handler - Get the required
Handler
. - info
- Log an INFO message.
- is_
logging - Checks whether or not this logger is processing log requests.
- set_
level - Set the logging level for this
Logger
instance. - severe
- Log a SEVERE message.
- warning
- Log a WARNING message.
Structs§
- Console
Handler - Publishes log entries to the console.
- Console
Type Error - Returned from
FromStr::from_str()
when an unknown string is passed-in. - Console
Type Iter - An iterator over the variants of ConsoleType
- File
Handler - Publishes log entries to the file whose name was provided during initialization.
- Iso8601
Formatter - ISO 8601 / RFC 3339 date & time format.
- LogEntry
- Used to provide relevant information about each log entry.
- Logger
- This is the work-horse, providing the primary methods of the crate.
- Logger
Builder - Used by
Logger
to provide more flexibility in the configuration of the final logger. - Mock
Formatter - Mock Formatter.
- Mock
Handler - This is used as a fake or mock handler.
- Simple
Formatter - Simple format.
- String
Handler - Publishes log entries to an internal list.
- Unix
Timestamp Formatter - Unix Timestamp format.
Enums§
- Console
Type ConsoleType
configures theConsoleHandler
’s output.- Format
Type - Used as a simple way to obtain the various
Formatter
s. - Formatter
- Provides wrappers for holding each type of formatter.
- Handler
- Available handlers.
- Level
- Log entry level setting.
Traits§
- Format
Trait - Provides methods for formatting
LogEntry
s. - Handler
Trait - Provides common methods required for all handlers.
Attribute Macros§
- logger
- Provides for logging within the attributed function/method.
<!doctype html>
Coverage Report
Created at 2025-08-27 22:00
Filename | Line Coverage 99.85 % | Function Coverage 100.00 % | Region Coverage 99.62 % | ||||||
---|---|---|---|---|---|---|---|---|---|
flogging_macros/src/format.rs | 100.00 % | 38 / 38 | 100.00 % | 2 / 2 | 100.00 % | 64 / 64 | |||
flogging_macros/src/lib.rs | 100.00 % | 47 / 47 | 100.00 % | 13 / 13 | 100.00 % | 74 / 74 | |||
flogging_macros/src/logger.rs | 100.00 % | 14 / 14 | 100.00 % | 1 / 1 | 100.00 % | 20 / 20 | |||
src/handlers/console_handler/console_type.rs | 100.00 % | 36 / 36 | 100.00 % | 6 / 6 | 100.00 % | 57 / 57 | |||
src/handlers/console_handler.rs | 100.00 % | 185 / 185 | 100.00 % | 20 / 20 | 99.43 % | 348 / 350 | |||
src/handlers/file_handler.rs | 98.66 % | 147 / 149 | 100.00 % | 16 / 16 | 98.50 % | 262 / 266 | |||
src/handlers/formatters/format_trait.rs | 100.00 % | 28 / 28 | 100.00 % | 4 / 4 | 100.00 % | 58 / 58 | |||
src/handlers/formatters/format_type.rs | 100.00 % | 36 / 36 | 100.00 % | 3 / 3 | 100.00 % | 63 / 63 | |||
src/handlers/formatters/formatter.rs | 100.00 % | 20 / 20 | 100.00 % | 4 / 4 | 100.00 % | 46 / 46 | |||
src/handlers/formatters/iso8601_formatter.rs | 100.00 % | 23 / 23 | 100.00 % | 6 / 6 | 100.00 % | 30 / 30 | |||
src/handlers/formatters/mock_formatter.rs | 100.00 % | 22 / 22 | 100.00 % | 6 / 6 | 100.00 % | 28 / 28 | |||
src/handlers/formatters/mod.rs | 100.00 % | 65 / 65 | 100.00 % | 4 / 4 | 100.00 % | 146 / 146 | |||
src/handlers/formatters/simple_formatter.rs | 100.00 % | 23 / 23 | 100.00 % | 6 / 6 | 100.00 % | 30 / 30 | |||
src/handlers/formatters/unixtimestamp_formatter.rs | 100.00 % | 23 / 23 | 100.00 % | 6 / 6 | 100.00 % | 30 / 30 | |||
src/handlers/handler.rs | 100.00 % | 24 / 24 | 100.00 % | 2 / 2 | 100.00 % | 58 / 58 | |||
src/handlers/mock_handler.rs | 100.00 % | 45 / 45 | 100.00 % | 11 / 11 | 100.00 % | 80 / 80 | |||
src/handlers/string_handler.rs | 100.00 % | 63 / 63 | 100.00 % | 13 / 13 | 100.00 % | 104 / 104 | |||
src/logger/builder.rs | 100.00 % | 295 / 295 | 100.00 % | 32 / 32 | 99.36 % | 467 / 470 | |||
src/logger/level.rs | 100.00 % | 43 / 43 | 100.00 % | 5 / 5 | 100.00 % | 64 / 64 | |||
src/logger/log_entry.rs | 100.00 % | 48 / 48 | 100.00 % | 10 / 10 | 100.00 % | 81 / 81 | |||
src/logger/mod.rs | 100.00 % | 116 / 116 | 100.00 % | 30 / 30 | 100.00 % | 202 / 202 | |||
src/logger/tests.rs | 100.00 % | 21 / 21 | 100.00 % | 3 / 3 | 100.00 % | 54 / 54 | |||
src/macros.rs | 100.00 % | 1 / 1 | 100.00 % | 1 / 1 | 100.00 % | 1 / 1 |