ColdFusion & Log4j Part 3 - Custom Attributes and Mapped Diagnostic Context
ColdFusion, Log4j
Customizing Logging Output with ColdFusion and Log4j
In the first part of this series, I introduced you to integration of ColdFusion with the Apache Log4j framework using the open-source CFLog4j package. By the end of my second article, we were writing our logs to a database of our choice. If you're like me, you spent quite a bit of time on trial and error learning the basics of using these technologies together, reading a lot of log files in the process! You may have noticed the profound lack of detailed fields available to you in your log output; somehow, concatenating several fields into a single message feels like cramming a square peg into a round hole.
In this article, I will show you how easy it is to customize your logging output to include whatever information you deem relevant!
Nested Diagnostic Contexts
Log4j provides an implementation of Nested Diagnostic Context (NDC), which is a fancy way of saying that we can record customized contextual attributes which allow the logs to be distinguished when multiple transactions occur near-simultaneously. Since NDCs are managed on a per-thread basis, we can safely use them to store identifying information along with our log payload. Additionally, Log4j provides a Mapped Data Context (MDC) which is based on a map, a structure that underlies the ColdFusion struct concept. In my example I will use the MDC for storing data that we want to record to the logs to help identify it better.
Extending CFLog4j
I decided that the best way to add extra attributes to the logs is be pass in a struct containing the additional key/value pairs into the top-level logging methods. In order to do this, we must override all four of the logging methods (info(), debug(), warn(), and error()) must be modified to accept an additional argument and pass it on to the logMessage() method, which also will be overridden to operate with this data. Once this is completed we can create a struct containing things like user name, IP address, or even the name of the event that caused the log entry to be recorded. I am working on another post that addresses continual verbose logging in Mach-II applications that employs just this technique.
Included here is the code for MyLog4j.cfc, which extends the base CFLog4j component to include the contextual logging functionality. You will notice that I've created three additional variables to process the extended attributes we are logging. Thanks to Qasim's excellent work on CFLog4j, I am able to use his class loader to create an instance of the org.apache.log4j.MDC object, which I then use to put() all of the extended attributes. At this point the call to logger.log() is the same as before; the object will be aware of the MDC and will be able to access the properties stored inside it. Finally, as a cleanup step I remove all of the values loaded into the MDC with remove(). At this point the only thing left to do is to configure the Log4j properties file, so that we can tell the Appender that it should record our additional information.
<cfcomponent displayname="MyLog4j" hint="Custom CFLog4j Object to write extended properties" extends="com.cflog4j.CFLog4j" output="false"> <cffunction name="info" returntype="void" access="public" output="false" hint=""> <cfargument name="logger" type="any" required="true" hint="Logger" /> <cfargument name="message" type="string" required="true" hint="message to be logged." /> <cfargument name="extendedAttributes" type="struct" required="false" default="#structNew()#" hint="Key/value pairs of additional properties" /> <cfset logMessage( arguments.logger, 'info', arguments.message , arguments.extendedAttributes ) /> </cffunction> <cffunction name="debug" returntype="void" access="public" output="false" hint=""> <cfargument name="logger" type="any" required="true" hint="Logger" /> <cfargument name="message" type="string" required="true" hint="message to be logged." /> <cfargument name="extendedAttributes" type="struct" required="false" default="#structNew()#" hint="Key/value pairs of additional properties" /> <cfset logMessage( arguments.logger, 'debug', arguments.message , arguments.extendedAttributes ) /> </cffunction> <cffunction name="warn" returntype="void" access="public" output="false" hint=""> <cfargument name="logger" type="any" required="true" hint="Logger" /> <cfargument name="message" type="string" required="true" hint="message to be logged." /> <cfargument name="extendedAttributes" type="struct" required="false" default="#structNew()#" hint="Key/value pairs of additional properties" /> <cfset logMessage( arguments.logger, 'warn', arguments.message , arguments.extendedAttributes ) /> </cffunction> <cffunction name="error" returntype="void" access="public" output="false" hint=""> <cfargument name="logger" type="any" required="true" hint="Logger" /> <cfargument name="message" type="string" required="true" hint="message to be logged." /> <cfargument name="extendedAttributes" type="struct" required="false" default="#structNew()#" hint="Key/value pairs of additional properties" /> <cfset logMessage( arguments.logger, 'error', arguments.message , arguments.extendedAttributes ) /> </cffunction> <!--- Private Methods ---> <cffunction name="logMessage" returntype="void" access="private" output="false" hint=""> <cfargument name="logger" type="any" required="true" hint="Logger" /> <cfargument name="type" type="string" required="true" hint="" /> <cfargument name="message" type="string" required="true" hint="message to be logged." /> <cfargument name="extendedAttributes" type="struct" required="true" hint="Additional attributes to log" /> <cfset var priority = createClassFromLog4jarLoader( 'org.apache.log4j.Priority' ).toPriority( arguments.type )> <cfset var key = "" /> <cfset var value = "" /> <cfset var MDC = createClassFromLog4jarLoader( 'org.apache.log4j.MDC' ) /> <cfloop collection="#arguments.extendedAttributes#" item="key"> <cfset value = arguments.extendedAttributes[key] /> <cfset MDC.put( key , value ) /> </cfloop> <cfif canLog( arguments.logger, priority, arguments.type )> <cfset arguments.logger.log( priority, arguments.message ) /> </cfif> <cfloop collection="#arguments.extendedAttributes#" item="key"> <cfset value = arguments.extendedAttributes[key] /> <cfset MDC.remove( key ) /> </cfloop> </cffunction> </cfcomponent>
Updating the Log4j Properties File
The properties file that you update depends on whether or not your application records logs to a database. If so, you'll also need to ensure that the extra columns that will contain these properties have been created and update the SQL statement accordingly. I am only showing an example for disk-based logging at this point but it should be straightforward to update the SQL statement to allow for the additional attributes.
For each attribute you add to the extendedAttributes struct you will need to update the ConversionPattern in the properties file. Log4j lets you specify the names of the properties with %X{attribute name}. So, if you created an attribute in the code that calls the logging method named 'username', to specify it in the ConversionPattern you would write %X{username}. This will cause the logfile to contain that information in addition to whatever else was in your ConversionPattern string to begin with.
Below is a short program listing that contains an updated version of the testCFLogger.cfm template included with the CFLog4j package. I am also including an updated properties file that shows the usage outlined above. Combined with the MyLog4j.cfc code, this is enough for you to build a working example in your development environment.
<!---- Initialize CFLogger as a singelton in application scope. ---> <!--- <cfif not structkeyexists( application, 'init' )> ---> <!--- create the CFLog4j Config object. ---> <cfset CFLog4jConfigPath = '/var/www/cflog4j/config/cflog4j.xml' /> <cfset CFLog4jConfig = createobject( 'component', 'com.cflog4j.CFLog4jConfig' ).init( CFLog4jConfigPath )> <cfset application.CFLog4J = createobject( 'component', 'com.cflog4j.MyLog4j' ).init( CFLog4jConfig ) /> <cfset application.init = true /> <!--- </cfif> ---> <!--- get a category instance from CFLog4j. those instances can be defined in cflog4j.properties file. ---> <cfset logger = application.CFLog4J.getCategoryInstance()> <cfset extendedAttributes = structNew() /> <cfset extendedAttributes['username'] = 'domainSername' /> <cfset extendedAttributes['ipaddress'] = cgi.remote_host /> <cfset application.CFLog4J.info( logger, "It's hard enough to find an error in your code when you're looking for it; it's even harder when you've assumed your code is error-free (Steve McConnell).", extendedAttributes )> <!--- This will not show up unless you change the Category level to DEBUG in properties file ---> <cfset application.CFLog4J.debug( logger, "It's hard enough to find an error in your code when you're looking for it; it's even harder when you've assumed your code is error-free (Steve McConnell).", extendedAttributes )>
Closing Thoughts
When I first developed this approach, I started to go crazy with logging statements in my application. IP address, user name, Mach-II event name, event arguments, etc. While there's nothing wrong with being verbose if you can afford the disk space, there's definitely a performance penalty for logging what amounts to every detail of a user click. Additionally, I ran into unexpected database errors when I shared a datasource between the logging function and the other features in the app. At a minimum, I recommend setting up a separate datasource and doing all of your logging against that instead of mixing logging and business queries in your app's main datasource.
Even when using separate data sources, you may find that application performance becomes sluggish as every action is logged to the database. Fortunately, as of ColdFusion 8, event gateways are available to all versions of CF. I have had success migrating my logging to an asychronous model, where my application code throws the logging operation over the wall and allows it to happen behind the scenes. I will be writing about this in an upcoming post.
Once again, Happy Logging!
ColdFusion & Log4j Part 2 - Writing Logs to a Database
ColdFusion, Log4jThe first part of my mini-series on Log4j and ColdFusion talked about CFLog4j and some basic background information. This post focuses on using CFLog4j and some custom-rolled Java to write logs to a database.
The JDBCAppender Object
Recent versions of Log4j include a JDBCAppender class that is used to write logs to a database. The class is able to create JDBC connections that are used for executing the log inserts. For stand-alone Java applications, this works well; however, one of the benefits of using ColdFusion is that the application server manages various resources for us, which include database connection pooling. Connection pooling allows the application server to manage a cache of connections to the database; by holding on to this pool of connections, the relatively expensive process of opening and closing connections happens less frequently. Additionally, the connection pooling options in ColdFusion provide tuning opportunities for security and performance from within the CF Administrator. It would be nice to avoid creating additional connections to the database outside of this managed pool.
As it turns out, by using libraries included with (and used by) ColdFusion, it is possible to use these managed database connections. Rather than create our own database connection for writing logs, which is all that JDBCAppender knows how to do, we can follow the recommendations outlined in the Javadoc for the class by creating a subclass with methods overriding getConnection() and closeConnection(). These methods will use the ColdFusion 8 library to retrieve a connection from the application server’s connection pool and return it when the log insert is finished. In this way we can stick to the established parameters of our CF datasources and avoid the peril associated with managing our own connections.
Creating our Custom JDBC Appender
I should start with a caveat - I am not a Java expert. Like many experienced CF developers, I have dabbled in Java over the years, mostly for integration projects, but I don’t claim that my solution is the “best” way to extend Log4j’s JDBCAppender for ColdFusion. In fact, I welcome any suggestions that can help me improve my design!
You will need to create a new java project in your IDE of choice that includes a source file for your custom JDBC appender. How you name the class is trivial, but in order for Log4j to find it you will want to place it in the package
org.apache.log4j.jdbc
In addition to any standard libraries included by default by your IDE, you will want to add build path references for:
-
Log4j-1.2.15.jar (or whatever version you have decided to use)
- Cfusion.jar (typically located in coldfusion8\lib)
The two methods that must be overridden are getConnection() and closeConnection(). In addition I recommend creating a new datasource property along with getters and setters so that the Configurator object automatically populates this from the .properties file (we will get to that next). This way you don’t have to hardcode the datasource name in your class, making it more portable.
For reference I am including a working example, but I realize this could be written in a variety of ways depending on your coding style. Once you have created your Class, you will want to build the project and export it as a jar file. Make note of the location because you will need to make the file available to ColdFusion later.
package org.apache.log4j.jdbc;
import org.apache.log4j.jdbc.JDBCAppender;
import coldfusion.server.ServiceFactory;
import java.sql.*;
public class SubJDBCAppender extends JDBCAppender {
protected String datasource = "";
public String getDatasource() {
return datasource;
}
public void setDatasource(String ds) {
datasource = ds;
}
protected void closeConnection(Connection con) {
if (con != null) {
try {
con.close();
}
catch (SQLException e) {
throw new RuntimeException("Unable to close connection");
}
}
}
protected Connection getConnection() throws SQLException {
if (connection == null) {
// Use the ColdFusion ServiceFactory to retrieve a connection from the pool
// This will return a JRun Connection Handle which is a subclass of Connection
connection = ServiceFactory.getDataSourceService().getDatasource(getDatasource()).getConnection();
}
return connection;
}
}
Properties File Differences
The RollingFileAppender is only used for writing to disk-based logs. Your new SubJDBCAppender is only used for writing to a database. These storage mediums have different constraints around their practical use. We will modify the CFLog4j.properties file to reflect these differences.
Both types of Appenders require that certain basic properties be configured in your properties file. Specifically, you will need to include configuration information for log4j.category.CFLog4j which will include the root category information. You’ve already seen how this is done for Log4j disk-based logging. In the JDBC example I suggest using this line:
log4j.category.CFLog4j=INFO,
jdbc
This describes a category of INFO (all messages of INFO level or higher will be logged) and a logger named jdbc.
Next you will need to specify the Appender class that should be used by Log4j. In this case we will be using the SubJDBCAppender class that you’ve written.
log4j.appender.jdbc=org.apache.log4j.jdbc.SubJDBCAppender
Both the RollingFileAppender and your SubJDBCAppender will make use of the same PatternLayout class. PatternLayout is a subclass of Layout, and allows the creation of customized formatting for the recording of log data using certain key characters. Full details of these characters is available in the Apache Logging Javadoc referenced earlier in these articles. I suggest leaving the log4j.appender.jdbc.layout property unchanged.
Now we get to the real differences between the two approaches. Basically everything else, other than what I point out above, can be deleted because there’s nothing like it in the database-based logging properties file. The rest of the file contains properties relevant only to logging via the SubJDBCAppender.
Remember when you wrote the code to set up the datasource property in your custom SubJDBCAppender? Here is where you put it to use. The Log4j framework uses various Configurator objects for importing declarative configuration preferences. The PropertyConfigurator will take the properties defined in the properties file and map them over to public properties on the Appender object, provided that the two have the same name. So, you will need to define the datasource in the properties file as follows:
log4j.appender.jdbc.datasource=of your logging datasource>
The buffer size determines how frequently the Log4j framework will write entries out to the database. I prefer to set it at 1 so that every record is immediately committed. You may prefer something else but for experimenting, this is the way to go.
log4j.appender.jdbc.buffersize=1
You will have to specify the SQL statement that will be used to insert your logs. A typical insert statement looks something like this:
Log4j.appender.jdbc.sql=insert
into(message, class, priority, log_date)
values (%m, %c, %p, %d{MM/dd/yyyy HH:mm:ss:SSS})
For reference I am including a working example properties file but you will need to create one that fits your purposes.
log4j.category.CFLog4j=INFO,jdbc
log4j.appender.jdbc=org.apache.log4j.jdbc.SubJDBCAppender
log4j.appender.jdbc.datasource=logger
log4j.appender.jdbc.layout=org.apache.log4j.PatternLayout
log4j.appender.jdbc.buffersize=1
log4j.appender.jdbc.sql=INSERT INTO BasicLog (message, class, priority, log_date) VALUES ("%m", "%c", "%p", "%d{yyyy-MM-dd HH:mm:ss}")Other Configuration Details
Since you have exported the SubJDBCAppender as a jar file, you will need to place it somewhere so that ColdFusion can load it. You can do this either by copying it into the classpath or creating a new entry in the ColdFusion Administrator Java settings pointing to the jar file. I suggest the latter. In either case you will need to bounce the ColdFusion administrator for the changes to be available.
The table you will be logging into must also be created where it is accessible by the datasource you plan to use for writing the entries. It would be adequate to create a table with four columns for message, class, priority, and log date.
Once you have configured the ColdFusion server so that it can find your SubJDBCAppender.jar file and configured your database to include the required logging table, you will be ready to start logging to the database. I suggest using the same working test code from the disk-based CFLog4j example I talked about in my last post; that way you are starting with a functional Log4j implementation so you know you can revert back to it if necessary.
Limits of this Implementation
While the above process will work nicely to let you log to the database there are a few limitations that I am not fond of. I will be addressing both of these in my research and blogging about them in the future.
The first is that this approach is not going to work very well in a shared hosting environment, as you probably are not going to be able to modify the classpath used by ColdFusion, much less bounce the server. While I have some ideas about resolving this problem, I haven’t identified the best alternative yet.
The second problem relates to what is logged. While it is useful to log the information for which CFLog4j/Log4j are configured, this is not really adequate for my purposes. What if I want to track the user name who caused the logging code to be invoked, or their IP address? What if I want to track a specific Mach-II event or something else? I can stuff it all into the message field, but this gets crowded real fast, especially when we think about doing metrics and analytics using log data. As it turns out, Log4j provides a nice way to log additional custom properties and I will cover that in another post.
Until then, happy logging!
EDIT - I noticed that the properties file information I included was not 100% right. Especially not for the sample text that is included in CFLog4j (has quotes, semicolons etc). The updated properties will work fine in MySQL but in MSSQL you will need to 'SET QUOTED_IDENTIFIER OFF;' at the start of the SQL statement.
ColdFusion and Log4j
ColdFusion, Log4jA Brief Introduction to CFLog
As it is with many ColdFusion features, the CFLog
<cflog
text = "text"
application = "yes|no"
file = "filename"
log = "log type"
type = "information|warning|error|fatal">As you probably already knew, CFLog
If you are writing logs to a shared location, such as the application log, you might want to include the application name. Generally I prefer to log information for specific apps in their own files to avoid confusion, so I don't see recording the application name as that important.
If you choose to specify a filename, ColdFusion will automatically create a log file in the coldfusion8\logs folder. You have no control over where the file goes; it is always in this folder. If you are in a shared hosting environment, this is no good at all, as you probably can't access those logs!
You may not be aware that in Log4j the type of log can be used for more than just flagging an entry in the log file as one type or another. Log4j provides the means to configure different loggers that will selectively listen in for messages of a certain log level. In fact, the listed possibilities for type are derived directly from the log levels available by default in Log4j - trace, debug, info, warn, error, and fatal. In order to avoid getting into too much detail about the various levels or the hierarchical nature of Log4j's Logger objects, I will refer you to the great introduction available on the Apache Logging site. This is a must-read if you are really interested in digging deeper into Log4j.
Finally, besides not allowing the selection of a path for application logs, which is limiting in a shared hosting environment,
Enter CFLog4j
When I began my research, I figured that someone else must have run into this problem before. Indeed, Qasim Rasheed has developed an excellent starting point for logging to a database with Log4j. The open-source CFLog4j provides a relatively easy way to access Log4j features, and is suitable out-of-the-box for use in a shared hosting environment. In fact, if all you’re looking to do is write your own private logs to disk in a shared environment, CFLog4j will do what you need without any additional modifications.
CFLog4j includes a class loader that will allow you to use the most recent version of Log4j. CF8 ships with an older version, and the one that comes with CFLog4j is also not the most recent, so I recommend downloading the latest stable build of 1.2.x (1.2.15 as of this writing). Learning to use CFLog4j for disk-based logs was an important step in my development of understanding about Log4j, so I encourage you to experiment with it.
Once you have downloaded the most recent versions of the files, configuring CFLog4j is fairly straightforward. There are two configuration files that you will be concerned with:
1. cflog4j.xml – This file is CFLog4j specific and contains the location of the properties and log4j jar files. You will need to update the file to point to the location of cflog4j.properties (for now just leave it in the same place as the xml file), and to the location of the log4j jar file.
2. cflog4j.properties – This file is not unique to CFLog4j. This is a typical Log4j properties file that could be from any Java application. It defines all of the configuration details for your Loggers. I recommend sticking with the parameters Qasim set up in his example file, with the exception of the File property which you should change to the location where you want your log file to be written.
The CFLog4j.properties File
As I mentioned above, this file is not unique to CFLog4j. Any application using Log4j will have to configure the Logger objects somehow. This can be done programmatically but is commonly done through configuration files. Log4j has the ability to use XML-based configuration, or more typically, a Java .properties file.
I am not going to get into too much depth about this file as there are extensive articles written about it; here is a good one. The one detail I really want to point out is the property for the Appender. The Log4j Appender is what actually writes the log information out to the storage medium. It is also the object that we will be extending in order to write to a database. Log4j comes with a JDBCAppender class that can be used to write to a database but it is not appropriate for use from ColdFusion.
At this point you should be able to use the supplied testCFLogger.cfm file to write some test logs. Note that if you changed the location of the XML configuration file (so that it is not in the same directory as the CFLog4j CFC’s) you will need to pass in the path to the constructor of the Configurator object.
Now that you can write your logs to disk, my next posts talks about writing them to a database.




Loading....