Допустим, нам захотелось логгировать каждый метод некого Java-класса по разному:
%переменными
,Данная статья показывает как эти требования выполнить. В целях соблюдения простоты разделенем логгирования производится только по методам; в реальности может захотеться иметь иерархический уточнняющий конфиг, вроде Приложение
> Компонент
> Сервис
> Метод
… Ссылка на полный исходный код будет внизу.
class ThingService {
log = LoggerFactory.getLogger();
getThing() {
log.debug("getThing..."); // => one.log
}
listThings() {
log.debug("listThings..."); // => another.log
}
}
Для реализации выбрана добротная библиотека логгирования "logback", предоставляющая интересные возможности для кастомизации:
ch.qos.logback:logback-classic:1.2.3
Настраивается как из XML-конфига, так и напрямую из Джавы, подходы можно комбинировать:
public void configureLogback() throws JoranException {
LoggerContext lc = LoggerFactory.getILoggerFactory();
lc.reset(); // reset prev config
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(lc);
configurator.doConfigure("config.xml"); // any data source
StatusPrinter.printInCaseOfErrorsOrWarnings(lc);
// а теперь комбинация:
Logger root = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.setLevel(Level.INFO); // переопределение
}
Вкратце о логгировании:
%переменную
,Для простоты была выбрана чистая джавиная конфигурация. Тут всё довольно очевидно если держать перед глазами XML-конфиг. Основная задача — создать свои собственные appender/encoder и зарегистрировать их — они будут вызываться логбэком из своих недр. Почти каждый создаваемый объект нужно не забыть запустить методом start()
. Абстрактный пример:
Logger rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
LoggerContext lc = rootLogger.getLoggerContext();
lc.reset(); // reset prev config
var encoder = new PatternLayoutEncoder();
encoder.setContext(lc);
encoder.setPattern("%-5level %message%n");
encoder.start();
var appender = new ConsoleAppender<ILoggingEvent>();
appender.setContext(lc);
appender.setEncoder(encoder);
appender.start();
rootLogger.setLevel(Level.DEBUG);
rootLogger.addAppender(appender);
Чтобы логбэк мог отличить один метод от другого, перед вызовом метода сохраняем его имя в ThreadLocal
Mapped Diagnostic Context. Далее, при анализе, эти значения напрямую из класса MDC
не достаём, так как логгирующий код будет исполняться уже в другом потоке и этих данных там не будет — достаём их через ILoggingEvent.getMDCPropertyMap()
.
В общем случае, как верно заметил vooft, нужно поддерживать стек вызовов и не затирать MDC-значение, а возвращать его к предыдущему фрейму, что делается через введение нового ThreadLocal
. Схематический пример:
try {
MDC.put(MDC_KEY_METHOD, currentMethod);
// 1. Сохранить currentMethod в стек вызовов
// 2. Вызвать сам метод
// 3. В реальности оборачивать вызовы лучше через AOP, конечно.
} finally {
String previousMethod = // Достаётся из стека вызовов
MDC.put(previousMethod);
}
Создадим и не забудем зарегистрировать свой собственный аппендер:
class MultiAppender extends AppenderBase<ILoggingEvent> {
@Override
protected void append(ILoggingEvent event) {
method = event.getMDCPropertyMap().get(MDC_KEY_METHOD);
Appender appender = getOrCreateAppender(method);
appender.doAppend(event);
}
Сам он почти ничего не делает, только делегирует логгирование пачке настоящих файловых аппендеров, по одному на каждый метод. Делегирует одному, самому подходящему. "Настоящие" аппендеры создаются по требованию, так:
fileAppender = new FileAppender<ILoggingEvent>();
fileAppender.setContext(lc);
fileAppender.setAppend(false);
fileAppender.setEncoder(getOrCreateEncoderByMethod(lc, method));
fileAppender.setFile(logFileByMethod.get(method));
fileAppender.start();
Для этого держим кэш авто-создаваемых объектов типа Encoder
:
Map<String, String> patternByMethod = new HashMap<>();
// вызывается выше; упрощёно
Encoder getOrCreateEncoderByMethod(LoggerContext lc, String method) {
String pattern = patternByMethod.get(method);
encoder = new PatternLayoutEncoder();
encoder.setContext(lc);
encoder.setPattern(pattern);
encoder.start();
return encoder;
}
В класс MultiAppender
добавим проверку: если уровень события преодолевает порог заданный для метода, только тогда логгируем его:
Map<String, Level> levelByMethod = new HashMap<>();
protected void append(ILoggingEvent event) {
Level minLevel = levelByMethod.get(methodName);
if (event.getLevel().levelInt >= minLevel.levelInt) {
appender.doAppend(event);
}
В принципе, эту логику можно вынести в фильтр.
Чтобы не городить свой огород, а воспользоваться проверенной инфраструктурой логбэка, нужно определить свой класс конвертера, обязательно целиком публичный чтобы его могли инстанциировать извне. Если нужен MDC
, берём его из эвента. Здесь заводится обработчик переменной %custom
:
public class CustomConverter extends ClassicConverter {
public String convert(ILoggingEvent event) {
// mdc = event.getMDCPropertyMap();
return "variable-expanded";
}
}
Во время общего процесса конфигурации зарегистрируем обработчик:
void configurePatterns(LoggerContext lc) {
Map<String, String> rules
= lc.getObject(CoreConstants.PATTERN_RULE_REGISTRY);
if (rules == null) {
rules = new HashMap<String, String>();
lc.putObject(CoreConstants.PATTERN_RULE_REGISTRY, rules);
}
rules.put("custom", CustomConverter.class.getName());
}
И будем использовать в качестве энкодера, например, PatternLayoutEncoder
, который сам всё подхватит. В данном случае переменная %custom
раскроется в строку "variable-expanded"
.
Возможность такая есть из коробки: достаточно вызвать функцию-конфигуратор ещё раз, не забыв сделать там LoggerContext::reset()
и очистку накопленного кэша.
Если законфигурированный нами метод вызывает к жизни новые потоки, то на них, естественно, заданные правила логгирования распространяться не будут — в новом потоке thread local'ы сами собой не появятся. Так что, если хочется применить настройки метода к новому потоку, придётся скопировать туда MDC
:
Map<String, String> mdcOrig = MDC.getCopyOfContextMap();
ExecutorService es = Executors.newFixedThreadPool(1);
es.submit(() -> threadWorker(mdcOrig));
void threadWorker(Map<String, String> parentMdc) {
MDC.setContextMap(parentMdc);
log.error("expected to appear in method2*.log");
}
https://github.com/zencd/logback-setup
К сожалению, не доступен сервер mySQL