I don't think you really want an Appender
here. It would be simpler to write a Filter
instead. For reference you can find information regarding creating extensions for log4j2 on the Extending Log4j2 page of the manual
In the example below I created a simple filter that matches when the log event has a Throwable
associated with it and mismatches when there is no Throwable
(i.e. the Throwable
is null or the log was generated from a method call that did not include a Throwable
parameter).
In the example I send all matching log events to a simple file appender to illustrate that in fact it does capture only events with a Throwable
. You could modify this filter to do whatever you need. You could change it to always be NEUTRAL
to every event, but when a non-null Throwable
is found it then triggers some special logic to send that Throwable
to your external tool. Basically you would be using the filter as more of an interceptor. I will describe that change at the end.
First, here's some basic Java code to generate some logging including a log event that has a Throwable
.
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class SomeClass {
private static final Logger log = LogManager.getLogger();
public static void main(String[] args){
if(log.isDebugEnabled())
log.debug("This is some debug!");
log.info("Here's some info!");
log.error("Some error happened!");
try{
specialLogic();
}catch(RuntimeException e){
log.error("Woops, an exception was detected.", e);
}
}
public static void specialLogic(){
throw new RuntimeException("Hey an exception happened! Oh no!");
}
}
Next, here's the class I call ThrowableFilter
:
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.filter.AbstractFilter;
import org.apache.logging.log4j.message.Message;
@Plugin(name = "ThrowableFilter", category = "Core", elementType = "filter", printObject = true)
public final class ThrowableFilter extends AbstractFilter {
private ThrowableFilter(Result onMatch, Result onMismatch) {
super(onMatch, onMismatch);
}
public Result filter(Logger logger, Level level, Marker marker, String msg, Object[] params) {
return onMismatch;
}
public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
return filter(t);
}
public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
return filter(t);
}
@Override
public Result filter(LogEvent event) {
return filter(event.getThrown());
}
private Result filter(Throwable t) {
return t != null ? onMatch : onMismatch;
}
/**
* Create a ThrowableFilter.
* @param match The action to take on a match.
* @param mismatch The action to take on a mismatch.
* @return The created ThrowableFilter.
*/
@PluginFactory
public static ThrowableFilter createFilter(@PluginAttribute(value = "onMatch", defaultString = "NEUTRAL") Result onMatch,
@PluginAttribute(value = "onMismatch", defaultString = "DENY") Result onMismatch) {
return new ThrowableFilter(onMatch, onMismatch);
}
}
Finally, here is the log4j2.xml configuration file I used to test this:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<File name="ExceptionFile" fileName="logs/exception.log" immediateFlush="true"
append="true">
<ThrowableFilter onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout
pattern="%d{yyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</File>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console" />
<AppenderRef ref="ExceptionFile" />
</Root>
</Loggers>
</Configuration>
Running the logic in SomeClass
produces the following output:
On the console:
23:23:25.931 [main] DEBUG example.SomeClass - This is some debug!
23:23:25.946 [main] INFO example.SomeClass - Here's some info!
23:23:25.946 [main] ERROR example.SomeClass - Some error happened!
23:23:25.946 [main] ERROR example.SomeClass - Woops, an exception was detected.
java.lang.RuntimeException: Hey an exception happened! Oh no!
at example.SomeClass.specialLogic(SomeClass.java:25) ~[classes/:?]
at example.SomeClass.main(SomeClass.java:18) [classes/:?]
In the logs/exception.log file:
2019-03-06 23:23:25.946 [main] ERROR example.SomeClass - Woops, an exception was detected.
java.lang.RuntimeException: Hey an exception happened! Oh no!
at example.SomeClass.specialLogic(SomeClass.java:25) ~[classes/:?]
at example.SomeClass.main(SomeClass.java:18) [classes/:?]
Now to change the filter to act more as an interceptor you could alter the following methods:
//Remove parameters from constructor as they will not be used.
private ThrowableFilter() {
super();
}
...
public Result filter(Logger logger, Level level, Marker marker, String msg, Object[] params) {
//Changed to always return NEUTRAL result
return Result.NEUTRAL;
//old logic: return onMismatch;
}
...
private Result filter(Throwable t) {
//TODO: trigger the external tool here when t != null, pass t if needed.
//Changed to always return NEUTRAL result
return Result.NEUTRAL;
//old logic: return t != null ? onMatch : onMismatch;
}
/**
* Create a ThrowableFilter.
* @return The created ThrowableFilter.
*/
@PluginFactory
public static ThrowableFilter createFilter() {
return new ThrowableFilter();
}
Then in the configuration you would remove the parameters to the filter. It would now look like this:
<ThrowableFilter/>
Hope this helps you.