Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 of DEBUG_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 of DEBUG_LEVEL, and add_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!() and exiting()),
  • 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 like format!().

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:

  1. println!("{}", self.con_fmt.format(log_entry));

  2. 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

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
  • 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 to confile_handler.rs
  • FileHandler to ConfileHandler
  • use crate::*; to use 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

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
  • 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 to csv_formatter.rs
  • UnixTimestampFormatter to CsvFormatter
  • use crate::FormatTrait; to use 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 the set_fn_name() method.
  • level - The log Level 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

    
    
flogging - Rust
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.

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)
  • 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.
  • Special Cases
    • entering! and exiting!
    • These two macros have the same features as the others, but they may also be used without any parameters. In such a case, their defaults will be used.
§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.

§LoggerBuilder

LoggerBuilder is used by Logger to provide various configuration options for setting up your logger. The available options/methods are:

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:

§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:

§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:

  1. At the module/file level:
    • use flogging::*;
    • const_logger!({...});=>
  2. On each function/method you want to add logging to:
    • #[logger]=>
  3. Inside each such attributed function/method:
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§

ConsoleHandler
Publishes log entries to the console.
ConsoleTypeError
Returned from FromStr::from_str() when an unknown string is passed-in.
ConsoleTypeIter
An iterator over the variants of ConsoleType
FileHandler
Publishes log entries to the file whose name was provided during initialization.
Iso8601Formatter
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.
LoggerBuilder
Used by Logger to provide more flexibility in the configuration of the final logger.
MockFormatter
Mock Formatter.
MockHandler
This is used as a fake or mock handler.
SimpleFormatter
Simple format.
StringHandler
Publishes log entries to an internal list.
UnixTimestampFormatter
Unix Timestamp format.

Enums§

ConsoleType
ConsoleType configures the ConsoleHandler’s output.
FormatType
Used as a simple way to obtain the various Formatters.
Formatter
Provides wrappers for holding each type of formatter.
Handler
Available handlers.
Level
Log entry level setting.

Traits§

FormatTrait
Provides methods for formatting LogEntrys.
HandlerTrait
Provides common methods required for all handlers.

Attribute Macros§

logger
Provides for logging within the attributed function/method.

<!doctype html>Coverage Report

Coverage Report

Created at 2025-08-27 22:00

FilenameLine 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

Release Log

Version 0.6.0 * - 2025-08-27


Important Notes:

  • Removed

    impl Handler{
        fn new(){...}
        fn create(name){...}
    }
  • Removed

    Logger::reset_level()

Both of the above APIs are redundant. So removing them now, before v1.0.0 is released, is best.

  • Added a new method to HandlerTrait - set_test_mode(). This will require updating of any custom handlers.

Great News

Initial release of the online instructional guide: The FLogging Guide.


Added new methods to LoggerBuilder:

  • remove_file()
    • Use to remove a log file before adding a file handler. This is a way of resetting the log file prior to each test run.
  • add_pconsole_handler() and add_pconsole_handler_with()
    • Use to add a production version of the console handler. This handler is different, in that log entries set to LeveL::INFO, will have their msg printed to stdout without any formatting, whilst all other Levels will be printed to stderr using the set formatter.
  • set_fn_name()
    • Set the current function/method name. Only required when using the method form of operation instead of the macro form.

Added new method to Logger - pconsole_logger(). Added new associative function to Logger - remove_file().

To facilitate the pconcole_handlers, a new enum ConsoleType has been added.

Extensive work done on improving both unit and integration tests.

Version 0.5.0 * - 2025-08-03

Details (click to see)

Important Note:

Only applicable for those who have previously created their own custom formatter - FormatType::Custom(String) changed to FormatType::Custom.


Now generally, there were some improvements to the API documentation.

Version 0.4.1 * - 2025-07-29

Details (click to see)
  • Major improvements to the API documentation.
  • Increased test coverage to 100%, and included the Coverage Report.

Version 0.4.0 * - 2025-07-27

Details (click to see)

This is the initial release.

The reason for not being (0.1.0), is the way I track the internal development of projects not yet published. However, now that this one is published, the versioning will progress as expected, in accordance with Semantic Versioning.

Changelog Common Changelog

Version 0.6.0 * - 2025-08-27

  • Initial release of the online Guide: “The FLogging Guide”. ( ebe97fc)

Changed

  • Breaking: Minor change to the Iso8601Formatter::fmt_string. ( 16a7e87)
  • Breaking: Minor change to the SimpleFormatter::fmt_string and UnixTimestampFormatter::fmt_string. ( 5a435f7)
  • Change: ConsoleHandler contains console_type: ConsoleType instead of stderr: bool.
    ( aeceb23)
  • Change: Name: impl ConsoleHandler::create() to: impl ConsoleHandler::_create(). ( 4a24410)
  • Change: Name: impl FileHandler::create() to: impl FileHandler::_create(). ( e08498e)
  • Change: Update and improve tests. ( b51d4ba)
  • Change: Many updates to the Guide.
    ( 0d4e738) ( b16e260) ( bda2ef3) ( 3a0bd0b) ( b997cbb) ( bd3c370) ( af573a3)
  • Change: Update Guide, remove api and coverage directories from source control. ( f8acd75)
  • Change: Various improvements to the documentation. ( 19c327a)

Added

  • Breaking Add: New method to HandlerTrait - set_test_mode(). ( 0fc162b)
  • Add: New instructional Guide. ( 73d048a)
  • Add: New LoggerBuilder method: remove_file(). ( f9d5b95)
  • Add: New LoggerBuilder method: set_fn_name(). ( 0a76ce2)
  • Add: enum ConsoleType. ( b0f404b)
  • Add: New LoggerBuilder methods: add_pconsole_handler() and add_pconsole_handler_with(). ( 85bbe5a)
  • Add: New method to Logger - pconsole_logger(). ( 29bef45)
  • Add: New Logger associative function: remove_file(). ( 9f848fd)

Removed

  • Breaking: Remove impl Handler{...} - new() and create(name). ( 3623196)
  • Breaking: Removed Logger::reset_level(). No longer applicable. Use Logger::set_level(). ( 153c324)
  • Remove: main.rs from project. ( 6a2a3cc)

Fixed

  • Fix: Many tests due to new and modified API and internal code. ( 0c6aa2f)

Version 0.5.0 * - 2025-08-03

Details (click to see)

Changed

  • Breaking: Modified FormatType::Custom(String) to FormatType::Custom. ( 88183d3)
  • Document: Convert inline links to reference links. ( e5a7bf0)
  • Change: ConsoleHandler to contain stderr: bool instead of mod_path: String. Will now output to std::io::stderr if true. ( 41f5e9e)
  • Change: StringHandler removed name: String and renamed private method StringHandler::create() to StringHandler::new(). ( 41f5e9e)

Added

  • Add: Macro and method: is_logging. ( ee882f3)
  • Add: econsole_logger() and add_econsole_handler(). ( 41f5e9e)
  • Add: Handler::EConsole. ( 41f5e9e)

Version 0.4.1 * - 2025-07-29

Details (click to see)

Changed

  • Document comments improved or expanded. ( d35d3e5)

Added

Fixed

Version 0.4.0 * - 2025-07-27

Initial release.