[Программирование, Java] Раздельное логгирование методов в Java/logback

Автор Сообщение
news_bot ®

Стаж: 1 год 10 месяцев
Сообщений: 27820

Создавать темы news_bot ® написал(а)
14-Авг-2019 13:34


Задача
Допустим, нам захотелось логгировать каждый метод некого Java-класса по разному:
  • Для каждого метода свой лог-файл,
  • … свой формат лога,
  • … свой минимальный уровень логгирования,
  • формат лога расширяем собственными переменными вида %custom,
  • возможность обновлять эту конфигурацию на лету.

Данная статья показывает как эти требования выполнить. В целях соблюдения простоты разделенем логгирования производится только по методам; в реальности может захотеться иметь иерархический уточнняющий конфиг, вроде ПриложениеКомпонентСервисМетод… Ссылка на полный исходный код будет внизу.
Logback
Для реализации выбрана добротная библиотека логгирования "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().
try {
        MDC.put(MDC_KEY_METHOD, methodName);
        // Вызов метода
        // В реальности оборачивать лучше через AOP, конечно.
    } finally {
        MDC.put(remove);
    }

Свой лог-файл на каждый метод
Создадим и не забудем зарегистрировать свой собственный аппендер:
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
Литература
Официальный мануал logback
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_java, #_logging, #_logback, #_java
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 15-Дек 04:29
Часовой пояс: UTC + 5