How to separate log files per application in JBoss Enterprise Application Platform (EAP) 6

Solution Unverified - Updated -

Environment

  • Red Hat JBoss Enterprise Application Platform (EAP)
    • 6.0.x
    • 6.1.x
    • 6.2.x
    • 6.3.x

Issue

  • How do I separate log files per application in JBoss EAP 6?
  • There was way to do this in JBoss EAP 5 using TCLMFilter, also referenced on the JBoss wiki. Can I use this same filter in EAP 6?

Resolution

There isn't an equivalent of the TCLMFilter class in EAP6, but there are currently a few alternatives available.

  1. A new feature called "Logging Profiles" is implemented in JBoss EAP 6.1.0. Refer to this knowledgebase solution for instructions on utilizing logging profiles.

  2. Create a file handler for each application and add a category to the file handler.

  3. Package a separate logging.properties (similar to the one in standalone/configuration) with each application/deployment. The only downside of this is that this creates a separate logging context for this application, thus making the deployment's logging unable to be managed through any of the JBoss management interfaces.

  4. Implement a custom logging handler that utilizes JBoss Modules to separate log messages according to which deployment is currenly executing.

Options 1, 2, and 3 have the disadvantage that you must maintain at least one handler for each application, whether this be included in the application or the server configuration.

How to create the custom logger

Below are the steps to create a basic custom handler that utilizes JBoss Modules to seprate log messages by which deployment logged the message.

Step 1: Write the custom handler class. Below is a basic example. When compiling this class, keep these two jars in the CLASSPATH:

  • $JBOSS_HOME/modules/.../org/jboss/logmanager/main/jboss-logmanager-1.3.2.Final-redhat-1.jar
  • $JBOSS_HOME/jboss-modules.jar.

After compiling this class, package it in a jar (e.g. tcclHandler.jar).

Note: The following code is not supported by the Red Hat. This is a just an example to achieve what you want.

package com.redhat.gss.logging;

import org.jboss.modules.ModuleClassLoader;
import org.jboss.logmanager.ExtHandler;
import org.jboss.logmanager.ExtLogRecord;
import org.jboss.logmanager.handlers.FileHandler;
import java.util.logging.Handler;
import java.util.HashMap;
import java.util.Map;
import java.io.File;
import java.util.List;
import java.util.ArrayList;
import java.io.FileNotFoundException;

/*
 * Logs message from each application to a different log file.
 * If logs aren't from an application, it logs to server.log
 */
public class TcclHandler extends ExtHandler {
  private static final String SERVER = "server";
  private static final Map<String, FileHandler> fileHandlers = new HashMap<String, FileHandler>();
  private static Map<String, String> deploymentMap = null;
  private ThreadLocal<Boolean> invoked = new ThreadLocal<Boolean>() {
    protected Boolean initialValue() {
      return Boolean.FALSE;
    }
  };
  private Object lock = new Object();

  private String dirName = null;

  public String getLogNames() {
    StringBuilder builder = new StringBuilder();
    for(String key : deploymentMap.keySet()) {
      String logName = deploymentMap.get(key);
      builder.append(key).append(":").append(logName).append(",");
    }
    builder.deleteCharAt(builder.length()-1);
    return builder.toString();
  }

  public void setLogNames(String logNames) {
    this.deploymentMap = new HashMap<String, String>();
    for(String stringPair : logNames.split(",")) {
      //Example format
      //"myDeployment:funName"
      //That takes myDeployment.war and writes it to funName.log
      String[] pair = stringPair.split(":");
      if(pair.length != 2)
        throw new IllegalStateException("Invalid format of log name string");
      deploymentMap.put(pair[0], pair[1]);
    }
  }

  @Override
  protected void doPublish(final ExtLogRecord record) {
    synchronized(lock) { //probly not necessary
      if(invoked.get().equals(Boolean.TRUE)) {
        return;
      } else {
          invoked.set(Boolean.TRUE);
      }
    }

    String deployment = getDeploymentName();

    // Did we configure a custom log name for this deployment?
    String logName = deploymentMap.get(deployment);
    if(logName == null) {
      logName = deployment; // default log name
    }

    FileHandler fileHandler = fileHandlers.get(logName);
    if(fileHandler == null) {
      fileHandler = createFileHandler(logName);
    }
    fileHandler.publish(record);

    if(!fileHandler.isAutoFlush()) {
      fileHandler.flush();
    }

    synchronized(lock) {
      invoked.set(Boolean.FALSE);
    }
  }

