HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML

The most basic way to capture these elements of interest is through application logs. Most Java-based production systems have them in some form, and most of them probably implement a custom API or use one of a handful of third-party packages that may or may not be cross-compatible. Out comes java.util.logging in the new Java 2 Platform, Standard Edition (J2SE) v1.4. Developed collaboratively with input from several key contributors (see “JSRs: Java Specification Requests” at http://jcp.org/jsr/detail/47.jsp for details), this package can be used as is, extended for additional functionality, and in conjunction with enterprise application services.

How does it work out of the box? What are its limitations and how easy is it to extend its capabilities? I’ll discuss these issues, plus show how to add database-level logging to the package’s framework.

J2SE Logging: Out of the Box
Let’s start by discussing what can be done with the basic logging package. I’ll give a general overview, but I suggest you take a trip to http://java.sun.com/ for a more complete picture.

Here’s how it works: the package provides an API for producing a LogRecord. This record is what the logs are populated with; it contains such properties as the datetime, log level (there are seven currently, from FINEST all the way to SEVERE), and, of course, the message itself.

Formatting and output for log records are done through Formatter and Handler classes. Formatters determine which format the LogRecord will be in, currently either plaintext or XML (the default). Handlers define how the logs are exported (through a file, a socket, to the console, or in-memory). To set up a Handler class to point to a file called “mylog.xml” that writes each LogRecord as an XML message, you’d use the FileHandler class and set its formatter to an instance of an XMLFormatter, as follows:

FileHandler flhandler = new FileHandler( "mylog.xml" );
XMLFormatter xmlfrmtr = new XMLFormatter();
flhandler.setFormatter( xmlfrmtr );

To integrate logging into an application you must obtain a static instance of the Logger class. Through this class a user will define the output type, specify a log format, and publish the individual log records.

To publish an informational level message in the “mylog.xml” file, you first need to retrieve a Logger for your subsystem (commonly the class itself), then attach the handler to it (in our case it’s the FileHandler). Now you’re ready to generate messages.

private static Logger logger =
Logger.getLogger("com.mylogger.TestDriver");
logger.addHandler( flhandler );
logger.info("TestDriver(): Testing INFO");

For the logger.info() method, you can also use the more generic logger.log(), which takes Level as a param, as well as the message:

logger.log(Level.INFO, "TestDriver():
Testing INFO").

The code above will produce an XML <record> message with each field in the LogRecord shown as a separate tag:

<record>
<date>2001-11-20T20:21:03</date>
<millis>1006305663460</millis>
<sequence>0</sequence>
<logger>com.mylogger.TestDriver</logger>
<level>INFO</level>
<class>com.mylogger.TestDriverFile
</class>
<method>main</method>
<thread>10</thread>
<message>TestDriver(): Testing INFO</message>
</record>

A complete example of how logging is added to an application appears in Listing 1. (Listings 1–5 can be downloaded from below.)

What isn’t obvious in this example is the presence of a LogManager. This class contains default configurations for loggers and their supporting classes (read in from a logging.properties file), and is a single global entity shared by each logger within the application. It also maintains a list of global handlers to which, by default, each logger sends its output.

Extending Its Capabilities
As with any thorough framework, the logging package can be extended to increase its capabilities and provide custom functionality on how to format and generate logs. Perhaps e-mailing levels of SEVERE to administrators or providing the ability for other applications to query and report on the logs is necessary. In the latter case we can replace the common log file with a simple database table. I’ll demonstrate how to do this by extending both the Handler and Formatter classes, giving total control of where the database is located and how to write a log record to it.

First, it’s important to understand the class diagram, which shows what we’ll be extending. Referring to Figure 1, you’ll notice an abstract formatter and handler. The formatter decides only how the LogRecord will look. It works on the individual properties of the LogRecord and creates a single string (usually) to be returned that reflects a complete record in a particular format. The handler, which calls the formatter for a LogRecord, worries only about what to do with the outcome of that format’s record when returned.

Figure 1

Take the common FileHandler, for instance. This class specifies the filename to which logs are written, then for each incoming LogRecord calls the default XMLFormatter to retrieve the entire record as an XML document and write it out to the log. With the ease of this framework, it’s only a small change to switch from outputting the XML document from a file to the console, or using a plaintext representation instead of XML.

Database Handler and Customized Formatters
Providing database support to this logging framework will involve implementing only two classes: a formatter to build the SQL necessary to insert an entire LogRecord into the database, and a handler to do the transactions. We’ll also include a third class, another formatter that will split each field in the log record into separate database fields.

As shown in Figure 1, the three new classes are DBHandler, TblXMLFormatter, and TblFieldFormatter. How do they work together? The user instantiates a DBHandler and passes the JDBC driver being used and the appropriate connect string. In this case we’re using MySQL:

DBHandler dbhandler = new DBHandler("org.gjt.mm.mysql.Driver",
"jdbc:mysql://localhost/logger?
user=jim&password=changeme");

Then, instead of using the XmlFormatter, a new TblXmlFormatter is instantiated and attached to the DBHandler. This will return column names and LogRecord values in a format suitable for inclusion in a database transaction.

TblXMLFormatter tblxmlfrmtr = new TblXMLFormatter();
dbhandler.setFormatter( tblxmlfrmtr );

The handler will also write a default table name, which can be changed. The schema of this table depends on which formatter is used. If the user wants the log to consist of XML documents, the TblXmlFormatter is used; this will populate a single column with the entire log (see Figure 2, Logtable). If individual columns have to be populated, the TblFieldFormatter will do the job (see Figure 2, logtable_detail). This class inserts each field of the LogRecord into a separate column and can also control whether a field is written to the table.

Figure 2

Both formatters allow for changing the column names of the table, just as the handler allows for changing the table name. This provides flexibility in the location the logs are written to and allows integration into an existing enterprise schema, if required.

DBHandler
Implementing a new target to send logs to requires developing a new Handler class. For the target of a database table, this class should encapsulate all necessary logic for connecting to the database, inserting formatted log records into the specified table, and handling any SQLExceptions.

For the DBHandler we’ll inherit directly from the Handler base class. This requires minimally implementing the publish(), close(), and flush() methods, as well as a constructor that will set up our connection.

The first step will be designing that constructor. Since we want this class to be database-independent, we’ll need to pass in the JDBC driver that will be used, plus the connect string, including username and password. Some standard configuration activities are required next, such as setting the LogManager and default Level and Formatting. Then the database-specific code is executed, which involves setting class-level variables to the driver and URL, and establishing and holding a connection in another variable.

public DBHandler( String driver, String url )
throws SQLException, ClassNotFoundException, SecurityException {
// … other configuration code here
this.jdbcdriver = driver;
this.jdbcurl = url;
dbconnect(); // connect to the database
}
private void dbconnect()
throws SQLException, ClassNotFoundException {
Class.forName(jdbcdriver);
this.conn = DriverManager.
getConnection(jdbcurl);
}

This connection will be used for each transaction performed by the publish() method, where the bulk of the work is done. This method is responsible for dynamically creating an insert statement from the formatter, executing the transaction, and catching any SQLExceptions.

To do this, it expects the formatter to return both the table column names and the log values. The column names are a static string obtained by the header portion of the formatter (to be discussed in the next section). The log values are obtained by passing in the LogRecord and getting back a single string containing each column’s value. The logic looks like this:

public void publish( LogRecord record ) {
// …
Statement stmt = conn.
createStatement();
String sql = "insert into " + tblname + " (" + frmtr.getHead() + ") values (" + frmtr.format(logrecord) + ")";
stmt.executeUpdate( sql );
stmt.close();
// 
}

Why not pass the entire insert statement back from the format() method? That could be done, but it requires the formatter to know the name of the table. This would break the rules of encapsulation for that class, since the formatter should only work with a LogRecord and not have to worry about the table name or database information.

Another design decision was to stay away from prepared statements. Using them would require an added level of complexity since we’d need to return individual values to set them rather than a single string, which is what format() is designed to do.

The flush() method isn’t applicable to this type of handler, so no implementation is present. For any of the StreamHandlers, however, this would flush the writer. The last required method, close(), will attempt to close the database connection.

A complete listing of this class can be found in Listing 2.

TblXMLFormatter
This formatter will need to produce two columns to incorporate into the SQL generated by the DBHandler. The first column will be the timestamp, which will use the standard System datetime. The second column will be a large string containing the entire XML message.

To extend the formatter requires implementing format() and, optionally, getHeader() and getTail(), which return an empty string by default. Although we can use the base class formatter to create our new class, you’ll notice we’re inheriting directly from the XML Formatter class, so we can take advantage of the code that already produces the XML from the LogRecord.

First, we have the getHeader() method, which is responsible for returning the names and the order of the columns being returned.

String returnColNames = col_time stamp+","+col_msg;

If we use the default column names from this class, the SQL in the DBHandler will go from this:

"insert into LOGTABLE (" + frmtr.getHead() + ") values (" + frmtr.format(logrecord) + ")";

to this:

"insert into LOGTABLE (LOG_TIMESTAMP, LOG_MSG) values (" + frmtr.format(logrecord) + ")"

Next, in the format() method, we call super.format() to extract the XML and concatenate it with the system datetime that will be returned. This matches the order of what getHeader() returns. Also note that we’re using the JDBC escape sequence for the timestamp to ensure database independency.

java.sql.Timestamp tm = new java.sql.Timestamp(System.currentTimeMillis());
// include timestamp, plus xml message from super
String returnVals = "{ts '" + tm.toString() + "'},'"+ super.format(record) + "'";

After format() is called, the final SQL in DBHandler will look similar to this:

"insert into LOGTABLE (LOG_TIMESTAMP, LOG_MSG) values ( {ts ‘2001-11-25 18:25:00.61‘}, ‘<record>xml message elements</record>’)"

The complete code for this class can be found in Listing 3.

TblFieldFormatter
This formatter is provided to give the user the option of using only a selected number of fields from the LogRecord and separating them into individual columns in the database.

As with the TblXMLFormatter, this class also overrides getHeader() and doesn’t use getTail(). Since the behavior of format() is entirely different from SimpleFormatter or XMLFormatter, we extend the Formatter class directly.

For getHeader(), we again return the names and order of the columns being used. Extra logic must also be introduced to provide the flexibility of turning on/off columns, but the order must remain consistent.

The format() method must also apply logic to determine which columns to display, and also to keep the order consistent with what is returned by getHeader(). Unlike TblXMLFormatter, we must manually extract each field from the LogRecord. Following is an example of how to include Level directly from the LogRecord, but only if enabled.

if ( enablecol_level ) returnVals += ",'" + record.getLevel().toString() + "'";

Finally, as with TblXMLFormatter, there are get/set methods that allow changes to the names of the database columns, and an additional “enable” method per column for enabling/disabling their output.

Listing 4 provides the entire code for this class; Listing 5 shows how this formatter is incorporated into our original example application.

Conclusion
By adding the foregoing database capabilities to the new J2SE logging package, we’ve expanded the target of where logs can be stored and increased our options for how to query them. Your logs can now fit into any JDBC-compliant enterprise database schema and use standard database tools to produce reports.

It was my intention in this article to give insight into this framework and outline the steps necessary to customize where your logs go and how they look when they get there. This framework is a powerful utility in the new J2SE, whether used out of the box or customized to your specific requirements. I’ll certainly be watching how it matures over time!

Author Bio
Jim Mangione is director of software engineering for the Report & Query Division of CareScience, located in Philadelphia. Jim has over 12 years’ experience in client/server and Web-based application development and holds a master’s degree in computer science from Drexel University.

[email protected]

Download Source Files (~ 6.37 KB ~Zip File Format)

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.