Version 2

    With the fresh release of JBoss AS 7.0.1 comes the feature of being able to use custom log handlers in addition to those provided by the server. This article shows with the example of a JDBC-based handler how you could write your own handlers. I have used an Oracle DB for testing this so it would be nice if people would test this on other flavors, too, so we can find common ground where it works in as many places as possible

     

    We will start bottom-up by looking at the handler. All custom handlers must extend the java.util.logging.Handler

     

     

    public class JdbcLogger extends Handler
    {
              private static final Object PARAM_TIMESTAMP = "$TIMESTAMP";
              private static final Object PARAM_LEVEL = "$LEVEL";
              private static final Object PARAM_MESSAGE = "$MESSAGE";
              private static final String PARAM_MDC = "$MDC[";
              private static final Object PARAM_PASSTROUGH = "?";
    
    
              private String driverClassName;
              private String jdbcUrl;
              private String username;
              private String password;
              private String insertStatement;
    
    
              private Connection connection;
    
    
              private List<String> parameters = new ArrayList<String>();
    
    
              @Override
              public void publish(LogRecord record)
              {
                        if (!ensureReady())
                        {
                                  return;
                        }
                        try
                        {
                                  insertRecord(record);
                        }
                        catch (SQLException e)
                        {
                                  e.printStackTrace();
                        }
              }
    
    
              private synchronized boolean ensureReady()
              {
                        if (connection != null)
                        {
                                  return true;
                        }
                        try
                        {
                                  parseStatementParameters();
                                  setupConnection();
                        }
                        catch (ClassNotFoundException e)
                        {
                                  e.printStackTrace();
                                  return false;
                        }
                        catch (SQLException e)
                        {
                                  e.printStackTrace();
                                  return false;
                        }
                        return true;
              }
    
    
              private void parseStatementParameters()
              {
                        int paramsStart = insertStatement.indexOf("(");
                        int paramsStop = insertStatement.lastIndexOf(")");
                        String paramsString = insertStatement.substring(paramsStart + 1, paramsStop);
                        for (String param : paramsString.split(","))
                        {
                                  parameters.add(param.trim());
                                  paramsString = paramsString.replace(param, "?");
                        }
                        insertStatement = String.format("%s(%s)", insertStatement.substring(0, paramsStart), paramsString);
              }
    
    
              private void setupConnection() throws ClassNotFoundException, SQLException
              {
                        Class.forName(driverClassName);
                        connection = DriverManager.getConnection(jdbcUrl, username, password);
              }
    
    
              private void insertRecord(LogRecord logRecord) throws SQLException
              {
                        PreparedStatement statement = null;
                        try
                        {
                                  statement = connection.prepareStatement(insertStatement);
                                  setStatementParameters(statement, logRecord);
                                  statement.executeUpdate();
                        }
                        finally
                        {
                                  if (statement != null)
                                  {
                                            statement.close();
                                  }
                        }
              }
    
    
              private void setStatementParameters(PreparedStatement statement, LogRecord logRecord) throws SQLException
              {
                        for (int i = 0; i < parameters.size(); i++)
                        {
                                  statement.setObject(i + 1, getParameterValue(i, logRecord));
                        }
              }
    
    
              private Object getParameterValue(int i, LogRecord record)
              {
                        String parameter = parameters.get(i);
                        if (PARAM_PASSTROUGH.equals(parameter))
                        {
                                  return null;
                        }
                        if (PARAM_TIMESTAMP.equals(parameter))
                        {
                                  return new Date(record.getMillis());
                        }
                        else if (PARAM_LEVEL.equals(parameter))
                        {
                                  return record.getLevel().toString();
                        }
                        else if (PARAM_MESSAGE.equals(parameter))
                        {
                                  return getFormatter().format(record);
                        }
                        else if (parameter.startsWith(PARAM_MDC))
                        {
                                  int startIndex = parameter.indexOf("[") + 1;
                                  int stopIndex = parameter.indexOf("]") - 1;
                                  String key = parameter.substring(startIndex, stopIndex + 1);
                                  return MDC.get(key);
                        }
                        else
                        {
                                  return parameter;
                        }
              }
    
    
              @Override
              public void flush()
              {
              }
    
    
              @Override
              public void close()
              {
                        if (connection != null)
                        {
                                  try
                                  {
                                            connection.close();
                                  }
                                  catch (SQLException e)
                                  {
                                            e.printStackTrace();
                                  }
                        }
              }
    
    
              public void setDriverClassName(String driverClassName)
              {
                        this.driverClassName = driverClassName;
              }
    
    
              public void setJdbcUrl(String jdbcUrl)
              {
                        this.jdbcUrl = jdbcUrl;
              }
    
    
              public void setUsername(String username)
              {
                        this.username = username;
              }
    
    
              public void setPassword(String password)
              {
                        this.password = password;
              }
    
    
              public void setInsertStatement(String insertStatement)
              {
                        this.insertStatement = insertStatement;
              }
    }
    
    

     

    A quick overview of the main loop of the class, the publish(LogRecord):

    • Log request comes in
    • We check that the logger is set up (connection setup on-demand), the parameters of the insertStatement are placed in a list and the statement is converted to ?,?,? format. We also set up the database connection
    • We prepare a statement and for each parameter, fetch the real value and set the parameter. After that, the statement is executed.

     

    This handler takes 5 parameters:

     

    1. driverClassName: the FQCN of the database driver class
    2. jdbcUrl: the JDBC url to the database
    3. usename: the username of the database account
    4. password: the password of the database account
    5. insertStatement: the insert statement to use

     

    Parameters 1-4 are pretty self-explanatator so let's look at the insertStatement. An example would be

     

     

    insert into log_table values (?, $TIMESTAMP, $LEVEL, $MDC[ip], $MDC[user], $MESSAGE, hardcoded)
    

     

    the ? means a null value (useful for Oracle-style insert-triggers handling primary key generation), $TIMESTAMP, $LEVEL and $MESSAGE are the correspoding pieces of information available from the LogRecord being handled and the $MDC is a lookup from the MDC (Mapped Diagnostics Context). All other text will be inserted as-is.

     

    Next we will package the class into a JAR file (dblogger.jar) and make a module for it under $JBOSS_HOME/modules. Take a look at the JBoss Modules or AS documentation for more information, we'll be making a "com.acme.dblogger" module and in the main/module.xml (with the dblogger.jar we created in the same directory) we'll have

     

     

    <module xmlns="urn:jboss:module:1.0" name="com.acme.dblogger">
      <resources>
        <resource-root path="dblogger.jar"/>
            <!-- Insert resources here -->
      </resources>
      <dependencies>
        <module name="javax.api"/>
        <module name="org.jboss.logging"/>
        <module name="com.oracle.db"/>
      </dependencies>
    </module>
    
    

     

    You'll notice that I'm using the "com.oracle.db" module I've created for the Oracle driver, you need similar module for your own database flavor. It might also be possible to pack the driver in the same module and list it in the resources but you probably need the driver for your application in any case. I don't think deploying the driver jar will work since I don't think the custom logger have any relationship to the deployment so it might not see it.

     

    My oracle module.xml looks like

     

     

    <module xmlns="urn:jboss:module:1.0" name="com.oracle.db">
      <resources>
        <resource-root path="ojdbc6.jar"/>
      </resources>
      <dependencies>
        <module name="javax.api"/>
        <module name="javax.transaction.api"/>
      </dependencies>
    </module>
    
    

     

    Now we have a logger module, let's put it into use. Edit $JBOSS_HOME/configuration.xml, look up the other loggers and place it at the same level as

     

     

                <custom-handler name="DB" class="com.acme.dblogger.JdbcLogger" module="com.acme.dblogger">
                    <level name="INFO"/>
                    <formatter>
                        <pattern-formatter pattern="%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n"/>
                    </formatter>
                    <properties>
                        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
                        <property name="jdbcUrl" value="jdbc:oracle:thin:@host:1521:sid"/>
                        <property name="username" value="scott"/>
                        <property name="password" value="tiger"/>
                        <property name="insertStatement" value="insert into log_table values (?, $TIMESTAMP, $LEVEL, $MDC[ip], $MDC[user], $MESSAGE, hardcoded)"/>
                    </properties>
                </custom-handler>
    
    

     

    and hook it up under the root logger with

     

     

                <root-logger>
                    <level name="INFO"/>
                    <handlers>
                        <handler name="CONSOLE"/>
                        <handler name="FILE"/>
                        <handler name="DB"/>
                    </handlers>
                </root-logger>
    
    

     

    or use it as any other logger.

     

    As you can see from error handling, parsing validation etc this is only a proof-of-concept that shows how to hook things up. As of writing, there is still a bug in the custom handler that cause logging to loop if you use e.printStackTrace or System.out.println. I also tried making a version that uses BoneCP as connection pool but there were some classloading issues that prevented me from instantiating it.