«1. Обзор

В этом уроке мы поговорим о фреймворке Flogger, свободном API ведения журналов для Java, разработанном Google.

2. Зачем использовать Flogger?

Со всеми существующими на рынке фреймворками ведения журналов, такими как Log4j и Logback, зачем нам еще один фреймворк ведения журналов?

Оказывается, у Flogger есть несколько преимуществ перед другими фреймворками — давайте посмотрим.

2.1. Удобочитаемость

Плавный характер API Flogger имеет большое значение для того, чтобы сделать его более читабельным.

Давайте рассмотрим пример, в котором мы хотим регистрировать сообщение каждые десять итераций.

С традиционной структурой ведения журналов мы бы увидели что-то вроде:

int i = 0;

// ...

if (i % 10 == 0) {
    logger.info("This log shows every 10 iterations");
    i++;
}

Но теперь, с Flogger, вышеизложенное можно упростить до:

logger.atInfo().every(10).log("This log shows every 10 iterations");

Хотя можно утверждать, что версия Flogger оператор logger выглядит немного более подробным, чем традиционные версии, он обеспечивает большую функциональность и в конечном итоге приводит к более читаемым и выразительным операторам журнала.

2.2. Производительность

Объекты ведения журнала оптимизируются, если мы избегаем вызова toString для объектов ведения журнала:

User user = new User();
logger.atInfo().log("The user is: %s", user);

Если мы регистрируем, как показано выше, серверная часть имеет возможность оптимизировать ведение журнала. С другой стороны, если мы вызываем toString напрямую или объединяем строки, эта возможность теряется:

logger.atInfo().log("Ths user is: %s", user.toString());
logger.atInfo().log("Ths user is: %s" + user);

2.3. Расширяемость

Фреймворк Flogger уже покрывает большую часть основных функций, которые мы ожидаем от каркаса протоколирования.

Однако бывают случаи, когда нам нужно добавить функциональность. В этих случаях можно расширить API.

В настоящее время для этого требуется отдельный вспомогательный класс. Мы могли бы, например, расширить Flogger API, написав класс UserLogger:

logger.at(INFO).forUserId(id).withUsername(username).log("Message: %s", param);

Это может быть полезно в случаях, когда мы хотим отформатировать сообщение последовательно. Затем UserLogger предоставит реализацию пользовательских методов forUserId(String id) и withUsername(String username).

Для этого класс UserLogger должен расширить класс AbstractLogger и предоставить реализацию API. Если мы посмотрим на FluentLogger, то увидим, что это просто регистратор без дополнительных методов, поэтому мы можем начать с копирования этого класса как есть, а затем построить его на основе, добавив к нему методы.

2.4. Эффективность

Традиционные фреймворки широко используют varargs. Эти методы требуют, чтобы новый Object[] был выделен и заполнен до того, как метод может быть вызван. Кроме того, любые переданные фундаментальные типы должны быть автоматически упакованы.

Все это требует дополнительного байт-кода и задержки в месте вызова. Особенно прискорбно, если оператор журнала на самом деле не включен. Стоимость становится более очевидной в журналах уровня отладки, которые часто появляются в циклах. Flogger избавляется от этих затрат, полностью избегая varargs.

Flogger решает эту проблему, используя свободную цепочку вызовов, из которой могут быть построены операторы регистрации. Это позволяет фреймворку иметь только небольшое количество переопределений метода журнала и, таким образом, иметь возможность избегать таких вещей, как varargs и автоупаковка. Это означает, что API может включать множество новых функций без комбинаторного взрыва.

Типичная структура ведения журнала будет иметь следующие методы:

level(String, Object)
level(String, Object...)

где level может быть одним из примерно семи имен уровня журнала (например, серьезный), а также иметь канонический метод журнала, который принимает дополнительный уровень журнала:

log(Level, Object...)

В дополнение к этому обычно существуют варианты методов, которые принимают причину (экземпляр Throwable), связанную с оператором журнала:

level(Throwable, String, Object)
level(Throwable, String, Object...)

Понятно, что API связывает три проблемы. в один вызов метода:

  1. It’s trying to specify the log level (method choice)
  2. Trying to attach metadata to the log statement (Throwable cause)
  3. And also, specifying the log message and arguments.

Этот подход быстро увеличивает количество различных методов регистрации, необходимых для решения этих независимых задач.

Теперь мы можем понять, почему важно иметь два метода в цепочке:

