← На главную

Памятка по логированию в Scala и вообще мире Java

Ни одно серьезное приложение не обходится без логирования, будь то сайтик, GUI или даже что-нибудь под Android. В мире Java ситуация с логированием на первый взгляд несколько запутанная. Есть какие-то SLF4J, какой-то Logback, зачем-то нужно создавать XML-файлы, вот это все. Но, как это часто бывает, если сесть, и не спеша во всем разобраться, на самом деле все оказывается очень просто и логично.

Итак, есть штука под названием SLF4J. Это что-то вроде интерфейса, который должны поддерживать все библиотеки для логирования. То есть, интерфейс один, а реализаций много. Есть java.util.logging (JUL), Common Logging (JCL), log4j, Logback. Наибольшей популярностью, похоже, пользуется Logback. Так вот, авторы библиотек пишут все логи через SLF4J и ничего не знают о том, какая конкретная реализация используется в том или ином приложении. Авторы программ выбирают конкретную реализацию. SLF4J как-то там сам ее находит, в результате чего весь проект использует эту одну реализацию. Правда, круто?

В случае со Scala есть заслуживающая внимания библиотека scala-logging, являющаяся оберткой над SLF4J. Используя макросы Scala, она добавляет проверку перед записью строки в лог, будет ли при текущих настройках эта строка на самом деле куда-то записана.

То есть, вы пишете:

log.debug(s"Test $msg")

… а после развертывания макроса это превращается в:

if(log.isDebugEnabled) log.debug(s"Test $msg")

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

Давайте попробуем прикрутить логирование к нашему REST-сервису на базе Finagle. В build.sbt дописываем зависимости:

"ch.qos.logback" % "logback-classic" % "1.1.3", "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0"

В конструкторе класса FinagleServiceExample создаем логер:

import com.typesafe.scalalogging._ import org.slf4j.LoggerFactory // ... private val logger = Logger(LoggerFactory.getLogger(this.getClass))

После чего берем и просто пишем логи:

logger.info(s"service started") logger.debug(s"reading $key") logger.error(s"bad request: $req")

Существуют следующие уровни логирования, от меньшего к большему: TRACE, DEBUG, INFO, WARN, ERROR. В настройках, речь о которых пойдет ниже, выбирается эффективный уровень. При выборе определенного эффективного уровня в лог попадают все сообщения этого уровня и выше. Например, если выбран эффективный уровень INFO, в логах вы увидите все INFO, WARN и ERROR сообщения. Кроме того, есть особый эффективный уровень OFF, который отключает все логирование.

Logback ищет файл с настройками по следующим правилам:

  1. Если в classpath есть logback.groovy, используется он;
  2. Иначе в classpath ищется файл logback-test.xml;
  3. Если нет и его, пробуем найти logback.xml;
  4. Если и он не найден, используем настройки по умолчанию, то есть, пишем просто в консоль;

Давайте положим в src/test/resources файл logback-test.xml следующего содержания:

<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern> %d{HH:mm:ss.SSS} %-5level [%thread] %logger{128} - %msg%n </pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> </configuration>

Как несложно догадаться, здесь мы говорим Logback писать все сообщения уровня DEBUG и выше в STDOUT, используя заданный формат. Если теперь запустить тесты, в выводе вы действительно увидите все DEBUG, INFO и ERROR логи. Можете заменить уровень в строке <root level="DEBUG"> на INFO или ERROR и убедиться, что логов становится меньше.

В каталоге src/main/resources создадим logback.xml немного иного содержания:

<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern> %date %-5level [%thread] %logger{128} - %msg%n </pattern> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>finagle-example.log</file> <encoder> <pattern> %date %-5level [%thread] %logger{128} - %msg%n </pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="STDOUT" /> <appender-ref ref="FILE" /> </root> </configuration>

Эти настройки тоже в особых пояснениях не нуждаются. Мы говорим писать как в консоль, так и в файл, используя немного другой формат логов. Можно собрать standalone jar, используя команду:

sbt assembly

… затем запустить его, сказав:

java -jar ./target/scala-2.11/finagle-example-assembly-0.1.jar

… и проверить, что Logback успешно подхватывает настройки.

Можно переопределить настройки без пересборки jar, сказав:

java -Dlogback.configurationFile=path/to/logback.xml \ -jar ./target/scala-2.11/finagle-example-assembly-0.1.jar

Можно заставить Logback автоматически перечитывать документацию, изменив первую строчку в logback.xml примерно так:

<configuration scan="true" scanPeriod="10 seconds" >

Можно фильтровать логи по классам и пакетам, при этом относительно иерархий работает интуитивная семантика:

<logger name="me.eax.some.package" level="INFO" />

В appender-секциях можно указывать минимальный уровень сообщений:

<filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>INFO</level> </filter>

Логи конкретных пакетов и классов можно направлять в заданные аппендеры:

<logger name="me.eax.some.package" level="INFO"> <appender-ref ref="FILE" /> </logger>

Можно настроить ротацию и архивацию логов:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>/path/to/application.log</file> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%date %-5level [%thread] %logger{128} - %msg%n</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"> <FileNamePattern>/path/to/application.%i.log.zip</FileNamePattern> <MinIndex>1</MinIndex> <MaxIndex>10</MaxIndex> </rollingPolicy> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>2MB</MaxFileSize> </triggeringPolicy> </appender>

Альтернативный вариант:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>/path/to/application.log</file> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%date %-5level [%thread] %logger{128} - %msg%n</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern> /path/to/application-%d{yyyy-MM-dd}.log.gz </fileNamePattern> <maxHistory>10</maxHistory> </rollingPolicy> </appender>

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

Ссылки по теме:

А чем вы пишете логи?