Hello,
I tried the DynamicThresholdFilter suggestion, and it worked nice, so did the sifting appender. I didn't tested the ThresholdFilter, as in the end it seems it was not a good idea to forbid the user events to go to the STANDARD file... it is a "master log file" after all, better let it be complete. Thanks again for all the suggestions, Ralph.
However, the DynamicThresholdFilter generated more logging than desired for many less important loggers. As a refinement of the idea, I created a custom TurboFilter with similar capabilities, but specific per logger too. I'm posting below my new XML config and filters, someone else may use it, plus I'd like some suggestions on ugly parts of the code ;-) (see FIXME comments below).
Some of my new config XML:
<configuration debug="true">
<turboFilter class="com.sample.project.CustomTurboFilter">
<userCustomization>
<user>ID_USER_1</user>
<loggerLevel>
<logger>com.sample.project</logger>
<level>DEBUG</level>
</loggerLevel>
<loggerLevel>
<logger>com.sample.legacy</logger>
<level>INFO</level>
</loggerLevel>
</userCustomization>
<userCustomization>
<user>ID_USER_2</user>
<loggerLevel>
<logger>com.sample.project</logger>
<level>TRACE</level>
</loggerLevel>
...
</userCustomization>
</turboFilter>
<appender name="FILTERED" class="ch.qos.logback.classic.sift.SiftingAppender">
<discriminator class="ch.qos.logback.classic.sift.MDCBasedDiscriminator">
<key>userId</key>
<defaultValue>unknown</defaultValue>
</discriminator>
<filter class="com.sample.project.CustomFilter"/>
<sift> ... </sift>
</appender>
<logger name="com.sample.project" level="WARN">
<appender-ref ref="FILTERED" />
</logger>
<root level="WARN">
<appender-ref ref="STANDARD" />
</root>
</configuration>
The CustomTurboFilter:
public class CustomTurboFilter extends TurboFilter {
private Map<String,Map<String,Level>> userCustomizationsMap = new HashMap<String, Map<String,Level>>();
private Set<String> customizedUsersSet;
public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable throwable) {
if (!isStarted())
return FilterReply.NEUTRAL;
String user = MDC.get("userId");
if (user == null)
return FilterReply.NEUTRAL;
Map<String, Level> map = userCustomizationsMap.get(user);
if (map == null)
return FilterReply.NEUTRAL; //undefined user
//test each logger-level for this user, find the closest match to 'logger'
String loggerName = logger.getName();
Level specificLoggerLevel = null;
int specificLoggerNameLength = -1;
for (Entry<String, Level> e : map.entrySet()) {
String name = e.getKey();
//FIXME This is awful code, I know, but I found no other way to check logger hierarchy (and LoggerComparator is not fit for this)
//FIXME guess it could be a bit faster using only 'startsWith', 'length' and 'charAt', but still String manipulation
if (loggerName.equals(name)) {
specificLoggerLevel = e.getValue(); //exact logger match
break;
}
//FIXME Here I attempt to find the most specific logger definition for the current logger (awful too)
int length = name.length();
if (loggerName.startsWith(name+".") && length > specificLoggerNameLength) {
//the logger in this definition is a parent of 'logger', and longer name than the last one
specificLoggerNameLength = length;
specificLoggerLevel = e.getValue();
}
}
if (specificLoggerLevel != null && level.isGreaterOrEqual(specificLoggerLevel)) {
return FilterReply.ACCEPT; //match found, force accept
} else {
return FilterReply.NEUTRAL; //no matching logger found, ignore
}
}
public void addUserCustomization(UserLoggersMapping userLoggersMapping) {
String user = userLoggersMapping.getUser();
user = (user == null ? "" : user.trim());
if (!user.isEmpty() && !userCustomizationsMap.containsKey(user)) {
Map<String,Level> loggerLevelMap = new HashMap<String, Level>();
for (LoggerLevelPair llp : userLoggersMapping.getLoggerLevelList()) {
String logger = llp.getLogger();
logger = (logger == null ? "" : logger.trim());
if (!logger.isEmpty() && !loggerLevelMap.containsKey(logger) && llp.getLevel() != null) {
addInfo("Logger["+logger+"] level customized to "+llp.getLevel()+" for user "+user);
loggerLevelMap.put(logger, llp.getLevel());
}
}
if (!loggerLevelMap.isEmpty()) {
userCustomizationsMap.put(user, loggerLevelMap);
}
}
}
public Set<String> getCustomizedUsersSet() {
return customizedUsersSet;
}
public void start() {
customizedUsersSet = Collections.unmodifiableSet(userCustomizationsMap.keySet()); //Will be used by CustomFilter
super.start();
}
//Just JavaBeans, also shortening the code :-P
public static class UserLoggersMapping {
private String user;
private List<LoggerLevelPair> loggerLevelList = new ArrayList<LoggerLevelPair>();
private String getUser() {return user;}
public void setUser(String user) {this.user = user;}
private List<LoggerLevelPair> getLoggerLevelList() {return loggerLevelList;}
public void addLoggerLevel(LoggerLevelPair levelLoggerPair) {loggerLevelList.add(levelLoggerPair);}
}
public static class LoggerLevelPair {
private Level level;
private String logger;
private Level getLevel() {return level;}
public void setLevel(Level level) {this.level = level;}
private String getLogger() {return logger;}
public void setLogger(String logger) {this.logger = logger;}
}
}
The CustomFilter. It is optional, used to deny sifted logging to users not specified by the CustomTurboFilter:
public class CustomFilter extends Filter<ILoggingEvent> {
private Set<String> customizedUsersSet;
public FilterReply decide(ILoggingEvent event) {
//deny anything but a user filtered by CustomTurboFilter
if (!isStarted() || customizedUsersSet == null)
return FilterReply.DENY;
String user = MDC.get("userId");
if (user == null)
return FilterReply.DENY;
return customizedUsersSet.contains(user) ? FilterReply.NEUTRAL : FilterReply.DENY;
}
public void start() {
//FIXME is this safe? Joran's sequential processing seems to make it so
Iterator<TurboFilter> iterator = ((LoggerContext) getContext()).getTurboFilterList().iterator();
while (iterator.hasNext()) {
TurboFilter tf = iterator.next();
if (tf instanceof CustomTurboFilter) {
customizedUsersSet = ((CustomTurboFilter) tf).getCustomizedUsersSet();
addInfo("Allowed users: " + customizedUsersSet);
break;
}
}
if (customizedUsersSet == null)
addWarn("No matching TurboFilter found");
super.start();
}
}
So, can I improve the code somehow, or is there another approach to evaluate? All contributions are most welcome.
Thanks again!