Search code examples
grailslogbacklogback-groovygrails3

Is there a way to inject a Grails datasource into logback.groovy for use with DBAppender?


So, Grails already sets up datasources backed by connection pools. Is there a way to leverage those for use with DBAppender in Logback so that I don't have to create a separate parallel datasource/connection pool?

logback.groovy is somewhat external to Grails, so it doesn't accept Spring autowiring, and other tricks like grails.util.Holders.findApplication() don't appear to work.


Solution

  • Woof, this was a chore. Frankly, I'm a bit disillusioned with Logback. Logback creates it's own Spring ApplicationContext. So we have two separate contexts. Ugh. Also, it certainly doesn't help that the DSL that Logback uses for configuring Spring in Groovy is different than Grails'.

    Since Logback gets fired up before Grails is completely started, we need to tell Logback to create some dummy appenders that will just store log messages until we fire up appenders from within Grails. We use the logback extension for Spring to do this.

    build.gradle:

    compile 'org.logback-extensions:logback-ext-spring:0.1.4'
    

    logback.groovy:

    import ch.qos.logback.ext.spring.DelegatingLogbackAppender
    
    appender('DB', DelegatingLogbackAppender)
    appender('STDOUT', DelegatingLogbackAppender)
    

    resources.groovy:

    import ch.qos.logback.ext.spring.ApplicationContextHolder
    import ch.qos.logback.classic.encoder.PatternLayoutEncoder
    import ch.qos.logback.classic.db.DBAppender
    import ch.qos.logback.core.ConsoleAppender
    import ch.qos.logback.core.db.DataSourceConnectionSource
    import org.slf4j.LoggerFactory
    
    beans = {
        applicationContextHolder(ApplicationContextHolder)
    
        loggerContext(LoggerFactory) { bean ->
            bean.factoryMethod = "getILoggerFactory"
        }
    
        patternLayoutEncoder(PatternLayoutEncoder) { bean ->
            bean.initMethod = 'start'
            bean.destroyMethod = 'stop'
    
            context = ref(loggerContext)
            pattern = "%level %logger - %msg%n"
        }
    
       STDOUT(ConsoleAppender) { bean ->
            bean.initMethod = 'start'
            bean.destroyMethod = 'stop'
            context = ref(loggerContext)
            encoder = ref(patternLayoutEncoder)
        }
    
        connectionSource(DataSourceConnectionSource) { bean ->
            bean.initMethod = 'start'
            bean.destroyMethod = 'stop'
            context = ref(loggerContext)
            dataSource = ref(dataSource)
        }
    
        DB(DBAppender) { bean ->
            bean.initMethod = 'start'
            bean.destroyMethod = 'stop'
            context = ref(loggerContext)
            connectionSource = ref(connectionSource)
        }
    }
    

    The ref(dataSource) in the DataSourceConnectionSource references the dataSource you have configured in application.yml or application.groovy.

    Say you have multiple dataSources (or even one configured just for logback called dataSources.logging. In that case the bean reference would be dataSource_logging. The default dataSource in that case (called dataSources.dataSource bean reference is just dataSource. Took me a while to figure that one out.

    All in all, i miss the days of configuring Log4j from within the Grails config file using the Grails DSL. I get that separating logging from Grails means one less thing for Graeme and the Grails team to deal with, but this was a major PITA for something that I thought would be common. ¯\_(ツ)_/¯