1. Обзор
Logback — одна из наиболее широко используемых сред ведения журналов в сообществе Java. Это замена своего предшественника Log4j. Logback предлагает более быструю реализацию, предоставляет больше возможностей для настройки и большую гибкость в архивировании старых файлов журналов.
В этом руководстве мы познакомимся с архитектурой Logback и рассмотрим, как мы можем использовать ее для улучшения наших приложений.
2. Логбэк-архитектура
Архитектура Logback состоит из трех классов: Logger
, Appender
и Layout
.
Регистратор — это контекст
для сообщений журнала. Это класс, с которым взаимодействуют приложения для создания сообщений журнала.
Appenders
помещают сообщения журнала в свои конечные места назначения. Logger
может иметь более одного
Appender . Обычно мы думаем, что Appenders
прикреплены к текстовым файлам, но Logback гораздо более эффективен, чем это.
Макет
подготавливает сообщения к выводу. Logback поддерживает создание пользовательских классов для форматирования сообщений, а также надежные параметры конфигурации для существующих.
3. Настройка
3.1. Зависимость от Maven
Logback использует Simple Logging Facade для Java (SLF4J) в качестве собственного интерфейса. Прежде чем мы сможем начать регистрировать сообщения, нам нужно добавить Logback и SLF4J в наш pom.xml
:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
<scope>test</scope>
</dependency>
Maven Central имеет последнюю версию Logback Core и самую последнюю версию slf4j-api
.
3.2. Путь к классам
Logback также требует logback-classic.jar
в пути к классам для среды выполнения.
Мы добавим это в pom.xml
в качестве тестовой зависимости:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.6</version>
</dependency>
4. Базовый пример и конфигурация
Давайте начнем с быстрого примера использования Logback в приложении.
Во-первых, нам нужен файл конфигурации. Мы создадим текстовый файл с именем logback.xml
и поместим его куда-нибудь в наш путь к классам:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Далее нам нужен простой класс с основным
методом:
public class Example {
private static final Logger logger
= LoggerFactory.getLogger(Example.class);
public static void main(String[] args) {
logger.info("Example log from {}", Example.class.getSimpleName());
}
}
Этот класс создает Logger
и вызывает info()
для создания сообщения журнала.
Когда мы запускаем пример,
мы видим, что наше сообщение выводится на консоль:
20:34:22.136 [main] INFO Example - Example log from Example
Легко понять, почему Logback так популярен; мы в считанные минуты.
Эта конфигурация и код дают нам несколько советов о том, как это работает:
- У нас есть приложение с именем
STDOUT
, которое ссылается на имя классаConsoleAppender
.
- Существует шаблон, который описывает формат нашего сообщения журнала.
- Наш код создает
Logger
, и мы передаем ему наше сообщение через методinfo()
.
Теперь, когда мы поняли основы, давайте посмотрим поближе.
5. Контексты регистратора
5.1. Создание контекста
Чтобы записать сообщение в Logback, мы инициализируем Logger
из SLF4J или Logback:
private static final Logger logger
= LoggerFactory.getLogger(Example.class);
Затем мы используем его:
logger.info("Example log from {}", Example.class.getSimpleName());
Это наш контекст ведения журнала. Когда мы его создали, мы передали LoggerFactory
наш класс. Это дает регистратору
имя (есть также перегрузка, которая принимает строку).
Контексты ведения журнала существуют в иерархии, очень похожей на иерархию объектов Java:
-
Регистратор
является предком, если его имя, за которым следует точка, предшествует именирегистратора
- потомка . -
Регистратор
является родителем, когда между ним и дочерним элементом нет предков.
Например, класс Example ниже находится в пакете
com.foreach.logback
. В пакете com.foreach.logback.appenders
есть еще один класс с именем ExampleAppender
. ``
Регистратор ExampleAppender
является дочерним элементом регистратора Example.
Все регистраторы
являются потомками предопределенного корневого регистратора.
У регистратора
есть уровень,
который можно установить либо с помощью конфигурации, либо с помощью Logger.setLevel().
Установка уровня в коде переопределяет файлы конфигурации.
Возможные уровни в порядке приоритета: TRACE, DEBUG, INFO, WARN
и ERROR.
** ** У каждого уровня есть соответствующий метод, который мы используем для регистрации сообщения на этом уровне.
Если регистратору
явно не назначен уровень, он наследует уровень своего ближайшего предка. Корневой регистратор по умолчанию использует DEBUG.
Мы увидим, как переопределить это ниже.
5.2. Использование контекста
Давайте создадим пример программы, которая демонстрирует использование контекста в иерархии журналирования:
ch.qos.logback.classic.Logger parentLogger =
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foreach.logback");
parentLogger.setLevel(Level.INFO);
Logger childlogger =
(ch.qos.logback.classic.Logger)LoggerFactory.getLogger("com.foreach.logback.tests");
parentLogger.warn("This message is logged because WARN > INFO.");
parentLogger.debug("This message is not logged because DEBUG < INFO.");
childlogger.info("INFO == INFO");
childlogger.debug("DEBUG < INFO");
Когда мы запускаем это, мы видим эти сообщения:
20:31:29.586 [main] WARN com.foreach.logback - This message is logged because WARN > INFO.
20:31:29.594 [main] INFO com.foreach.logback.tests - INFO == INFO
Начнем с извлечения Logger
с именем com.foreach.logback
и приведения его к ch.qos.logback.classic.Logger.
Контекст Logback необходим для установки уровня в следующем операторе; обратите внимание, что абстрактный регистратор
SLF4J не реализует setLevel().
Мы устанавливаем уровень нашего контекста на INFO
. Затем мы создаем еще один регистратор
с именем com.foreach.logback.tests.
Наконец, мы регистрируем два сообщения с каждым контекстом, чтобы продемонстрировать иерархию. Logback регистрирует сообщения WARN
и INFO
и фильтрует сообщения DEBUG
** ** .
Теперь воспользуемся корневым регистратором:
ch.qos.logback.classic.Logger logger =
(ch.qos.logback.classic.Logger)LoggerFactory.getLogger("com.foreach.logback");
logger.debug("Hi there!");
Logger rootLogger =
(ch.qos.logback.classic.Logger)LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
logger.debug("This message is logged because DEBUG == DEBUG.");
rootLogger.setLevel(Level.ERROR);
logger.warn("This message is not logged because WARN < ERROR.");
logger.error("This is logged.");
Мы видим эти сообщения, когда выполняем этот фрагмент:
20:44:44.241 [main] DEBUG com.foreach.logback - Hi there!
20:44:44.243 [main] DEBUG com.foreach.logback - This message is logged because DEBUG == DEBUG.
20:44:44.243 [main] ERROR com.foreach.logback - This is logged.
Подводя итог, мы начали с контекста Logger
и напечатали сообщение DEBUG
.
Затем мы получили корневой регистратор, используя его статически определенное имя, и установили его уровень на ERROR.
Наконец, мы продемонстрировали, что Logback действительно фильтрует любые операторы, кроме ошибок.
5.3. Параметризованные сообщения
В отличие от сообщений в примерах фрагментов выше, наиболее полезные сообщения журнала требуют добавления строк.
Это влечет за собой выделение памяти, сериализацию объектов, объединение строк
и, возможно, последующую очистку мусора.
Рассмотрим следующее сообщение:
log.debug("Current count is " + count);
Мы берем на себя затраты на создание сообщения независимо от того, регистрирует ли Logger сообщение или нет.
Logback предлагает альтернативу с параметризованными сообщениями:
log.debug("Current count is {}", count);
Скобки {} будут принимать любой объект
и использовать его метод toString()
для создания сообщения только после проверки того, что сообщение журнала требуется.
Давайте попробуем несколько других параметров:
String message = "This is a String";
Integer zero = 0;
try {
logger.debug("Logging message: {}", message);
logger.debug("Going to divide {} by {}", 42, zero);
int result = 42 / zero;
} catch (Exception e) {
logger.error("Error dividing {} by {} ", 42, zero, e);
}
Этот фрагмент дает:
21:32:10.311 [main] DEBUG com.foreach.logback.LogbackTests - Logging message: This is a String
21:32:10.316 [main] DEBUG com.foreach.logback.LogbackTests - Going to divide 42 by 0
21:32:10.316 [main] ERROR com.foreach.logback.LogbackTests - Error dividing 42 by 0
java.lang.ArithmeticException: / by zero
at com.foreach.logback.LogbackTests.givenParameters_ValuesLogged(LogbackTests.java:64)
...
Мы видим, как String,
int и
Integer могут
быть переданы в качестве параметров.
Кроме того, когда Exception
передается в качестве последнего аргумента метода ведения журнала, Logback распечатывает для нас трассировку стека.
6. Подробная конфигурация
В предыдущих примерах мы использовали файл конфигурации из 11 строк, созданный в разделе 4 , для вывода сообщений журнала на консоль. Это поведение Logback по умолчанию; если он не может найти файл конфигурации, он создает ConsoleAppender
и связывает его с корневым регистратором.
6.1. Поиск информации о конфигурации
Файл конфигурации можно поместить в путь к классам и назвать либо logback.xml,
либо logback-test.xml.
Вот как Logback попытается найти данные конфигурации:
- Найдите файлы с именами
logback-test.xml
, logback.groovy
** ** илиlogback.xml
в пути к классам в указанном порядке . - Если библиотека не найдет эти файлы, она попытается использовать Java
ServiceLoader
, чтобы найти средство реализацииcom.qos.logback.classic.spi.Configurator.
- Настройте себя для вывода лога непосредственно на консоль
Примечание: текущая версия Logback не поддерживает конфигурацию Groovy, поскольку не существует версии Groovy, совместимой с Java 9.
6.2. Базовая конфигурация
Давайте подробнее рассмотрим нашу примерную конфигурацию.
Весь файл находится в тегах <configuration> .
** **
Мы видим тег, который объявляет Appender
типа ConsoleAppender
и называет его STDOUT
. Внутри этого тега находится кодировщик. У него есть шаблон с escape-кодами в стиле sprintf :
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
Наконец, мы видим корневой
тег. Этот тег переводит корневой регистратор в режим DEBUG и связывает его вывод с
Appender
с именем STDOUT
:
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
6.3. Устранение неполадок конфигурации
Файлы конфигурации журнала могут быть сложными, поэтому существует несколько встроенных механизмов для устранения неполадок.
Чтобы видеть отладочную информацию по мере того, как Logback обрабатывает конфигурацию, мы можем включить ведение журнала отладки:
<configuration debug="true">
...
</configuration>
Logback будет печатать информацию о состоянии на консоли по мере обработки конфигурации:
23:54:23,040 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback-test.xml]
at [file:/Users/egoebelbecker/ideaProjects/logback-guide/out/test/resources/logback-test.xml]
23:54:23,230 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender
of type [ch.qos.logback.core.ConsoleAppender]
23:54:23,236 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
23:54:23,247 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type
[ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
23:54:23,308 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to DEBUG
23:54:23,309 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
23:54:23,310 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
23:54:23,313 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@5afa04c - Registering current configuration
as safe fallback point
Если при анализе файла конфигурации возникают предупреждения или ошибки, Logback записывает сообщения о состоянии на консоль.
Существует второй механизм вывода информации о состоянии:
<configuration>
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
...
</configuration>
StatusListener перехватывает сообщения о
состоянии и выводит их во время настройки, а также во время работы программы .
Вывод всех конфигурационных файлов выводится на печать, что делает его полезным для поиска «мошеннических» конфигурационных файлов в пути к классам.
6.4. Автоматическая перезагрузка конфигурации
Перезагрузка конфигурации ведения журнала во время работы приложения является мощным средством устранения неполадок. Logback делает это возможным с параметром сканирования
:
<configuration scan="true">
...
</configuration>
Поведение по умолчанию — сканирование файла конфигурации на наличие изменений каждые 60 секунд. Мы можем изменить этот интервал, добавив scanPeriod
:
<configuration scan="true" scanPeriod="15 seconds">
...
</configuration>
Мы можем указать значения в миллисекундах, секундах, минутах или часах.
6.5. Изменение регистраторов
В нашем примере файла выше мы установили уровень корневого регистратора и связали его с консольным Appender
.
Мы можем установить уровень для любого регистратора
:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.foreach.logback" level="INFO" />
<logger name="com.foreach.logback.tests" level="WARN" />
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Давайте добавим это в наш путь к классам и запустим код:
Logger foobar =
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foreach.foobar");
Logger logger =
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foreach.logback");
Logger testslogger =
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foreach.logback.tests");
foobar.debug("This is logged from foobar");
logger.debug("This is not logged from logger");
logger.info("This is logged from logger");
testslogger.info("This is not logged from tests");
testslogger.warn("This is logged from tests");
Мы видим этот вывод:
00:29:51.787 [main] DEBUG com.foreach.foobar - This is logged from foobar
00:29:51.789 [main] INFO com.foreach.logback - This is logged from logger
00:29:51.789 [main] WARN com.foreach.logback.tests - This is logged from tests
Не устанавливая уровень наших регистраторов
программно, их устанавливает конфигурация; com.foreach.foobar
наследует DEBUG
от корневого регистратора.
`Регистраторы также наследуют
appender
-ref` от корневого регистратора. Как мы увидим ниже, мы можем переопределить это.
6.6. Замена переменной
Файлы конфигурации logback поддерживают переменные. Мы определяем переменные внутри сценария конфигурации или извне. Переменная может быть указана в любой точке скрипта конфигурации вместо значения.
Например, вот конфигурация для FileAppender
:
<property name="LOG_DIR" value="/var/log/application" />
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${LOG_DIR}/tests.log</file>
<append>true</append>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
В верхней части конфигурации мы объявили свойство
** с именем LOG_DIR.
** Затем мы использовали его как часть пути к файлу внутри определения приложения
.
Свойства объявляются в теге <property>
в сценариях конфигурации, но они также доступны из внешних источников, таких как системные свойства. Мы могли бы опустить объявление свойства
в этом примере и установить значение LOG_DIR
в командной строке:
$ java -DLOG_DIR=/var/log/application com.foreach.logback.LogbackTests
Мы указываем значение свойства с помощью ${propertyname}.
Logback реализует переменные как замену текста. Подстановка переменных может происходить в любой точке конфигурационного файла, где может быть указано значение.
7. Аппендеры
`
Регистраторы передают
LoggingEvents Appenders
.
Appenders выполняют фактическую работу по регистрации. Обычно мы думаем о ведении журнала как о чем-то, что идет в файл или консоль, но Logback способен на гораздо большее.
Logback-core предоставляет несколько полезных
приложений` .
7.1. ConsoleAppender
Мы уже видели ConsoleAppender
** в действии. Несмотря на свое название, ConsoleAppender
добавляет сообщения в System.out
** или System.err.
Он использует OutputStreamWriter
для буферизации ввода-вывода, поэтому его направление в System.err
не приводит к небуферизованной записи.
7.2. FileAppender
FileAppender
** ** добавляет сообщения в файл. Он поддерживает широкий спектр параметров конфигурации. Давайте добавим приложение файла к
нашей базовой конфигурации:
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>tests.log</file>
<append>true</append>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.foreach.logback" level="INFO" />
<logger name="com.foreach.logback.tests" level="WARN">
<appender-ref ref="FILE" />
</logger>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
FileAppender настраивается
с именем файла через <file>.
Тег <append>
** дает указание Appender
** добавить к существующему файлу, а не обрезать его. Если мы запустим тест несколько раз, мы увидим, что вывод журнала добавляется в один и тот же файл.
Если мы повторно запустим наш тест сверху, сообщения из com.foreach.logback.tests
отправятся как на консоль, так и в файл с именемtests.log. Регистратор
- потомок наследует ассоциацию корневого регистратора с ConsoleAppender
с его ассоциацией с FileAppender.
Аппендеры
являются кумулятивными.
Мы можем переопределить это поведение:
<logger name="com.foreach.logback.tests" level="WARN" additivity="false" >
<appender-ref ref="FILE" />
</logger>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
Установка для аддитивности значения
false
** ** отключает поведение по умолчанию. Тесты
не будут записываться в консоль, как и ее потомки.
7.3. RollingFileAppender
Часто добавление сообщений журнала в тот же файл не является тем поведением, которое нам нужно. Мы хотим, чтобы файлы «прокручивались» в зависимости от времени, размера файла журнала или их комбинации.
Для этого у нас есть RollingFileAppender:
<property name="LOG_FILE" value="LogFile" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>
<!-- keep 30 days' worth of history capped at 3GB total size -->
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
RollingFileAppender имеет RollingPolicy
.
В этом примере конфигурации мы видим TimeBasedRollingPolicy.
** `` `` **
Подобно FileAppender,
мы настроили это приложение
с именем файла. Мы объявили свойство и использовали его, потому что мы будем повторно использовать имя файла ниже.
Мы определили fileNamePattern
внутри RollingPolicy.
** Этот шаблон определяет не только имя файлов, но и то, как часто их нужно накатывать. TimeBasedRollingPolicy
** исследует шаблон и выполняет проверку в наиболее точно определенный период.
Например:
<property name="LOG_FILE" value="LogFile" />
<property name="LOG_DIR" value="/var/logs/application" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/${LOG_FILE}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/%d{yyyy/MM}/${LOG_FILE}.gz</fileNamePattern>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
Активный файл журнала — /var/logs/application/LogFile.
** Этот файл в начале каждого месяца заменяется на /Current Year/Current Month/LogFile.gz,
а RollingFileAppender
создает ** новый активный файл.
Когда общий размер заархивированных файлов достигает 3 ГБ, RollingFileAppender
** ** удаляет архивы в порядке поступления.
Есть коды на неделю, час, минуту, секунду и даже миллисекунду. Logback имеет ссылку здесь .
RollingFileAppender
** ** также имеет встроенную поддержку сжатия файлов. Он сжимает наши свернутые файлы, потому что мы назвали их LogFile.gz.
TimeBasedPolicy
** — не единственный вариант переноса файлов. Logback также предлагает SizeAndTimeBasedRollingPolicy,
которая будет прокручиваться на основе текущего размера файла журнала, а также времени. Он также предлагает FixedWindowRollingPolicy,
** который прокручивает имена файлов журнала каждый раз при запуске средства ведения журнала.
Мы также можем написать собственную RollingPolicy .
7.4. Пользовательские приложения
Мы можем создавать собственные приложения , расширяя один из базовых классов приложений
Logback
. У нас есть учебник по созданию пользовательских приложений
здесь .
8. Макеты
Макеты
форматируют сообщения журнала. Как и остальная часть Logback, макеты
** ** расширяемы, и мы можем создавать свои собственные. Тем не менее, PatternLayout
по умолчанию предлагает то, что нужно большинству приложений, а затем и некоторые.
До сих пор мы использовали PatternLayout
во всех наших примерах:
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
Этот сценарий конфигурации содержит конфигурацию для PatternLayoutEncoder.
** Мы передаем Encoder
нашему Appender,
** и этот кодировщик использует PatternLayout
для форматирования сообщений.
Текст в теге <pattern>
определяет, как форматируются сообщения журнала. PatternLayout
реализует большое количество слов преобразования и модификаторов формата для создания шаблонов.
Давайте сломаем этот вниз. PatternLayout
распознает слова преобразования с %, поэтому преобразования в нашем шаблоне генерируют:
%d{HH:mm:ss.SSS}
– метка времени с часами, минутами, секундами и миллисекундами[%thread]
— имя потока, создающего сообщение журнала, заключенное в квадратные скобки.%-5level
— уровень события логирования, дополненный до 5 символов%logger{36}
– имялоггера
, усеченное до 35 символов .%msg%n
— сообщения журнала, за которыми следует символ-разделитель строк, зависящий от платформы.
Итак, мы видим сообщения, похожие на это:
21:32:10.311 [main] DEBUG com.foreach.logback.LogbackTests - Logging message: This is a String
Исчерпывающий список слов преобразования и модификаторов формата можно найти здесь .
9. Заключение
В этой обширной статье мы рассмотрели основы использования Logback в приложении.
Мы рассмотрели три основных компонента архитектуры Logback: Logger
, Appender
и Layout
. Logback имеет мощные сценарии конфигурации, которые мы использовали для управления компонентами для фильтрации и форматирования сообщений. Мы также обсудили два наиболее часто используемых приложения для создания, переноса, организации и сжатия файлов журналов.
Как обычно, фрагменты кода можно найти на GitHub .