logger.atInfo().withCause(e).log("Message: %s", arg);

Давайте теперь посмотрим, как мы можем использовать его в нашей кодовой базе.

3. Зависимости

Установить Flogger довольно просто. Нам просто нужно добавить flogger и flogger-system-backend в наш pom:

<dependencies>
    <dependency>
        <groupId>com.google.flogger</groupId>
        <artifactId>flogger</artifactId>
        <version>0.4</version>
    </dependency>
    <dependency>
        <groupId>com.google.flogger</groupId>
        <artifactId>flogger-system-backend</artifactId>
        <version>0.4</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

«

«После настройки этих зависимостей мы можем перейти к изучению API, который находится в нашем распоряжении.

4. Изучение Fluent API

private static final FluentLogger logger = FluentLogger.forEnclosingClass();

Прежде всего, давайте объявим статический экземпляр для нашего регистратора:

int result = 45 / 3;
logger.atInfo().log("The result is %d", result);

Теперь мы можем начать регистрацию. Мы начнем с чего-то простого:

Сообщения журнала могут использовать любой спецификатор формата printf языка Java, например %s, %d или %016x.

4.1. Избегайте работы на лог-сайтах

Создатели Flogger рекомендуют избегать работы на лог-сайтах.

public static String collectSummaries() {
    longRunningProcess();
    int items = 110;
    int s = 30;
    return String.format("%d seconds elapsed so far. %d items pending processing", s, items);
}

Допустим, у нас есть следующий долговременный метод для суммирования текущего состояния компонента:

logger.atFine().log("stats=%s", collectSummaries());

Заманчиво вызвать collectSummaries непосредственно в нашем операторе журнала:

Независимо от настроенного журнала уровней или ограничения скорости, метод collectSummaries теперь будет вызываться каждый раз.

Практически бесплатная стоимость отключенных операторов ведения журнала лежит в основе структуры ведения журнала. Это, в свою очередь, означает, что большее их количество можно оставить в коде без вреда для себя. Написание оператора журнала, как мы только что сделали, лишает этого преимущества.

logger.atFine().log("stats=%s", LazyArgs.lazy(() -> collectSummaries()));

Вместо этого мы должны использовать метод LazyArgs.lazy:

Теперь на сайте журнала почти не выполняется никакой работы — только создание экземпляра для лямбда-выражения. Flogger будет оценивать эту лямбду только в том случае, если он намеревается действительно зарегистрировать сообщение.

if (logger.atFine().isEnabled()) {
    logger.atFine().log("summaries=%s", collectSummaries());
}

Хотя разрешено охранять операторы журнала с помощью isEnabled:

Это не обязательно, и мы должны избегать этого, потому что Flogger делает эти проверки за нас. Этот подход также защищает операторы журнала только по уровню и не помогает с операторами журнала с ограниченной скоростью.

4.2. Работа с исключениями

Как насчет исключений, как мы с ними справляемся?

try {
    int result = 45 / 0;
} catch (RuntimeException re) {
    logger.atInfo().withStackTrace(StackSize.FULL).withCause(re).log("Message");
}

Ну, Flogger поставляется с методом withStackTrace, который мы можем использовать для регистрации экземпляра Throwable:

Где withStackTrace принимает в качестве аргумента перечисление StackSize с постоянными значениями SMALL, MEDIUM, LARGE или FULL. Трассировка стека, сгенерированная withStackTrace(), будет отображаться как исключение LogSiteStackTrace в серверной части java.util.logging по умолчанию. Однако другие серверные части могут обрабатывать это по-другому.

4.3. Конфигурация и уровни ведения журнала

До сих пор мы использовали logger.atInfo в большинстве наших примеров, но Flogger поддерживает множество других уровней. Мы рассмотрим их, но сначала давайте представим, как настроить параметры ведения журнала.

Для настройки ведения журнала мы используем класс LoggerConfig.

LoggerConfig.of(logger).setLevel(Level.FINE);

Например, когда мы хотим установить уровень ведения журнала FINE:

logger.atInfo().log("Info Message");
logger.atWarning().log("Warning Message");
logger.atSevere().log("Severe Message");
logger.atFine().log("Fine Message");
logger.atFiner().log("Finer Message");
logger.atFinest().log("Finest Message");
logger.atConfig().log("Config Message");

И Flogger поддерживает различные уровни ведения журнала:

