Friday, April 18, 2008

Logger name based filtering in Logback?

While reading through the Logback manual chapter on filters, I realized that there is no simple possibility to filter by logger name (except employing Janino, which seemed to be overkill for this task).

So I wrote one myself that does the job:


/***
* Coalevo Project
* http://www.coalevo.net
*
* (c) Verein zur Foerderung der Internetkommunikation, Austria
* http://www.vfi.or.at
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***/
package net.coalevo.logging.filter;

import ch.qos.logback.core.filter.AbstractMatcherFilter;
import ch.qos.logback.core.spi.FilterReply;
import ch.qos.logback.classic.spi.LoggingEvent;

/**
* This class implements a filter that will match logger names.
* <p/>
* Kind of a standard filtering I always used in log4j.
*
* @author Dieter Wimberger (wimpi)
* @version 1.0 (18/Apr/2008)
*/
public class LoggerNameFilter extends AbstractMatcherFilter {

private String m_LoggerName;

public FilterReply decide(Object o) {
LoggingEvent event = (LoggingEvent)o;

if (event.getLoggerRemoteView().getName().startsWith(m_LoggerName)) {
return onMatch;
} else {
return onMismatch;
}
}//decide

public void setLoggerName(String loggerName) {
m_LoggerName = loggerName;
}//setLoggerName

public String getLoggerName() {
return m_LoggerName;
}//getLoggerName

public void start() {
if (m_LoggerName != null) {
super.start();
}
}//start

}//class LoggerNameFilter


It can be configured within an appender section using:

<filter class="net.coalevo.logging.filter.LoggerNameFilter">
<LoggerName>Logger name</LoggerName>
<OnMismatch>NEUTRAL</OnMismatch>
<OnMatch>DENY</OnMatch>
</filter>


Enjoy!

PS: Yes, technically it will match all loggers from the given hierarchy. i.e. it's a prefix matching, but if you need other behavior it should be rather simple to modify the filter.

Sunday, April 13, 2008

Hack - How to use Logback JoranConfigurator in OSGi avoiding JAXP

I have recently started to work a little bit with Logback, to see whether I could overhaul and enhance Coalevo logging completely; primarily because it really never was a good idea to use Commons-Logging in the first place.

Now, the Logback manual Ch. 3, about Joran configuration, suggests basically to use:

LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();

try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(lc);
lc.shutdownAndReset();
configurator.doConfigure($source$);
} catch (JoranException je) {
StatusPrinter.print(lc);
}


where I use $source$ for the possible options in Logback, which are basically InputSource, a File, a String specifying a filename, an URL, and last but not least (as we will see) a List<SaxEvent>.

Investigating a little bit, all these methods, with exception of the last (i.e. List<SaxEvent>), use SaxEventRecorder to obtain List<SaxEvent>:

public List recordEvents(InputSource inputSource)
throws JoranException {
SAXParser saxParser = null;
try {
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setValidating(false);
spf.setNamespaceAware(true);
saxParser = spf.newSAXParser();
} catch (Exception pce) {
String errMsg = "Parser configuration error occured";
addError(errMsg, pce);
throw new JoranException(errMsg, pce);
}

try {
saxParser.parse(inputSource, this);
return saxEventList;

} catch (IOException ie) {
String errMsg = "I/O error occurred while parsing xml file";
addError(errMsg, ie);
throw new JoranException(errMsg, ie);
} catch (Exception ex) {
String errMsg = "Problem parsing XML document. See previously reported errors. Abandoning all further processing.";
addError(errMsg, ex);
throw new JoranException(errMsg, ex);
}
}//recordEvents


The problem for an OSGi based use is simple; it's not recommendable to simply use JAXP, namely this:

SAXParserFactory spf = SAXParserFactory.newInstance();


The problem is related to the fact that the JAXP discovery mechanism assume visibility of the parser through the context ClassLoader of the actual thread, which may cause some troubles.

Thus, it is suggested to acquire the SAXParserFactory from a specific bundle, using the ServiceRegistry.

Now, to use the JoranConfigurator in an OSGi environment (e.g. a Log bundle), it needs a little trick to ensure that the SAXParserFactory reference is obtained the "OSGi way".

The mechanism I am proposing here is to simply extend and override the SaxEventRecorder:


private static class MySaxEventRecorder extends SaxEventRecorder {

public List recordEvents(InputSource inputSource)
throws JoranException {
SAXParser saxParser = null;
try {
saxParser = c_Services.getSAXParserFactory(ServiceMediator.WAIT_UNLIMITED).newSAXParser();
} catch (Exception pce) {
String errMsg = "Parser configuration error occured";
addError(errMsg, pce);
throw new JoranException(errMsg, pce);
}

try {
saxParser.parse(inputSource, this);
return saxEventList;

} catch (IOException ie) {
String errMsg = "I/O error occurred while parsing xml file";
addError(errMsg, ie);
throw new JoranException(errMsg, ie);
} catch (Exception ex) {
String errMsg = "Problem parsing XML document. See previously reported errors. Abandoning all further processing.";
addError(errMsg, ex);
throw new JoranException(errMsg, ex);
}

}
}//MySaxEventRecorder


This line here:

saxParser = c_Services.getSAXParserFactory(ServiceMediator.WAIT_UNLIMITED).newSAXParser();

will obtain a SAXParserFactory the way I described in this blog entry about mediating OSGi services.

You may also use an alternative, like:

private SAXParserFactory getParserFactory()
throws Exception {
ServiceReference refs[] = m_BundleContext.getServiceReferences(
SAXParserFactory.class.getName(),
"(&(parser.namespaceAware=true)"
+ "(parser.validating=true))");
if (refs == null) {
throw new Exception("Could not obtain SAX Parser Factory.");
}
return (SAXParserFactory) m_BundleContext.getService(refs[0]);
}//getParserFactory


The last piece that is missing is to change the configuration to work this way:

try {
SaxEventRecorder recorder = new MySaxEventRecorder();
recorder.setContext(c_LoggerContext);
recorder.recordEvents(new InputSource(new FileInputStream(f)));
} catch (JoranException je) {
StatusPrinter.print(c_LoggerContext);
}
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(c_LoggerContext);
Activator.getLoggerContext().shutdownAndReset();
configurator.doConfigure(f);


...and voila! Logback is configured with a SAXParser from a SAXParserFactory obtained the "OSGi way".