Вход по дешевке

Ведение журнала со сверхнизкими накладными расходами для JVM

Входить или не входить?

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

При создании обычных приложений вопрос о том, должно ли действие регистрироваться или нет, редко является предметом серьезных дискуссий. (Под «обычными» мы подразумеваем большинство приложений электронной коммерции с довольно обычными нефункциональными требованиями, касающимися пропускной способности, задержки, доступности и т. Д.) Иногда мы сталкиваемся с классом приложений, которые ломают этот шаблон. Иногда отдельные модули обычной системы могут подвергаться очень строгим ограничениям в отношении их производительности. Приложение может выполнять дорогостоящие вычисления в замкнутом цикле или может потребовать ответа (в среднем) в сжатые сроки. Инвазивное оснащение этих приложений изменяет их временное поведение - даже простые вызовы журнала несут накладные расходы, которые могут кардинально повлиять на качество обслуживания клиентов. В подобных сценариях вопрос, поставленный в начале статьи - Регистрировать или не регистрировать? - становится предметом споров. Вы жертвуете безупречной производительностью ради наблюдаемости, или производительность является желанным источником дохода, который затмевает все остальные проблемы?

Представляем Zerolog?

В этой статье мы рассмотрим Zerolog (сокращенно Zlg) - фасад со сверхнизкими накладными расходами для ведения журналов для приложений Java, чувствительных к производительности. Zlg преследует двоякую цель:

  1. Сверхнизкие накладные расходы для подавления ведения журнала. Другими словами, затраты на вызов метода журнала, когда ведение журнала для этого уровня было отключено, незначительны. Кроме того, можно вызвать Zlg таким образом, чтобы снизить эти затраты до абсолютного нуля.
  2. Бескомпромиссное покрытие кода. Подавление ведения журнала не должно влиять на показатели покрытия операторов и ветвей. Запись в журнале - это такое же утверждение, как и любое другое, - стоит ли его писать, стоит проверить.

В совокупности эти цели делают Zlg подходящим для использования в сверхвысокопроизводительных приложениях с малой задержкой и в средах с высоким уровнем надежности.

Насколько это быстро?

Стоит ли эта статья моего времени? Конечно же, библиотеки лесозаготовок и фасады уже оптимизированы до нитки. Стоит ли рассматривать еще одну альтернативу?

Это справедливые вопросы. Тест JMH, проведенный на процессоре i7–4770 Haswell с подавленным ведением журнала, сравнивает штрафы за вызов для Zlg с некоторыми из основных средств ведения журнала. Каждому регистратору передаются четыре примитива для форматирования, что является точным представлением типичной записи журнала.

Примечание. Запуск проводился на CentOS 7 под управлением JDK 10. Для репликации запустите ./gradlew launch -Dlauncher.class=AllBenchmarks.

Момент, пожалуйста, просто чтобы прояснить некоторые вещи, прежде чем мы углубимся слишком глубоко. Современные фасады журналов и библиотеки (такие как SLF4J и Log4j 2.x) просто фантастические. Их более чем достаточно для 99% обычных приложений, которые выполняют сотни или тысячи операций в секунду и обычно отвечают в течение миллисекунд. Откровенно говоря, количество сетевых вызовов и операций ввода-вывода в современной архитектуре микросервисов на порядки превосходит количество вызовов регистрации и любых других низкоуровневых инструментов. Извлечение наносекунд производительности при жизни в мире миллисекунд было бы равносильно членовредительству в техническом смысле. Так что нет, эта статья не для всех и даже не для большинства.

Где я могу найти его?

Zerolog - это проект Java 8 с открытым исходным кодом (лицензия BSD), размещенный на GitHub.



Начиная

Зависимости

Сборки Gradle размещаются на JCenter. Добавьте следующий фрагмент в свой файл сборки, заменив x.y.z версией, указанной на значке загрузки в верхней части этого README.

compile "com.obsidiandynamics.zerolog:zerolog-core:x.y.z"
compile "com.obsidiandynamics.zerolog:<binding>:x.y.z"

Вам понадобится модуль zerolog-core и, как правило, модуль привязки. Единственная поддерживаемая в настоящее время привязка Zlg - zerolog-slf4j17, которая должна работать с любым регистратором, имеющим привязку SLF4J 1.7.x. (Это охватывает все основные регистраторы.) Например, чтобы использовать Zlg с Log4j 1.2.17, добавьте следующее в ваш build.gradle (заменив x.y.z соответствующим образом).

