Externalize and reload grails Log4j configuration

01 / Jul / 2015 by Sandeep Poonia 2 comments

In Grails 2.x and in some earlier versions, the log4j configuration resides in grails-app/conf/Config.groovy. We can modify log4j closure here to add new categories and tweak the log levels.

In development environemnt any changes to log4j closure are loaded automatically without bouncing the application, but not when the application is running from a war.

Our requirement is to have the ability to change log levels and add new categories at runtime.

Depending upon whether we want to use closure based or xml based log4j configuration, we can choose any of the below approaches.

1. Modify web.xml

While creating a war, grails generates web.xml for your application using template. To override the default web.xml, we need to override the template. Run grails install-templates in order to do that. Add two new context-param in src/templates/war/web.xml.

  1. log4jConfigLocation – holds the location of the log4 configuration file
  2. log4jRefreshInterval – defines the refresh interval after which the log4j configuration should be updated

We are going to use the Log4jConfigListener class of spring library instead of Grails log4jConfigListener as Grail’s Log4jConfigListener expects the configuration inside the config.groovy file. Spring’s Log4jConfigListener supports location and interval.

[code lang=”xml”]
<!– log4j parameters –>
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>classpath:log4j.xml</param-value>
</context-param>

<context-param>
<param-name>log4jRefreshInterval</param-name>
<param-value>10000</param-value>
</context-param>

<!– Register Spring’s Log4jConfigListener–>
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
[/code]

2. Using Bean Injection

[code lang=”groovy”]
import org.springframework.beans.factory.config.MethodInvokingFactoryBean

beans = {
log4jConfigurer(MethodInvokingFactoryBean) {
targetClass = "org.springframework.util.Log4jConfigurer"
targetMethod = "initLogging"
arguments = ["classpath:log4j.xml", 10000]//specify config location and refresh interval
}
}
[/code]

3. Custom approach

In approach 1 & 2, we can use .xml or .properties file. But if you are a grails lover and would like to use the same closure based syntax to define loggers then we can do this. Benefit of this approach is that it can load configuration from multiple files.

Specify external configuration files in grails.config.locations closure.

[code lang=”groovy”]
grails.config.locations = [
DataSourceConfig,
CommonConfig,
RabbitMQConfig,
"classpath:CommonLoggerConfig.groovy"
"file:${userHome}/.grailsConfigurations/${appName}/LoggerConfig.groovy"
]
[/code]

Now we can write a job that will periodically check for the changes in config files and will reload log4j configuration, if there are any. It will first update the configuration object and then will re-configure the log4j.

Here we are using the quartz api to do that. Create a new job ConfigurationReloaderJob.

[code lang=”groovy”]
import grails.util.Environment
import grails.util.Holders
import org.codehaus.groovy.grails.plugins.log4j.Log4jConfig

class ConfigurationReloaderJob {
static triggers = {
//Start the job 5 minutes after the application starts up and execute after every 1 minute
simple name: "ConfigurationReloaderJob", startDelay: 5 * 60 * 1000l, repeatInterval: 1 * 60 * 1000l
}

//to store the hash of external config files
private static Map<String, Integer> fileHashMap = [:]

def execute() {
ConfigObject config = Holders.config
List locations = config.grails.config.locations

int hashCode
String text

locations.findAll {
//find all the external file locations(contains classpath: or file: in their name)
return (it instanceof String || it instanceof GString) && (it.toString().contains("classpath:") || it.toString().contains("file:"))
}.each {
String filePath ->
try {
//read contents of file
text = Holders.applicationContext.getResource(filePath)?.file?.text
} catch (FileNotFoundException ex) {
//ignore the exception if file not found on specified path
log.error("File not found: ${ex.message}")
}

//if file have data
if (text) {
//calculate hashCode of text
hashCode = text.hashCode()
if (!fileHashMap.containsKey(filePath) || fileHashMap.get(filePath) != hashCode) {
//Merger existing config with the text of file
config = config.merge(new ConfigSlurper(Environment.current.name).parse(text))

//reconfigure log4j
Log4jConfig.initialize(config)

fileHashMap.put(filePath, hashCode)
log.debug("Log configuration updated successfully.")
}
}
}
}
}
[/code]

Hooking into Grails Events

As we are specifying that the configurations will be loaded from the classpath or system path, we will be hooking into the Grails events to make sure that the file is in the classpath. Create a file under scripts/_Events.groovy and paste the code given below.

[code lang=”groovy”]
//Hook into war creation event
eventCreateWarStart = { warName, stagingDir ->
ant.copy(todir: "${stagingDir}/WEB-INF/classes") {
fileset(file: "${basedir}/log4j.properties")
}
}

//Hook into run-app event
eventRunAppStart = {
ant.copy(todir:"${basedir}/target/classes") {
fileset(file: "${basedir}/log4j.properties")
}
}
[/code]

You can modify the above code to work with multiple files or to copy files at file system path as specified in grails.config.locations.

Changes to config.groovy

In grails-app/conf/config.groovy, remove the log4j closure at the end of the file.

FOUND THIS USEFUL? SHARE IT

comments (2)

  1. Bobby

    how can tomcat know “classpath:${appName}-config.groovy” location?

    i am trying “classpath:external-config.groovy”..i am using server linux..

    Reply
    1. Sandeep Poonia

      It’s not the tomcat that needs to understand it. Grails will parse the config.groovy file and will fetch the config from associated external file.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *