Introducing the file logger use case

Let's consider a use case for building a file logging facility. 

Suppose that we want to provide an API for logging messages to files based upon a set of logging levels. By default, we will support three levels: info, warning, and error. A logger facility is provided so that a message will be directed to a file, as long as it comes with a high enough logging level.

The functional requirements can be summarized as follows:

  • An info-level logger accepts messages with info, warning, or error levels.
  • A warning-level logger accepts messages with warning or error levels only.
  • An error-level logger accepts messages with an error level only.

To implement the file logger, we will first define some constants for the three logging levels:

const INFO    = 1
const WARNING = 2
const ERROR = 3

These constants are designed to be in numerical order, so we can easily determine when a message has a logging level as high as what the logger can accept. Next, we define the Logger facility as follows:

struct Logger
filename # log file name
level # minimum level acceptable to be logged
handle # file handle
end

A Logger object carries the filename of the log file, the minimum level for which messages can be accepted by the logger, and a file handle that is used for saving data. We can provide a constructor for Logger as follows:

Logger(filename, level) = Logger(filename, level, open(filename, "w"))

The constructor automatically opens the specified file for writing. Now, we can develop the first logging function for info-level messages:

using Dates

function info!(logger::Logger, args...)
if logger.level <= INFO
let io = logger.handle
print(io, trunc(now(), Dates.Second), " [INFO] ")
for (idx, arg) in enumerate(args)
idx > 0 && print(io, " ")
print(io, arg)
end
println(io)
flush(io)
end
end
end

This function is designed to write the message into the file only if the INFO level is high enough to be accepted by the logger. It also prints the current time using the now() function and an [INFO] label in the log file. Then, it writes all the arguments separated by spaces and finally flushes the I/O buffer.

We can quickly test the code so far. First, we will use info_logger:

The message is correctly logged in the /tmp/info.log file. What happens if we send an info-level message to an error-level logger? Let's take a look:

Now, this is a little more interesting. As expected, because the error-level logger only accepts a message with an ERROR level or higher, it did not pick up the info-level message. 

At this point, we may be tempted to quickly finish the two other functions: warning! and error! and call it the day. If we were determined to do that, the warning! function would look just like info!, with just a few small changes:

What are the differences between these two logging functions? Let's take a look:

  • The function names are different: info! versus warning!.
  • The logging level constants are different: INFO versus WARNING.
  • The labels are different: [INFO] versus [WARNING].

Other than these, both functions shared the exact same code. Of course, we can just keep going and wrap up the project by writing error! the same way. However, this is not the best solution. Imagine that if the core logging logic needs to be changed, for example, the formatting of log messages, then we have to make the same change in three different functions. Worse yet, if we forget to modify all of these functions, then we end up with inconsistent logging formats. After all, we have violated the Don't Repeat Yourself (DRY) principle.

..................Content has been hidden....................

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