compile "com.obsidiandynamics.zerolog:zerolog-core:x.y.z"
compile "com.obsidiandynamics.zerolog:zerolog-slf4j17:x.y.z"
runtime "org.slf4j:slf4j-api:1.7.25"
runtime "org.slf4j:slf4j-log4j12:1.7.25"
runtime "log4j:log4j:1.2.17"

Примечание. zerolog-slf4j17 не объявляет конкретную slf4j-api версию зависимости Maven, что позволяет вам назначить любую версию API SLF4J, совместимую с двоичным кодом, в вашем проекте. В результате вы должны явно включить версию slf4j-api в файл сборки. Это можно отнести к конфигурации runtime (если вам не нужно использовать SLF4J непосредственно в вашем приложении вместе с Zlg).

логирование

Получение экземпляра регистратора не слишком отличается от SLF4J. Обычно именованный экземпляр регистратора сначала получается из фабрики и впоследствии назначается либо экземпляру, либо статическому полю, как показано ниже.

public final class SysOutLoggingSample {
  private static final Zlg zlg = Zlg.forDeclaringClass().get();  
  
  public static void open(String address, 
                          int port, double timeoutSeconds) {
    zlg.i("Hello world");
    zlg.i("Pi is %.2f", z -> z.arg(Math.PI));
    zlg.i("Connecting to %s:%d [timeout: %.1f sec]", 
          z -> z.arg(address).arg(port).arg(timeoutSeconds));    
    try {
      openSocket(address, port, timeoutSeconds);
    } catch (IOException e) {
      zlg.w("Error connecting to %s:%d", 
            z -> z.arg(address).arg(port).tag("I/O").threw(e));
    }
  }
}

Обратите внимание на несколько важных моментов:

  • Регистратор - это экземпляр Zlg, созданный для определенного класса (с использованием forClass()) или произвольного имени (с использованием forName()).
  • Вызов forDeclaringClass() является сокращенным эквивалентом forClass(TheDeclaringClass.class), где TheDeclaringClass - это имя класса, который объявляет регистратор.
  • По соглашению мы назначаем регистратор полю с именем zlg.
  • Ведение журнала вызывается через непрерывную цепочку, начиная с уровня журнала (сокращенно до первой буквы), указывающего обязательную строку формата, за которой следуют любые необязательные аргументы (примитивы или типы объектов), необязательный тег и необязательное исключение.
  • Строка формата имеет стиль printf, в отличие от большинства других регистраторов, которые используют нотацию {} (stash).

Ленивые аргументы

Шаблон связанных аргументов хорошо работает, когда значения уже доступны и могут быть переданы в регистратор как есть. Если для формулирования аргументов требуется дополнительная работа, то подавление журнала не помешает оценке этих выражений. Например, следующий вызов вызовет метод size() для List независимо от того, включено или отключено ведение журнала. Другими словами, вы в любом случае оплатите стоимость.

final List<Integer> numbers = Arrays.asList(5, 6, 7, 8);
zlg.i("The list %s has %d elements", 
      z -> z.arg(numbers).arg(numbers.size()).tag("list"));

Поставщики

Чтобы избежать ненужного вычисления аргументов, Zlg поддерживает поставщиков и преобразования в стиле FP. (FP означает функциональное программирование.) Приведенный выше пример можно переписать, используя ссылку на метод для предоставления примитивного значения через ссылку на метод получения.

zlg.i("The list %s has %d elements", 
      z -> z.arg(numbers).arg(numbers::size).tag("list"));

Просто изменив list.size() на list::size, мы избегаем потенциально излишнего вызова метода. Мы рекомендуем всегда отдавать предпочтение ссылкам на методы, а не замыканиям в стиле лямбда. Таким образом, не пишется новый код и это не влияет на покрытие кода.

Трансформирует

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

В следующем примере мы ищем имя человека из списка людей. Если имя не найдено, мы хотим регистрировать содержание списка, но не раскрывать фамилии людей. Рассматриваемое преобразование - это статическая tokeniseSurnames() функция, принимающая набор Name объектов. Чтобы добавить преобразование, мы используем служебный метод Args.map(Supplier, Function), предоставляющий как исходное (непреобразованное) значение, так и ссылку на метод преобразования. В остальном проблема Zlg.

