How it works...

Our FileLogger will, as the name suggests, log things to a file. It also accepts a maximum logging level on initialization.

You should not get used to directly logging things on a disk. As stated by The Twelve-Factor App guidelines (https://12factor.net/logs), logs should be treated as event streams, in the form of a raw dump to stdout. The production environment can then route all log streams to their final destination over systemd or a dedicated log router such as Logplex (https://github.com/heroku/logplex) or Fluentd (https://github.com/fluent/fluentd). These will then decide if the logs should be sent to a file, an analysis system like Splunk (https://www.splunk.com/), or a data warehouse like Hive (http://hive.apache.org/).

Every logger needs to implement the log::Log trait, which consists of the enabled, log, and flush methods. enabled should return if a certain log event is accepted by the logger. Here, you can go wild with whatever filtering logic you want [18]. This method is never called directly by log, so its only purpose is to serve you as a helper method inside of the log method, which we are going to discuss shortly. flush [46] is treated the same way. It should apply whatever changes you have buffered in your logging, but it is never called by log.

In fact, if your logger doesn't interact with the filesystem or the network, it will probably simply implement flush by doing nothing:

fn flush(&self) {}

The real bread and butter of the Log implementation is the log method[24], though, as it is called whenever a logging macro is invoked. The implementation typically starts with the following line, followed by the actual logging and a finishing call to self.flush()[25]:

if self.enabled(record.metadata()) {

Our actual logging then consists of simply writing a combination of the current logging level, Unix timestamp, target, and logging message to the file and flushing it afterward.

Technically speaking, our call to self.flush() should be inside the if block as well, but that would require additional scope around the mutably borrowed writer in order to not borrow it twice. Because this is not relevant to the underlying lesson here, as in, how to create a logger, we placed it outside the block in order to make the example more readable. By the way, the way we borrow writer from an RwLock is the subject of the Accessing resources in parallel with RwLocks in Chapter 8, Parallelism and Rayon. For now, it's enough to know that an RwLock is a RefCell that is safe to use in a parallel environment such as a logger.

After implementing Log for FileLogger, the user can use it as the logger called by log. To do that, the user has to do two things:

  • Tell log which log Level is going to be the maximum accepted by our logger via log::set_max_level() [66]. This is needed because log optimizes .log() calls on our logger away during runtime if they use log levels over our maximum level. The function accepts a LevelFilter instead of a Level, which is why we have to convert our level with to_level_filter() first [66]. The reason for this type is explained in the There's more... section.
  • Specify the logger with log::set_boxed_logger()[68]. log accepts a box because it treats its logger implementation as a trait object, which we discussed in the Boxing data section of Chapter 5, Advanced Data Structures. If you want a (very) minor performance gain, you can also use log::set_logger(), which accepts a static which you would have to create via the lazy_static crate first. See  Chapter 5, Advanced Data Structures, and the recipe Creating lazy static objects, for more on that.

This is conventionally done in a provided .init() method on the logger, just like with env_logger, which we implement in line [58]:

    fn init(level: Level, file_name: &str) -> Result<()> {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(file_name)?;
let writer = RwLock::new(BufWriter::new(file));
let logger = FileLogger { level, writer };
log::set_max_level(level.to_level_filter());
log::set_boxed_logger(Box::new(logger))?;
Ok(())
}

While we're on it, we can also open the file in the same method. Other possibilities include letting the user pass a File directly to init as a parameter or, for maximum flexibility, making the logger a generic one that accepts any stream implementing Write.

We then return a custom error created in the lines that follow [74 to 116].

An example initialization of our logger might look like this [119]:

FileLogger::init(Level::Info, "log.txt").expect("Failed to init FileLogger");
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.18.198