4.4. Ограничение скорости

Как насчет ограничения скорости? Как поступить в случае, когда мы не хотим регистрировать каждую итерацию?

IntStream.range(0, 100).forEach(value -> {
    logger.atInfo().every(40).log("This log shows every 40 iterations => %d", value);
});

Flogger приходит к нам на помощь с методом every(int n):

Sep 18, 2019 5:04:02 PM com.baeldung.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0
INFO: This log shows every 40 iterations => 0 [CONTEXT ratelimit_count=40 ]
Sep 18, 2019 5:04:02 PM com.baeldung.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0
INFO: This log shows every 40 iterations => 40 [CONTEXT ratelimit_count=40 ]
Sep 18, 2019 5:04:02 PM com.baeldung.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0
INFO: This log shows every 40 iterations => 80 [CONTEXT ratelimit_count=40 ]

Мы получаем следующий вывод, когда запускаем приведенный выше код:

IntStream.range(0, 1_000_0000).forEach(value -> {
    logger.atInfo().atMostEvery(10, TimeUnit.SECONDS).log("This log shows [every 10 seconds] => %d", value);
});

Что, если мы хотим регистрировать, скажем, каждые 10 секунды? Затем мы можем использовать atMostEvery(int n, TimeUnit unit):

Sep 18, 2019 5:08:06 PM com.baeldung.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1
INFO: This log shows [every 10 seconds] => 0 [CONTEXT ratelimit_period="10 SECONDS" ]
Sep 18, 2019 5:08:16 PM com.baeldung.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1
INFO: This log shows [every 10 seconds] => 3545373 [CONTEXT ratelimit_period="10 SECONDS [skipped: 3545372]" ]
Sep 18, 2019 5:08:26 PM com.baeldung.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1
INFO: This log shows [every 10 seconds] => 7236301 [CONTEXT ratelimit_period="10 SECONDS [skipped: 3690927]" ]

Теперь результат будет таким:

5. Использование Flogger с другими бэкендами

Итак, что, если мы хотите добавить Flogger в наше существующее приложение, которое уже использует, например, Slf4j или Log4j? Это может быть полезно в тех случаях, когда мы хотели бы воспользоваться нашими существующими конфигурациями. Как мы увидим, Flogger поддерживает несколько серверных частей.

5.1 Flogger с Slf4j

<dependency>
    <groupId>com.google.flogger</groupId>
    <artifactId>flogger-slf4j-backend</artifactId>
    <version>0.4</version>
</dependency>

Настроить серверную часть Slf4j очень просто. Во-первых, нам нужно добавить зависимость flogger-slf4j-backend к нашему pom:

System.setProperty(
  "flogger.backend_factory", "com.google.common.flogger.backend.slf4j.Slf4jBackendFactory#getInstance");

Затем нам нужно сообщить Flogger, что мы хотели бы использовать серверную часть, отличную от используемой по умолчанию. Мы делаем это, регистрируя фабрику Flogger через системные свойства:

И теперь наше приложение будет использовать существующую конфигурацию.

5.1 Flogger с Log4j

<dependency>
    <groupId>com.google.flogger</groupId>
    <artifactId>flogger-log4j-backend</artifactId>
    <version>0.4</version>
    <exclusions>
        <exclusion>
            <groupId>com.sun.jmx</groupId>
            <artifactId>jmxri</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.sun.jdmk</groupId>
            <artifactId>jmxtools</artifactId>
        </exclusion>
        <exclusion>
            <groupId>javax.jms</groupId>
            <artifactId>jms</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>apache-log4j-extras</artifactId>
    <version>1.2.17</version>
</dependency>

Мы выполняем аналогичные шаги для настройки серверной части Log4j. Давайте добавим зависимость flogger-log4j-backend к нашему pom:

System.setProperty(
  "flogger.backend_factory", "com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance");

Нам также нужно зарегистрировать серверную фабрику Flogger для Log4j:

Вот и все, теперь наше приложение настроено на используйте существующие конфигурации Log4j!

6. Заключение

«В этом руководстве мы увидели, как использовать фреймворк Flogger в качестве альтернативы традиционным фреймворкам ведения журналов. Мы увидели некоторые мощные функции, которые могут быть полезны при использовании фреймворка.

Мы также увидели, как мы можем использовать наши существующие конфигурации, регистрируя различные серверные части, такие как Slf4j и Log4j.