private static final Zlg zlg = Zlg.forDeclaringClass().get();public static final class Name {
  final String forename;
  final String surname;  Name(String forename, String surname) {
    this.forename = forename;
    this.surname = surname;
  }
}
public static void logWithTransform() {
  final List<Name> hackers = 
      Arrays.asList(new Name("Kevin", "Flynn"), 
                    new Name("Thomas", "Anderson"), 
                    new Name("Angela", "Bennett"));
  final String surnameToFind = "Smith";  
  if (! hackers.stream()
      .anyMatch(n -> n.surname.contains(surnameToFind))) {
    zlg.i("%s not found among %s", 
          z -> z
          .arg(surnameToFind)
          .arg(Args.map(Args.ref(hackers), 
                        LazyLogSample::tokeniseSurnames)));
  }
}
static List<String> tokeniseSurnames(Collection<Name> names) {
  return names
     .stream()
     .map(n -> n.forename + " " + 
          n.surname.replaceAll(".", "X")).collect(toList());
}

Преобразуемое значение может быть получено лениво. В приведенном ниже примере отображается текущее время с использованием настраиваемого DateFormat; объект Date создается условно.

private static final Zlg zlg = Zlg.forDeclaringClass().get();public 
static void logWithSupplierAndTransform() {
  zlg.i("The current time is %s", 
        z -> z.arg(Args.map(Date::new, LazyLogSample::formatDate)));
}
private static String formatDate(Date date) {
  return new SimpleDateFormat("MMM dd HH:mm:ss").format(date);
}

Одна вещь, которую следует отметить о преобразованиях и поставщиках: они представляют собой код, как и любой другой, и должны соответственно проходить модульное тестирование. У вас может быть преобразование с ошибками, и из-за его ленивой оценки вы не сможете его уловить при тестировании кода, содержащего инструкцию журнала (если ведение журнала было подавлено). Поскольку преобразования и поставщики - это простые «чистые» функции с единственной ответственностью, их модульное тестирование должно быть простым.

Теги

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

Уровни журнала

Уровни журнала ZLG достаточно хорошо согласованы с SLF4J (и большинством других логгеров). Zlg представляет новый уровень журнала - LogLevel.CONF - логически расположенный между DEBUG и INFO. Вольно заимствованный из JUL (java.util.logging), CONF предназначен для регистрации параметров инициализации и конфигурации, что полезно, когда ваше приложение предлагает пользователю различные варианты конфигурации.

Примечание. CONF канонически сопоставляется с INFO в тех средствах ведения журнала, которые не поддерживают CONF напрямую.

Встроенные уровни журнала: от самого низкого до самого высокого: TRACE, DEBUG, CONF, INFO, WARN, ERROR и OFF.

OFF не является допустимым уровнем журнала, поскольку его нельзя использовать для вывода записи журнала из кода приложения; его удобно использовать для подавления - самого высокого из всех уровней.

Конфигурация

Привязки

Будучи фасадом, Zlg делегирует все вызовы журнала фактическому регистратору - реализация LogService. По умолчанию Zlg поставляется с очень простой «отказоустойчивой» SysOutLogService, которая печатает записи в System.out в фиксированном формате. Пример ниже.

21:18:11.771 INF [main] SysOutLoggingSample.open:20: Connecting to github.com:80 [timeout: 30.0 sec]
21:18:11.773 WRN [main] [I/O] SysOutLoggingSample.open:25: Error connecting to github.com:80

Zlg обнаруживает установленные привязки с помощью механизма подключаемых модулей Java SPI. Просто включив привязку в путь к классам, Zlg переключится на новую привязку по умолчанию.

Конфигурация регистратора

Как и SLF4J, Zlg в значительной степени не вмешивается, когда дело доходит до управления конфигурацией регистратора, оставляя особенности конфигурации на усмотрение реализации связанного регистратора. Например, настройку Log4j 1.2 можно выполнить через log4j.properties; Zlg остается совершенно безразличным к этому.

Базовая конфигурация с zlg.properties

Zlg поддерживает базовую конфигурацию, считывая необязательный файл zlg.properties из пути к классам. Базовая конфигурация включает список дополнительных свойств. Например, он указывает базовый уровень журнала (ниже которого все журналы отключены) и может переопределить привязку по умолчанию. Ниже приведен пример zlg.properties.

