Quantcast
Viewing all articles
Browse latest Browse all 12

The Dark Art of Logging

Image may be NSFW.
Clik here to view.
Logging is a mechanism of keeping track of the runtime behavior of an application, mainly for debugging / tracing purposes. In almost all of significantly complex / vital applications, maintaining a proper application log is a must; hence, almost all developers are quite familiar with the paradigm of logging. And it isn’t rocket science. You just have to call a simple method with whatever you want to log, and mess around with a simple configuration file, and that’s all. So what’s the big deal?

The problem is that though logging is a simple thing to do, doing it correctly needs a lot of practice, experience and understanding. That makes it more of an art than a science. In my personal experience, mastering the art of logging requires lots of patience, experience and forward thinking. It’s quite difficult to teach someone how to do it (sort of tacit knowledge), and most freshers tend to see the effort put into writing to log files as something additional. But all that effort pays up when you go through long nights trying to figure out what went wrong in a production system. But for that, it is important that proper logging is done through-out the system, which requires all of the team to master this dark art. Just one guy doing it right in a team is simply not enough.

First of all, it is important to understand the purpose of logging. The primary purpose of maintaining a log is to be able to trace the execution of the application. This can be used for debugging and also to ensure that application performs operations as expected. Also, in some cases, these log files are used for auditing purposes as well.

Therefore, when we write to a log file, it is important to keep in mind that whatever the content that we log is actually useful for the above mentioned purposes. Logging for the sake of doing it will not help. We have to envision a scenario in the future where we are going through log files, and plan ahead and log the message to be useful at such times.

Log about Context

One very important thing to keep in mind is to ensure that sufficient information about the context is written to the log file. Consider the following example method, which calls a complex business operation with two parameters passed in by the user.

public void invokeBusinessOperation(String param1, String param2) {

	if (LOG.isTraceEnabled()) {
		LOG.trace("Invoking the foo() business operation");
	}

	businessObject.foo(param1, param2);

}

This will write the following to our log output.

Invoking the foo() business operation

So far, so good. In the future, if need to ensure that this business operation is invoked as expected, we can monitor the log file and make sure that it gets called. This is way better than not having any log statements, in which case we have no way of tracking this. But is this good enough?

Think of a situation where this application is being used by multiple concurrent users. What we will see in the log file is something like this.

Some arbitary log message from another component.
Invoking the foo() business operation
Another log message
Another log message
Invoking the foo() business operation
Some arbitary log message from another component.
Another log message
Invoking the foo() business operation
Invoking the foo() business operation
Another log message

This is not very useful, since we have no way of identifying the exact invocation that we are interested in (due to concurrency and multiple invocations by various users). But, if we change our logging statement like this,

public void invokeBusinessOperation(String param1, String param2) {

	if (LOG.isTraceEnabled()) {
		LOG.trace("Invoking the foo() business operation with params : " + param1 + ", " + param2);
	}

	businessObject.foo(param1, param2);

}
Some arbitary log message from another component.
Invoking the foo() business operation with params : abc, def
Another log message
Another log message
Invoking the foo() business operation with params : xyz, ght
Some arbitary log message from another component.
Another log message
Invoking the foo() business operation with params : bar, tea
Invoking the foo() business operation with params : jkl, mno
Another log message

Below is a real life example of log outputs, which really shows the importance of contextual logging.

Reservation Validation Failed: Contract does not cover Meal Plan
Reservation Validation Failed (Reg.ID : 12) - No Contract Rate in Contract (33) covers the Meal Plan  (1 : FB)

As seen, the second approach would allow us to identify the problem easily, where as the first one leaves us no clue about which reservation or which contract.

With the context information, the log output is more meaningful, and would be much more helpful in future tracing / debugging activities. Therefore, it is important that we log useful context information when we do logging. Otherwise, it does not serve the purpose of maintaining a log file.

Log sufficiently – Not too much, not too less

It is important that we do a fair amount of logging in our applications. We should be able to go through the log file and ensure that application execution happens in expected path, and also to isolate errors and trace back to the actual cause, with minimal effort. Getting that done is not easy.

If we go ahead and log each and every method execution and parameters, our log files will grow, and it will contain unnecessary information which will make the debugging process a nightmare. On the other hand, if we don’t have sufficient logging, then it will be impossible to trace back, since we have no clue about what went wrong and where it went wrong. So it is important to strike a balance between these two ends.

One approach to achieve this is to log at least once when executing an important business function, in Information Level (logging levels will be discussed later on). This facilitates tracing execution of the application easily. Also, when an exceptional situation occurs (exceptions, errors and important validation failures) it should be logged in an appropriate level.

Log at the correct level

All of the logging frameworks out there support various log levels. These log levels exist to allow developers to control the output of log files using configurations, to filter out unnecessary messages. I will use the log levels supported by Apache Commons Logging API for this discussion, but counterparts exist in other log frameworks under various names (note that the priority of the log level increases downwards).

