When you move to more asynchronous processing with Grails (e.g. as with quartz jobs), there’s no user-visible error when something goes wrong. There’s only the logs, and those aren’t the most prominent place for putting failures of mission-cricital jobs.
In a nutshell
- there’s a Grails domain that holds the log data
- a custom log4j appender writes to that domain
- the appender is configured in Config.groovy
- logging to the database like this only works after Bootstrap.init has run
So here’s the code for directly logging to the database with Grails. For data storage, we use a Grails domain which holds the data (you could use log4j’s JDBCAppender as well, but we’re in the Grails world …)
/**
* Contains log entries that the tech staff should have a look at
*/
class EventLog {
final static int SOURCE_MAXSIZE = 255
final static int MESSAGE_MAXSIZE = 1000
final static int DETAILS_MAXSIZE = 4000
Date dateCreated
String message
String details
String source
// did someone look at this error?
boolean cleared = false
static constraints = {
source(blank: false, nullable: false, maxSize: SOURCE_MAXSIZE)
message(blank: false, nullable: false, maxSize: MESSAGE_MAXSIZE)
details(blank: true, nullable: true, maxSize: DETAILS_MAXSIZE)
}
static mapping = {
sort "dateCreated"
}
}
This EventLog is written to by a custom log4 appender, the EventLogAppender.
Because Grails’ database access is only available from a certain point after the start of the application, database logging is only attempted when the application is initialized.
/**
* Log4j appender that writes its entries to the EventLog
*/
class EventLogAppender extends org.apache.log4j.AppenderSkeleton
implements org.apache.log4j.Appender {
static appInitialized = false
String source
@Override
protected void append(LoggingEvent event) {
if (appInitialized) {
//copied from Log4J's JDBCAppender
event.getNDC();
event.getThreadName();
// Get a copy of this thread's MDC.
event.getMDCCopy();
event.getLocationInformation();
event.getRenderedMessage();
event.getThrowableStrRep();
def limit = { string, maxLength -> string.substring(0, Math.min(string.length(), maxLength))}
String logStatement = getLayout().format(event);
// use new transaction so that the log entry will be written even if the currently running transaction is rolled back
EventLog.withNewTransaction {
EventLog eventLog = new EventLog()
eventLog.message = "Log4 Error Log"
eventLog.details = limit((logStatement ?: "Not details available, something is wrong"), EventLog.DETAILS_MAXSIZE)
eventLog.source = limit(source ?: "Source not set", EventLog.SOURCE_MAXSIZE)
eventLog.save()
}
}
}
/**
* Set the source value for the logger (e.g. which application the logger belongs to)
* @param source
*/
public void setSource(String source) {
this.source = source
}
@Override
void close() {
//noop
}
@Override
boolean requiresLayout() {
return true
}
}
When the init closure in Bootstrap.groovy runs, you can be sure that you can write to the database, thus enable the logger there.
class BootStrap {
def init = { servletContext ->
EventLogAppender.appInitialized = true
//...
}
}
Last but not least, you need to enable the custom logger in Config.groovy:
log4j = {
appenders {
//EnhancedPatternLayout is needed in order to support the %throwable logging of stacktraces
appender new EventLogAppender(source:'YourApp', name: 'eventLogAppender', layout:new EnhancedPatternLayout(conversionPattern: '%d{DATE} %5p %c{1}:%L - %m%n %throwable{500}'), threshold: org.apache.log4j.Level.ERROR)
console name:'stdout'
}
root {
error 'eventLogAppender'
info 'stdout'
}
// ... more logging config
}
Now you’re up and running with a custom database logger.