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.
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.
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.
jmangione@carescience.com
Download Source Files (~ 6.37 KB ~Zip File Format)