Перейти к основному содержимому

Свободное ведение журнала Flogger

· 9 мин. чтения

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");

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

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.

В настоящее время для этого требуется отдельный поддерживающий класс. Мы могли бы, например, расширить API Flogger, написав класс 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...)

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

log(Level, Object...)

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

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

Понятно, что API объединяет три задачи в один вызов метода:

  1. Он пытается указать уровень журнала (выбор метода)
  2. Попытка прикрепить метаданные к оператору журнала (причина Throwable )
  3. А также, указав сообщение журнала и аргументы.

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

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

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);
}

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

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

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

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

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

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

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

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

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

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

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

Как насчет исключений, как мы их обрабатываем?

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

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

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

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

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

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

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

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

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

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");

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

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

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

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

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

Sep 18, 2019 5:04:02 PM com.foreach.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.foreach.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.foreach.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0
INFO: This log shows every 40 iterations => 80 [CONTEXT ratelimit_count=40 ]

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

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

При этом результат теперь становится:

Sep 18, 2019 5:08:06 PM com.foreach.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.foreach.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.foreach.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. флоггер с slf4j

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

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

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

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

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

5.2. Флоггер с Log4j

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

<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>

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

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

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

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

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

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

Как обычно, исходный код этого руководства доступен на GitHub .