Вступление
Думаю, ни для кого не секрет, что такое логгеры и для чего они нужны. За время существования java было создано немало фреймворков логгирования. Среди самых известных можно выделить:
- JUL — java.util.logging
- log4j
- JCL — jakarta commons logging
- Logback
- SLF4J — simple logging facade for java
В данной статье будет рассмотрен каждый из указанных выше фреймворков на уровне «hello world». Будут приведены простые примеры использования основного функционала и конфигурирования. Статья не преследует цель сравнения логгеров между собой и выявление лучшего из них, эту возможность автор оставляет за вами, уважаемые читатели. В конце статьи будут приведены источники, где можно получить более детальную информацию по каждому фреймворку. Также перед прочтением данной статьи рекомендую ознакомиться с публикацией «Java Logging: история кошмара», где описана история развития систем логгирования в Java.
System.err.println
Первым и самым примитивным способом логгирования был метод System.err.println. Думаю, комментарии излишние, достаточно взглянуть на приведенный ниже код:
// Определяем файл в который будем писать лог System.setErr(new PrintStream(new File("log.txt"))); // Выводим сообщения System.err.println("Сообщение 1"); System.err.println("Сообщение 2"); // Выводим сообщение об ошибке try { throw new Exception("Сообщение об ошибке"); } catch (Exception e) { e.printStackTrace(); }
Java.util.logging
Данный фреймворк включен в стандарт и поставляется вместе с JDK, поэтому ничего дополнительно скачивать и подключать вам не надо. JUL имеет следующие уровни логгирования по возрастанию: FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, а так же ALL и OFF, включающий и отключающий все уровни соответственно.
Логгер создается вызовом одного из статических методов класса java.util.logging.Logger:
Logger log = Logger.getLogger(LoggingJul.class.getName());
Методы логгера могут принимать в качестве аргументов строковые сообщения, шаблоны сообщений, исключения, ресурсы локализованных текстовок сообщений, а также, начиная с Java 8, поставщиков строковых сообщений:
// Строковое сообщение String stringMessage = "Сообщение"; // Строковое сообщение с параметрами String stringMessageFormat ="Сообщение {0}"; // Исключение Throwable throwable = new Throwable(); // ResourceBundle хранящий сообщения ResourceBundle resourceBundle = ResourceBundle.getBundle("logging.jul.bundle"); // Поставщик сообщений Supplier<String> stringMessageSupplier = ()->"Сообщение";
Выделяется две группы методов: название которых соответствует уровню логгирования и методы log, loggp, logrb, принимающие уровень логгирования в качестве параметра с типом Level. Первая группа содержит методы двух типов: принимающих строковое сообщение или поставщика строковых сообщений:
log.info(stringMessage); log.info(stringMessageSupplier);
Вторая группа методов имеет следующие вариации:
// Вывести сообщение с указанием уровня логгирования log.log(new LogRecord(Level.INFO, stringMessage)); log.log(Level.INFO, stringMessage); log.log(Level.INFO, stringMessageSupplier); log.log(Level.INFO, stringMessageFormat, args); log.log(Level.INFO, stringMessage, throwable ); log.log(Level.INFO, throwable, stringMessageSupplier); // Вывести сообщение с указанием уровня логгирования, класса и метода log.logp(Level.INFO, "ClassName", "MethodName", stringMessage); log.logp(Level.INFO, "ClassName", "MethodName", stringMessageSupplier); log.logp(Level.INFO, "ClassName", "MethodName", stringMessageFormat, args); log.logp(Level.INFO, "ClassName", "MethodName", stringMessage, throwable); log.logp(Level.INFO, "ClassName", "MethodName", throwable, stringMessageSupplier); // Вывести сообщение с указанием уровня логгирования, класса, // метода и resourceBundle, хранящего сообщения log.logrb(Level.INFO, "ClassName", "MethodName", resourceBundle, "messageId"); log.logrb(Level.INFO, "ClassName", "MethodName", resourceBundle, "messageId", throwable); // Вывести сообщение об ошибке log.throwing("ClassName","MethodName", throwable);
Теперь обратимся к конфигурации фреймворка. По умолчанию JUL будет выводить сообщения на консоль, однако можно задать конфигурацию в файле свойств. Для задания способа вывода сообщений необходимо для вашего логгера указать какие хендлеры он будет использовать. Существует следующие классы хендлеров: FileHandler, ConsoleHandler, StreamHandler, SocketHandler, MemoryHandler. Особенностью JUL является то, что настройки хендлеров задаются в целом для всего класса, а не для конкретного экземпляра, что может порождать не мало проблем, например если вам потребуется сообщения различных логгеров выводить в различные файлы или с различным форматированием. Рассмотрим простой пример конфигурационного файла:
# Настройки глобального логгера handlers =java.util.logging. FileHandler .level=ALL # Конфигурация файлового хендлера java.util.logging.FileHandler.level =ALL java.util.logging.FileHandler.formatter =java.util.logging.SimpleFormatter java.util.logging.FileHandler.limit = 1000000 java.util.logging.FileHandler.pattern = log.txt # Конфигурация консольного хендлера java.util.logging.ConsoleHandler.level = ALL java.util.logging.ConsoleHandler.pattern = log.log java.util.logging.ConsoleHandler.formatter =java.util.logging.SimpleFormatter
Для того что бы JUL применил данную конфигурацию нужно передать параметр -Djava.util.logging.config.file = <путь до файла>, либо при старте приложения выполнить код:
LogManager.getLogManager().readConfiguration(<ваш класс>.class.getResourceAsStream("logging.properties"));
Log4j
Данный фреймворк на текущий момент имеет уже вторую версию, которая увы не совместима с первой. Поскольку первая версия log4j существует достаточно давно и, в виду ее большой популярности, существует не мало статей на просторах интернета, сегодня мы рассмотрим вторую. Для использования log4j2 вам необходимо подключить библиотеки log4j-api-2.x и log4j-core-2.x. Log4j имеет несколько отличное от JUL именование уровней логгирования: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, а так же ALL и OFF включающий и отключающий все уровни соответственно.
Логгер создается вызовом статического метода класса org.apache.logging.log4j.Logger:
Logger log = LogManager.getLogger(LoggingLog4j.class); // или Logger log = LogManager.getLogger(“name”);
Логгер умеет принимать помимо привычных нам String, Object и Throwable еще два новых типа — MapMessage и Marker:
// Карта сообщений (напечатается как msg1="Сообщение 1” msg2="Сообщение 2”) MapMessage mapMessage = new MapMessage(); mapMessage.put("msg1", "Сообщение 1"); mapMessage.put("msg2", "Сообщение 2"); // Маркер, объект по которому можно фильтровать сообщения Marker marker = MarkerManager.getMarker("fileonly"); // Строковое сообщение String stringMessage = "Сообщение"; // Строковое сообщение с параметрами String stringMessageFormat = "Сообщение {}, от {}"; // Исключение Throwable throwable = new Throwable(); // Объект Object object = new Object();
В классическом для логгеров стиле методы делятся на два типа: совпадающие с названием уровня логгирования и методы log, принимающие уровень логгирования в качестве параметра. Первые имеют вид:
log.info(mapMessage); log.info(object); log.info(stringMessage); log.info(marker, mapMessage); log.info(marker, object); log.info(marker, stringMessage); log.info(object, throwable); log.info(stringMessage, throwable); log.info(stringMessageFormat, args); log.info(marker, mapMessage, throwable); log.info(marker, object, throwable); log.info(marker, stringMessageFormat, args); log.info(marker, stringMessage, throwable); log.throwing(throwable);
Методы log в log4j2 выглядят так:
log.log(Level.INFO, mapMessage); log.log(Level.INFO, object); log.log(Level.INFO, stringMessage); log.log(Level.INFO, marker, mapMessage); log.log(Level.INFO, marker, object); log.log(Level.INFO, marker, stringMessage); log.log(Level.INFO, object, throwable); log.log(Level.INFO, stringMessageFormat, args); log.log(Level.INFO, stringMessage, throwable); log.log(Level.INFO, marker, mapMessage, throwable); log.log(Level.INFO, marker, object, throwable); log.log(Level.INFO, marker, stringMessageFormat, args); log.log(Level.INFO, marker, stringMessage, throwable); log.throwing(Level.INFO, throwable);
Если не определить конфигурацию, то при запуске log4j2 выдаст гневное сообщение, о том, что конфигурация не задана и будет печатать ваши сообщения на консоль уровнем не ниже ERROR. Конфигурация log4j2 задается несколькими вариантами: xml, json, yaml. Стоит отметить, что со второй версии нет поддержки конфигурации из property файла. Файл с конфигурацией автоматически ищется classpath, должен иметь название log4j2 и располагаться в пакете по умолчанию.
Конфигурация log4j2 состоит из описания логгеров, аппендеров и фильтров. Для более детального изучения обратитесь к документации, сейчас же лишь отметим пару ключевых моментов. Во-первых, есть различные вкусности в виде фильтров, в том числе и по маркерам:
- BurstFilter
- CompositeFilter
- DynamicThresholdFilter
- MapFilter
- MarkerFilter
- RegexFilter
- StructuredDataFilter
- ThreadContextMapFilter
- ThresholdFilter
- TimeFilter
Во-вторых, имеется широкий круг классов аппендеров, в том числе асинхронные аппендеры и аппендеры оборачивающие группу других аппендеров:
- AsyncAppender
- ConsoleAppender
- FailoverAppender
- FileAppender
- FlumeAppender
- JDBCAppender
- JMSAppender
- JPAAppender
- MemoryMappedFileAppender
- NoSQLAppender
- OutputStreamAppender
- RandomAccessFileAppender
- RewriteAppender
- RollingFileAppender
- RollingRandomAccessFileAppender
- RoutingAppender
- SMTPAppender
- SocketAppender
- SyslogAppender
Стоит также заметить, что log4j может создавать множество различающихся аппендеров одного и того же класса, например несколько файловых аппендеров, которые пишут в разные файлы.
Рассмотрим пример конфигурации, в которой объявлены два логгера (корневой и для нашего класса), первый из которых пишет в файл log.log, а второй пишет в log2.log с использованием фильтрации по маркеру:
<?xml version="1.0" encoding="UTF-8"?> <Configuration> <!-- Секция аппендеров --> <Appenders> <!-- Файловый аппендер --> <File name="file" fileName="log.log"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m %ex%n</Pattern> </PatternLayout> </File> <!-- Файловый аппендер --> <File name="file2" fileName="log2.log"> <!-- Фильтр по маркеру --> <MarkerFilter marker="fileonly" onMatch="DENY" onMismatch="ACCEPT"/> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m %ex%n</Pattern> </PatternLayout> </File> </Appenders> <!-- Секция логгеров --> <Loggers> <!-- Корневой логгер --> <Root level="trace"> <AppenderRef ref="file" level="DEBUG"/> </Root> <!-- Логгер нашего класса --> <Logger name="logging.log4j.LoggingLog4j" level="info" additivity="false"> <AppenderRef ref="file2" level="INFO"/> </Logger> </Loggers> </Configuration>
Commons-logging
Довольно старый проект, который представляет собой обертку над JUL и log4j, не привносящая никакого дополнительного функционала. Уровни логгирования у JCL совпадают с log4j, а в случае взаимодействия с JUL происходит следующее сопоставление:
fatal = Level.SEVERE error = Level.SEVERE warn = Level.WARNING info = Level.INFO debug = Level.FINE trace = Level.FINEST
Для использования JCL подключаем commons-logging-1.x.jar. Создаем логгер вызовом метода фабрики:
Log log = LogFactory.getLog(LoggingCl.class); // или Log log = LogFactory.getLog("name");
Методы JCL очень простые, совпадают с названием уровней логгирования, принимают только объекты и исключения и имеют две вариации:
Object object = "Сообщение"; Throwable throwable = new Throwable(); log.info(object); log.info(object, throwable);
Конфигурация JCL содержит отдельные блоки для log4j, JUL и собственной реализации. Если не задать конфигурацию, то используется собственная реализация, именуемая SimpleLog, которая выводит сообщения на консоль. Рассмотрим пример конфигурационного файла:
#Log4j org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger log4j.configuration=log4j.properties #JUL org.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler .level=INFO java.util.logging.FileHandler.pattern=jul.log java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter java.util.logging.FileHandler.limit=50000 java.util.logging.FileHandler.count=1 #SimpleLog org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog org.apache.commons.logging.simplelog.defaultlog=fatal org.apache.commons.logging.simplelog.showlogname=true org.apache.commons.logging.simplelog.showShortLogname=true org.apache.commons.logging.simplelog.showdatetime=true
Указать файл конфигурации JCL можно следующим образом:
java -Djava.util.logging.config.file=/absolute/path/to/your/config/file/commons-logging.properties -jar /absolute/path/to/your/jar/file/MyClass.jar
Logback
Данный фреймворк используется только в связке с оберткой SLF4J, которую мы будем рассматривать позднее. Для начала работы вам необходимы logback-core-1.x.jar и logback-classic-1.x.x.jar, а также slf4j-api-1.x.x.jar.
Взаимодействие с логгером мы будем осуществлять через API предоставляемый оберткой SLF4J. Уровни логгирования совпадают с log4j. Создание логгера в таком случае выглядит следующим образом:
org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LoggingLogback.class); // или org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger("name");
API позволяет выводить строковые сообщения, шаблоны строковых сообщений, исключения, а также использовать маркеры:
// Строковое сообщение String stringMessage = "Сообщение"; // Шаблон сообщения String stringMessageFormat = "Сообщение {} {}"; // Ошибка Throwable throwable = new Throwable(); // Маркер Marker marker = MarkerFactory.getMarker("marker");
Названия методов совпадают с уровнями логгирования и имеют вид:
log.info(stringMessage); log.info(stringMessageFormat, args); log.info(stringMessage, throwable); log.info(marker, stringMessage); log.info(marker, stringMessage, throwable); log.info(marker,stringMessageFormat, args);
Теперь рассмотрим непосредственны функционал logback. Конфигурация ищется в classpath в следующем порядке:
- Пытается найти logback.groovy
- Иначе пытается найти logback-test.xml
- Иначе пытается найти logback.xml
- Иначе использует базовую конфигурацию — выводим сообщения на консоль
Основными элементами конфигурации являются логгеры, аппендеры, лайауты, и фильтры.
Имеются следующие фильтры:
- Regular filters
- LevelFilter
- ThresholdFilter
- EvaluatorFilter
- Matchers
- TurboFilters
- CountingFilter
Имеются следующие аппендеры:
- OutputStreamAppender
- ConsoleAppender
- FileAppender
- RollingFileAppender
- SocketAppender and SSLSocketAppender
- ServerSocketAppender and SSLServerSocketAppender
- SMTPAppender
- SyslogAppender
- SiftingAppender
- AsyncAppender
О том что такое Layouts и Encoders в logback предлагаю прочитать подробно в документации, а сейчас лишь приведу простой пример файла logback.xml:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!--Аппендеры --> <!--Файловый аппендер --> <appender name="file" class="ch.qos.logback.core.FileAppender"> <file>log.log</file> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</Pattern> </layout> </appender> <!--Консольный аппендер --> <appender name="sout" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern> </layout> </appender> <!-- Фильтры --> <!-- Фильтр по маркеру --> <turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter"> <Marker>marker</Marker> <OnMatch>DENY</OnMatch> </turboFilter> <!-- Логгеры --> <!-- Корневой логгер --> <root level="info"> <appender-ref ref="file" /> </root> <!-- Логгер нашего класса --> <logger name="logging.logback.LoggingLogback" level="info" > <appender-ref ref="sout" /> </logger> </configuration>
SLF4J
Как уже говорилось ранее SLF4J является оберткой над logback, а также над JUL, log4j, или JCL, а также над любым логгером, который реализует ее интерфейс. Для работы с SLF4J нужны библиотека slf4j-api-1.x.x.jar и реализация одного из логгеров либо заглушка. Как правило реализации всех логгеров ( кроме logback) поставляются вместе с SLF4J и имеют названия на подобии slf4j-jcl-1.x.jar, slf4j-log4j12-1.x.jar, slf4j-nop-1.x.jar и т.п. Если в classpath не будет найдена реализация логгера ( или заглушка nop) SLF4J гневно ругнется и работать откажется. Конфигурация соответственно будет искаться в зависимости от положенной в classpath реализации.
API SLF4J мы рассмотрели в предыдущем пункте, поэтому давайте рассмотрим еще одну возможность обертки. В идеальном мире мы должны выводить сообщения через интерфейс обертки, и тогда у нас все будет хорошо, но реальный жестокий мир говорит о том, что всем нам приходится взаимодействовать со сторонними библиотеками или кодом, в которых используются другие логгеры и которые знать не знают о SLF4J. Что бы не подстраиваться под каждый логгер, а пустить все сообщения через одну реализацию интерфейса SLF4J, можно использовать bridging. В поставке обертки содержаться библиотеки jcl-over-slf4j.jar, log4j-over-slf4j.jar и jul-to-slf4j.jar, которые переопределяют поведение соответствующих логгеров и перенаправляют сообщения в обертку.
Что бы стало понятнее выше сказанное, рассмотрим пример. Допустим у нас имеются следующие логгеры:
java.util.logging.Logger julLog = java.util.logging.Logger.getLogger("julLog"); java.util.logging.Logger log4jLog = java.util.logging.Logger.getLogger("log4jLog"); org.slf4j.Logger slf4jLog = org.slf4j.LoggerFactory.getLogger(LoggingSlf4j.class); julLog.info("Сообщение от jul"); log4jLog.info("Сообщение от log4j"); slf4jLog.info("Сообщение от slf4j");
Мы хотим, что бы сообщение от JUL записывались в один файл, от log4j в другой, а от slf4j выводились на консоль. В качестве реализации обертки будем использовать logback, конфигурация сего безобразия будет выглядеть следующим образом:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!--Аппендеры --> <!--Файловый аппендер для JUL --> <appender name="jul" class="ch.qos.logback.core.FileAppender"> <file>log_jul.log</file> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</Pattern> </layout> </appender> <!--Файловый аппендер для log4j --> <appender name="log4j" class="ch.qos.logback.core.FileAppender"> <file>log_log4j.log</file> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</Pattern> </layout> </appender> <!--Консольный аппендер --> <appender name="sout" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern> </layout> </appender> <!-- Логгеры --> <!-- Корневой логгер --> <root level="info" > <appender-ref ref="sout" /> </root> <!-- Логгер для jul --> <logger name="julLog" additivity="false" > <level value="trace" /> <appender-ref ref="jul" /> </logger> <!-- Логгер для log4j --> <logger name="log4jLog" additivity="false" > <level value="trace" /> <appender-ref ref="log4j" /> </logger> </configuration>
Для того, что бы мост заработал необходимо выполнить код:
SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install();
Хочу узнать больше
- JUL — читать Java Logging: Logger и Java Logging: Configuration
- Log4j2 — читать Welcome to Log4j 2
- JCL- читать How to use Commons Logging
- Logback- читать The logback manual
- SLF4J — читать SLF4J documentation и Bridging legacy APIs
Заключение
В заключение хотелось бы вам сказать, что конечный выбор фреймворка логгирования остается всегда за вами, но к этому надо подходить здраво. Выбор должен обуславливаться удовлетворением многих критериев, как высокая производительность, удобный API, наличие нужных способов хранения логгируемых данных, так и спецификой ваших проектов, например, если ваш продукт будет использоваться в других проектах, то не стоит решать за пользователя каким логгером ему придется пользоваться, а в место этого отдать предпочтение обертке.