zlg.base.level=CONF
zlg.log.service=com.obsidiandynamics.zerolog.SysOutLogService

Свойство zlg.base.level указывает минимальный разрешенный уровень ведения журнала (независимо от того, что может быть разрешено привязанным регистратором). Значение по умолчанию - CONF, что означает, что, если базовая линия не будет изменена, записи TRACE и DEBUG будут проигнорированы. Выбор CONF в качестве уровня по умолчанию наиболее близок к типичной производственной конфигурации.

Изменение местоположения zlg.properties

Местоположение по умолчанию zlg.properties можно изменить, установив системное свойство zlg.default.config.uri. URI по умолчанию - cp://zlg.properties, cp:// обозначает «путь к классам». В качестве альтернативы расположение файловой системы можно указать с помощью схемы file://.

При переопределении местоположения файла по умолчанию, в идеале свойство zlg.default.config.uri должно быть передано как -D... аргумент JVM, гарантируя правильную загрузку подсистемы ведения журнала перед первоначальным использованием.

Встроенная конфигурация

В дополнение к zlg.properties, Zlg поддерживает встроенную конфигурацию в момент получения регистратора:

final Zlg zlg = Zlg
    .forDeclaringClass()
    .withConfigService(new 
LogConfig().withBaseLevel(LogLevel.TRACE))
    .get();

Встроенная конфигурация имеет приоритет, переопределяя любые системные значения по умолчанию или значения, предоставленные zlg.properties. Итак, если вы хотите заставить определенный класс регистрироваться на консоли как особый случай при использовании привязки SLF4J для всех других классов, вы можете просто сделать это:

final Zlg zlg = Zlg
    .forDeclaringClass()
    .withConfigService(new LogConfig()
                       .withBaseLevel(LogLevel.TRACE)
                       .withLogService(new SysOutLogService()))
    .get();

Почему Zerolog такой быстрый?

Лучше задать вопрос: Почему типичные регистраторы и фасады регистрации настолько медленные? Типичный оператор SLF4J (другие регистраторы в основном находятся в той же лодке) выглядит следующим образом:

logger.trace("float: {}, double: {}, int: {}, long: {}", 
             f, d, i, l);

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

  1. Использование varargs для передачи параметров. Обычно помимо двух-четырех (в зависимости от уровня подготовки разработчика API) аргументов форматирования, регистраторы предлагают API на основе varargs для размещения произвольного числа аргументов. Варарги - это просто синтаксический сахар; влияние на производительность оказывает распределение массива. Кроме того, любой escape-анализ, выполненный перед оптимизацией, приведет к заключению, что массив можно использовать за пределами области действия вызова метода, и, следовательно, выделение стека будет невозможно - произойдет полное выделение кучи.
  2. Упаковка примитивных типов. Аргументы форматирования - это либо Object типы, либо массивы vararg из Object. Передача примитивов в API приведет к автобоксу. Кэш автобокса обычно довольно мал; только относительно небольшая горстка примитивов интернирована. Даже интернированные примитивы по-прежнему требуют для разрешения ветвления, арифметики смещения и поиска в массиве. (См. Integer.valueOf(int), чтобы узнать, как это реализовано в JDK.)
  3. Сборка мусора. Это еще один симптом №1 и №2; как varargs, так и (не интернированный) автобокс в конечном итоге выделяют объекты в куче, независимо от того, включено ли ведение журнала или подавлено (если оно не сопровождается «защитной» ветвью); скорость выделения особенно высока, когда ведение журнала ведется из тесных циклов. Это отрицательно сказывается на пропускной способности и задержке приложения.

Чтобы решить №1 и №2 (№3 последует за ним) с использованием традиционного API в стиле SLF4J, потребуется предоставить аргумент с соответствующим типом для каждый из восьми примитивных типов Java, а также Object для переменной арности. Учитывая, что может быть любое количество смешанных примитивов и типов объектов в любой комбинации, создание единого интерфейса со всеми возможными комбинациями соответствующих типов данных является экспоненциальной функцией арности области аргументов. С четырьмя аргументами будет 6561 отдельный перегруженный метод для каждого уровня. Для этого потребуется некоторая форма генерации кода и наверняка убьет любое автозаполнение IDE. С семью аргументами это число составляет около двадцати миллионов (на пяти уровнях журнала).

