One way to observe our software’s behavior is to write programs that emit a message each time some noteworthy event occurs. Appropriate strategies for logging vary with the needs of a particular application, but our example here represents one possible design. This example also shows how to throw, catch, and log an exception.
Often we want to distinguish between different categories of noteworthy event. In this example we have two: if something has gone wrong, we’ll call that an error, and everything else we’ll call info.
An event will consist of a
Level plus some message
String that we want to see in the log output. We might imagine expanding this type to include additional fields such as a timestamp or some application-specific contextual information.
A basic log handle need only consist of a function that specifies what action to take to record an event.
This is a log that writes each message to one of the process’s standard output streams. Info messages go to stdout and error messages go to stderr.
This is another log that writes each message to a file. The
path parameter is a function which specifies the output file path for each log level.
Sometimes we want to disable logging altogether. In that case, we would use this log whose
record function does nothing.
We can write functions to define one log as a variant of another. Here the
formattedLog function returns a log which prefixes the message string with additional information, then writes the modified message to the original log.
We have written a few small functions to assist
paren function encloses a string in parentheses, and the
(!) function joins two strings with a space in between.
We can also combine multiple logs together. This function constructs a logger that records an event by writing it to each of two other logs.
multiLog function is associative, and it forms a monoid with
nullLog as its identity. This allows us to conveniently fold a collection of logs into a single log. We will make use of
fold later in this program.
In some circumstances, a program may be able to recover from a partial failure. When a program proceeds after an exception instead of crashing, we usually want to write an event to the log to ensure that the problem does not go unnoticed.
recoverFromException function converts an
IO a action to an
IO (Maybe a) action.
action throws an exception, we write an error event to the log and return
The first, which we call the “boot” log, is used only while the program is first starting up; it provides a place to record any errors that may occur while setting up the more complicated “app” log.
There are a number of ways that logging to a file can go awry – for example, if filesystem permissions disallow writing to the file.
initFileLog (defined below) produces a
Log. By using
recoverFromException, we allow our program to recover if it fails. The type of the
fileLog variable is
Now we can assemble the application’s full logging strategy: Each event will be recorded to the console, as well as to the log files (if possible). Both the
fold functions take advantage of the
Here we record one event, and the demonstration ends (though we ask you to imagine that the rest of an application continues from here).
Let us say that we expect our program to be run with two environment variables,
ERROR, specifying the desired log file paths.
getEnv function obtains the value of an environment variable, throwing an exception if no definition for this variable is present in the process environment.
assertWritable action determines whether there exists a file at the given
path, and whether we will be able to write to that file.
If the file does not exist, getPermissions throws an exception.
If we run the example without setting up the log files and environment variables, we see an error in the console log. The message comes from the exception thrown by getEnv.
To enable file logging for this program, we have to create the two files and run the program in a context with the
This time, in addition to the console output, we can also see that the final event has been recorded to the info log file.
Finally, we will remove write permissions from one of the log files and run the program once more.
This time the error message we see comes from the exception that we induced using