  private FileHandler createFileHandler(String logName) {
    try {
      File f = new File(dirName + File.separator + logName + ".log");
      FileHandler newHandler = new FileHandler(getFormatter(), f);
      fileHandlers.put(logName, newHandler);
      return newHandler;
    } catch(FileNotFoundException e) {
      throw new IllegalStateException("Cannot create log file", e);
    }
  }

  private static String getDeploymentName() {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();

    if(cl instanceof ModuleClassLoader) {
      ModuleClassLoader mcl = (ModuleClassLoader)cl;
      String moduleName = mcl.getModule().getIdentifier().toString();
      if(moduleName.startsWith("deployment")) {
        List<String> segments = new ArrayList<String>();
        for(String segment : moduleName.split("\\.")) {
          segments.add(segment);
        }
        // Remove 'deployment' from the beginning and the suffix from the end
        segments.remove(0);
        segments.remove(segments.size()-1);
        StringBuilder builder = new StringBuilder();
        for(String segment : segments) {
          builder.append(segment + ".");
        }
        builder.deleteCharAt(builder.length()-1);
        return builder.toString();
      } else {
        return SERVER;
      }
    } else { // Not instanceof ModuleClassLoader
      return SERVER;
    }
  }

  public String getDirName() {
    return this.dirName;
  }

  public void setDirName(String dirName) {
    this.dirName = dirName;
  }
}

Step 2: Create a module named e.g. com.redhat.gss.logging ($JBOSS_HOME/modules/com/redhat/gss/logging/main) and put tcclHandler.jar into the main folder.

CLI:

module add --name=com.redhat.gss.logging --dependencies=org.jboss.modules,org.jboss.logmanager --resources=/path-to/tcclHandler.jar

XML Configuration :

Create directory $JBOSS_HOME/modules/com/redhat/gss/logging/main
Create $JBOSS_HOME/modules/com/redhat/gss/logging/main/module.xml with the following content

<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="com.redhat.gss.logging">
    <dependencies>
        <module name="org.jboss.modules"/>
        <module name="org.jboss.logmanager"/>
    </dependencies>
    <resources>
        <resource-root path="tcclHandler.jar"/>
    </resources>
</module>

Step 3: Define the custom handler in the standalone.xml as following.

CLI :

/subsystem=logging/custom-handler=TCCL:add(name=TCCL,class=com.redhat.gss.logging.TcclHandler,module=com.redhat.gss.logging,formatter="%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n", properties={"dirName" => "/path/to/jboss-eap-6.0.1/standalone/log","logNames" => "WebOne:WebOneLog,WebTwo:WebTwoLog"})

XML Configuration:

<custom-handler name="TCCL" class="com.redhat.gss.logging.TcclHandler" module="com.redhat.gss.logging">
   <formatter>
      <pattern-formatter pattern="%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n"/>
   </formatter>
   <properties>
      <property name="dirName" value="/path/to/jboss-eap-6.0.1/standalone/log"/>
      <property name="logNames" value="WebOne:WebOneLog,WebTwo:WebTwoLog"/>  
  </properties>
</custom-handler>

Here /path/to/jboss-eap-6.0.1/standalone/log is the directory name of the log file. logNames allows you to map a deployment's log messages to a particular log name. For example in WebOne:WebOneLog, WebOne is the name of the application (without the file suffix), and WebOneLog is the name of log file. In the above, any log messages coming from WebOne.war will be directed to "standalone/log/WebOneLog.log".

Step 4: Start using the handler by referencing it from a logger. It's recommended to add it to your root logger, but it could be added to a set of loggers to only utilize this handler for certain loggers and/or Java package names.

See the online logging documentation for more details

Root Cause

JBoss EAP 6 is based upon JBoss Modules, which introduces a very different classloading model, making TCLMFilter ineffective at utilizing the thread-context classloader (TCCL) to figure out which logging messages should be allowed or revoked.

This solution is part of Red Hat’s fast-track publication program, providing a huge library of solutions that Red Hat engineers have created while supporting our customers. To give you the knowledge you need the instant it becomes available, these articles may be presented in a raw and unedited form.

Comments