Zlg решает вышеуказанные проблемы, накапливая примитивы по одному в экземпляр Zlg.LogChain. В LogChain API есть метод arg() для каждого из восьми примитивных типов Java, а также Object. Используя шаблон плавного связывания и работая с одним аргументом за раз, Zlg обходит проблему арности с помощью крошечного интерфейса. Когда ведение журнала подавлено, примитивы никогда не упаковываются, а массивы никогда не выделяются - таким образом, Zlg имеет нулевую скорость выделения объектов и нулевое влияние на сборщик мусора. (Это было подтверждено профилированием сборщика мусора и запуском с отключенным сборщиком мусора.)

В качестве дальнейшей оптимизации управляемый интерфейсом дизайн инвертированных зависимостей, используемый Zlg, позволяет ему заменять свою цепочку журналов накопителей с сохранением состояния на NopLogChain, как только он приходит к выводу, что ведение журнала было отключено для запрошенного уровня. Поскольку реализация NopLogChain представляет собой предварительно созданный одноэлементный объект с безоперационными методами и без общих полей, она подлежит агрессивной оптимизации. Тесты, проведенные с большим количеством аргументов форматирования, указывают на дополнительные затраты около 0,03–0,05 нс на каждый дополнительный аргумент.

Мокинг и покрытие кода

Как упоминалось ранее, одной из ключевых целей Zlg является значительное сокращение накладных расходов на ведение журнала без ущерба для покрытия кода. Мы знаем о более широкой философской дискуссии о покрытии кода: Какой код следует / не следует охватывать? Полезны ли показатели охвата? Эта дискуссия достигла религиозных масштабов. Следовательно, цель не состоит в том, чтобы убедить вас достичь каких-то произвольных целей по покрытию кода. Все, что мы говорим, это следующее: если вы считали, что точность отчетов регистрации имеет основополагающее значение для правильности вашего приложения, и поэтому были готовы инвестировать время и ресурсы в обеспечение его эффективности, то Zlg делает это возможным. Это все; покрывать или нет - выбор полностью за вами.

Дизайн Zlg в значительной степени основан на интерфейсе, чтобы упростить имитацию и тестирование, что само по себе позволяет нам поддерживать Zlg со 100% покрытием инструкций и переходов. Даже с интерфейсами использование макетных фреймворков (например, Mockito) не кажется естественным подходом для создания цепочек в свободном стиле - существует слишком много методов для имитации, и проверка должна учитывать глубину. (Это, вероятно, единственный практический недостаток плавного связывания.)

Короче говоря, рекомендуемый способ насмешки над Zlg - использовать MockLogTarget. Примеры ниже.

// sink for our mock logs
final MockLogTarget target = new MockLogTarget();
// pipe logs to a mock target
final Zlg zlg = target.logger();
// do some logging...
zlg.t("Pi is %.2f", z -> z.arg(Math.PI).tag("math"));
zlg.d("Euler's number is %.2f", z -> z.arg(Math.E).tag("math"));
zlg.c("Avogadro constant is %.3e", 
      z -> z.arg(6.02214086e23).tag("chemistry"));
zlg.w("An I/O error has occurred", 
      z -> z.threw(new FileNotFoundException()));
// find entries tagged with 'math'
final List<Entry> math = target.entries().tagged("math").list();
System.out.println(math);
// find entries at or above debug
final List<Entry> debugAndAbove = 
    target.entries().forLevelAndAbove(LogLevel.DEBUG).list();
System.out.println(debugAndAbove);
// find entries containing an IOException (or subclass thereof)
final List<Entry> withException = 
    target.entries().withException(IOException.class).list();
System.out.println(withException);
// count number of entries containing the substring 'is'
System.out.println("Entries containing 'is': " + 
                   target.entries().containing("is").count());
// assert that only one entry contained an exception
target.entries().withThrowable().assertCount(1);
// of all the tagged entries, assert that at most two 
// weren't tagged 'chemistry'
target.entries().tagged().not()
    .tagged("chemistry").assertCountAtMost(2);

Примечание. Если все, что вам нужно, это неактивный регистратор, чтобы заморозить любой потенциальный вывод, и вас не волнует имитация непрерывной цепочки вызовов, просто используйте Zlg.nop(), чтобы получить тихий регистратор.

Могу ли я использовать Mockito для имитации Zlg?