Level Description
TRACE This is the lowest logging level. This should be used for tracing activities such as method invocation and parameter logging, etc.
DEBUG This log level is commonly used for application debugging statements. This level should be used to log messages which are helpful in debugging, but which are too verbose to be written to the log under normal situations.
INFO This is the information log level. This should be used to log statements which are expected to be written to the log during the normal operations. For example, running a important business operation should be logged under this level.
WARN Warning Level. This should be used to log warnings, which are situations that are usually unexpected, but which does not significantly affect the business functionality.
ERROR Error level should be used to log all errors and exceptions which affect the business functionality of the application.
FATAL Fatal log level is used to write extreme situations where the entire application execution could be affected due to some cause.

Log before throwing exceptions

It is a good practice to log an error message before throwing an exception. This helps a lot when trying to identify the root cause of an exception. Although we cannot push in much detail to a normal exception (unless we add in attributes to keep those), we can log detailed contextual information to the application log, which will in turn pay off when we struggle in a late night trying to figure out what went wrong.

Usually, log statements for exception are written at ERROR level. However, if the exception that you are throwing is part of the business flow (which is a practice questioned by some), then it would be better to log such events in a lesser level, such as example WARN or INFO.

For example, in one of my past projects, we needed to do a complex validation sequence, and we used exceptions as the mechanism of notifying validation failures. However, we did not want to pollute the application log with ERROR statements for such cases, which are normally expected during execution of the application (ERROR statements should be used for real errors). So instead of ERROR level, we used the INFO level for those messages.

Avoid System.out / System.err

It is surprising to see that some developers of enterprise scale applications resort to System.out or System.err, while a proper logging framework is in use within the application. If we use System.out while rest of the application uses a logging framework, we lose the advantage of the logging framework. Logging frameworks can be configured to log at different levels and patterns, but System.out calls will be out of that configuration. Therefore, it is always better to use the same logging framework throughout the application.

One other thing is that automatic code generation for try-catch blocks in IDEs generate ex.printStackTrace() as the default catch block. The printStackTrace() method writes to System.err and this should be avoided in the presence of a logging framework. Instead of that, use the facilities provided by the logging framework for the task.

LOG.error("error description message here", ex);

Think of ‘Performance’ too

Although logging is really important for an application, it is not part of the actual business operation.  Although for many applications, overhead introduced by logging is negligible, there are situations where it should be seriously considered, especially since it involves writing to files (if the logger is configured so).

For example, consider a hypothetical application where for each business operation invocation (which takes less than a second to complete), we log 1KB of log output to a file. If that application is used heavily by concurrent users, say, 10000 business operations per second, then our application will need to write 1KB * 10000 = Approx. 10 MB of data per second. If we assume that the application logic needed to be executed can handle such a load without any issue, the bottleneck of the application will be the logging part, since it requires performing slow disk I/O.

Also, in most of the cases, writing context information to a log requires heavy string concatenations. String concatenations are pretty slow, and it will leave out garbage output in the Java’s String Constant Pool. However, we cannot avoid this as well (you can use a String Builder if there are many number of concatenations, but that will not be a good idea of the number of concatenations are less). One thing we could do is to use guard conditions for the logging statements, so that if the corresponding log level is disabled, we can skip the overhead of concatenation and running through the logging API code.

For example,

if (LOG.isDebugEnabled()) {
	LOG.debug("Dispatching " + topic + " emails to : " + emailAddress);
}

With the ‘if’ condition, the log entry will be invoked only when the relevant log level is enabled. If it is not, the log statement will not be executed, and the string concatenation will not happen as well. If the ‘if’ condition was not there, then the concatenation will happen, and then the method will be executed. Within the method execution it will figure out that the logging for the level is disabled, and then it will discard the log message.  It is advised to use these guard blocks for lower log levels such as TRACE and DEBUG, which are normally disabled in production setup.

Get your IDE to do the hard work

Most of the IDEs out there supports coding templates, which are very useful when it comes to logging. These templates can generate the boilerplate code that is exhaustive to write. Then it’s just a matter of getting the template executed when you are writing your log statements, etc.

Following examples are for Eclipse, but similar approaches are there for other IDEs as well.

In Eclipse, you can create code templates through Window -> Preferences -> Java -> Editor ->Templates.

For generation of logger declaration (using Commons Logging), I use the following template which I have named as ‘getlog’.

${:import(org.apache.commons.logging.Log, org.apache.commons.logging.LogFactory)}
private static final Log LOG = LogFactory.getLog(${enclosing_type}.class);

So when I type in getlog and hit Ctrl+Space, Eclipse generates the following for me, with the proper class name filled.

private static final Log LOG = LogFactory.getLog(THE CLASS NAME.class);

Likewise, we can create templates for generating the if-blocks for DEBUG level as follows.

if (LOG.isDebugEnabled()) {
	LOG.debug("${msg}");
}

With this, when you type in debug and hit Ctrl+Space and get the template executed, you will get the complete if-block, and your cursor placed inside the double quotes, ready to type the message.

One more thing to tweak the IDE is to change the standard code template for try-catch blocks, which contains the printStackTrace() method. This can be changed through Window -> Preferences -> Java -> Code Style -> Code Templates. Change the Code -> ‘Catch block body’ and  ‘Code in new catch blocks’  template to the following.

LOG.error("${msg}", ${exception_var});

And Finally,

As always, it’s easier said than done. It requires some practice to master this, but once you have, you will know instinctively when and how you should log, and when you shouldn’t.


Viewing all articles
Browse latest Browse all 12

Trending Articles