Хотя Mockito используется внутри компании для тестирования дневного света Zlg, рекомендуется использовать MockLogTarget исключительно для имитации регистратора на уровне приложения. Если вы действительно должны использовать фреймворк фиксации, вот пример имитации различных частей цепочки журналов с помощью Mockito 2.18:

final Zlg zlg = mock(Zlg.class, Answers.CALLS_REAL_METHODS);
final LogChain logChain = mock(LogChain.class, 
                               Answers.CALLS_REAL_METHODS);
when(logChain.format(any())).thenReturn(logChain);
when(logChain.arg(anyDouble())).thenReturn(logChain);
when(zlg.level(anyInt())).thenReturn(logChain);
zlg.t("the value of Pi is %.2f", z -> z.arg(Math.PI));
verify(logChain).format(contains("the value of Pi"));
verify(logChain).arg(eq(Math.PI));
verify(logChain).flush();

Ведение журнала с нулевыми накладными расходами

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

Если субнаносекундные штрафы для подавленных журналов по-прежнему слишком высоки и вам требуется истинный ноль, ваш единственный вариант - полностью исключить инструкции по ведению журнала. К счастью, во время компиляции этого делать не нужно; JIT DCE (устранение мертвого кода) может сделать это за вас на лету. Есть несколько паттернов, которые по-разному добиваются нулевого следа.

1. Ветвление по статической константе - приведет к DCE для одной из ветвей. Пример:

private static final Zlg zlg = Zlg.forDeclaringClass().get();private static final boolean TRACE_ENABLED = false;
public static void withStaticConstant(String address, 
                                      int port, double timeout) {
  if (TRACE_ENABLED) {
    zlg.t("Connecting to %s:%d [timeout: %.1f sec]", 
          z -> z.arg(address).arg(port).arg(timeout));
  }
}

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

2. Утверждения - при запуске с -ea будут оцениваться инструкции по ведению журнала; в противном случае они будут DCE'ed. Пример:

private static final Zlg zlg = Zlg.forDeclaringClass().get();
public static void withAssert(String address, int port, 
                              double timeout) {
  assert zlg.level(LogLevel.TRACE)
    .format("Connecting to %s:%d [timeout: %.1f sec]")
    .arg(address).arg(port).arg(timeout).log();
}

Вместо того, чтобы связывать аргументы в лямбде, в примере утверждения используется немного более длинный, непрерывный стиль цепочки, кульминацией которого является вызов log(), который возвращает константу true. Если утверждения разрешены с помощью аргумента -ea JVM, инструкция журнала будет оценена и никогда не приведет к ошибке утверждения. В противном случае DCE отбросит всю цепочку fluent.

Этот подход следует предпочесть, если нацелены на нулевые накладные расходы для производственного использования при сохранении включенного ведения журнала в тестовых сборках. Флаг -ea, естественно, облегчает это различие, не заставляя вас изменять свой класс перед сборкой. В любом случае вы пожертвуете покрытием кода, поскольку оба метода вводят паразитную инструкцию ветвления за кулисами; во время теста пройден только один путь. (Поскольку метод log() никогда не возвращает false, полное покрытие ветки невозможно.)

Примечание. За пределами примера assert использование стиля непрерывной цепочки настоятельно не рекомендуется. Вы рискуете забыть добавить последний log() в конец цепочки, что приведет к «проглатыванию» журнала без пересылки события журнала в базовый регистратор.

Преждевременная оптимизация - корень всех зол. Эта знаменитая цитата сэра Тони Хора (популяризированная Дональдом Кнутом) стала основной практикой среди инженеров-программистов. К сожалению, как и в случае со многими идеями, которые стали легендарными, первоначальный смысл этого утверждения был почти утерян, и современные инженеры-программисты применяют это высказывание иначе, чем его первоначальный замысел. По мере роста мощности компьютерных систем производительность программного обеспечения отошла на второй план по сравнению с другими проблемами. Сегодня инженеры-программисты нередко расширяют изречение Хоара до категоричной «никогда не следует оптимизировать свой код».

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

Была ли эта статья вам полезна? Я хотел бы услышать ваши отзывы, так что не сдерживайтесь! Если вас интересует Kafka или трансляция событий, или у вас есть вопросы, подписывайтесь на меня в Twitter. Я также являюсь сопровождающим Kafdrop и автором Effective Kafka.