Biblioteki do logowania dla języka Scala
Chcąc dowiedzieć się co dzieje się wewnątrz naszej aplikacji mamy dwie drogi. Pierwszym sposobem jest debugowanie. Jednak im więcej wątków w aplikacji i im bardziej komunikują się one w sposób asynchroniczny tym trudniej jest debugować. Drugim sposobem jest logowanie informacji. Najprostszym sposobem logowania informacji w Javie jest System.out.println
, a w Scali upraszcza się to do println
. Ale jest to złe z dwóch powodów:
- Po pierwsze, jeśli piszemy aplikację “konsolową” (ang. command line interface, CLI) to użytkownik będzie niepotrzebnie widział nieinteresujące go informacje z wewnętrznego procesu przetwarzania.
- Tak wypisanych informacji nie można zapisać w bazie danych ani wysłać do innego systemu.
Dlatego powstały biblioteki do logowania. Biblioteki takie pozwalają przekierować logi do pliku, zapisać je w bazie danych oraz wysłać je do dowolnego innego systemu.
Przegląd bibliotek
-
scala-logging - wygodna i wydajna biblioteka logowania opakowywująca bibliotekę
SLF4J
dla języka Scala. Niestety działa tylko dla Scala/JVM - util-logging - jest małym opakowaniem wbudowanego logowania Javy, aby uczynić go bardziej przyjaznym dla Scali. Niestety także, działa tylko dla Scala/JVM
-
scalajs-java-logging - implementacja
java.logging
dla Scala.js. Wspiera Scala.js w wersji 0.6.x i 1.0.x - airframe-log - biblioteka do ulepszania logowania aplikacji Scala z kolorami i lokalizacjami kodów źródłowych. Wspiera Scala.js w wersji 0.6.x i 1.0.x
-
slogging - biblioteka logowania zgodna z
scala-logging
(iSLF4J
) oparta na makrach dla Scala/JVM, Scala.js (wersja 0.6.x) i Scala Native - scribe - praktyczny szkielet logowania, który nie wymaga żadnej innej struktury logowania i może być w pełni skonfigurowany programowo. Wspiera Scala.js w wersji 0.6.x oraz Scala Native.
I konkretne próby zastosowania
scala-logging
Jest to najprawdopodobniej najpopularniejsza biblioteka do logowania w języku Scala. Niestety jej wadą jest to, że działa tylko dla JVM.
W pliku build.sbt
dodajemy bibliotekę do wspólnych zależności:
libraryDependencies ++= Seq(
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.0",
),
Scala-logging
można używać na dwa sposoby. Za pomocą traitów StrictLogging
i LazyLogging
. Oba traity tworzą zmienną logger
, która jest loggerem.
Trait StrictLogging
inicjalizuje logger
w momencie utworzenia klasy:
package com.typesafe.scalalogging
import org.slf4j.LoggerFactory
trait StrictLogging {
protected val logger: Logger = Logger(LoggerFactory.getLogger(getClass.getName))
}
package pl.writeonly.re.shared
import com.typesafe.scalalogging.StrictLogging
object StrictLoggingCore extends Core with StrictLogging {
def apply(arg: String): Unit = {
logger.info(s"Hello Scala $arg!")
}
}
Trait LazyLogging
inicjalizuje logger
w momencie pierwszego użycia loggera:
package com.typesafe.scalalogging
import org.slf4j.LoggerFactory
trait LazyLogging {
@transient
protected lazy val logger: Logger = Logger(LoggerFactory.getLogger(getClass.getName))
}
package pl.writeonly.re.shared
import slogging.LazyLogging
object LazyLoggingCore extends Core with LazyLogging {
def apply(arg: String): Unit = {
logger.info(s"Hello Scala $arg!")
}
}
Łączymy wszystko w obiekcie Core
:
package pl.writeonly.re.shared
trait Core {
def apply(arg: String): Unit
}
object Core extends Core {
override def apply(arg: String): Unit = {
StrictLoggingCore(arg)
LazyLoggingCore(arg)
}
}
Tworzymy test jednostkowy we frameworku uTest:
package pl.writeonly.re.shared
import utest._
object CoreTest extends TestSuite {
override val tests: Tests = Tests {
'core - {
Core("Awesome")
}
}
}
Wywołujemy:
sbt clean re/test
I wszystko się wysypuje, bo scala-logging
wspiera tylko JVM.
slogging
Jest to przepisana biblioteka scala-logging
, która działa dla Scala Native, Scala.js oraz oczywiście Scala/JVM.
W pliku build.sbt
dodajemy bibliotekę do wspólnych zależności:
libraryDependencies ++= Seq(
"biz.enef" %%% "slogging" % SloggingVersion,
),
Slogging
używa się identycznie jak scala-logging
.
Trait StrictLogging
inicjalizuje logger
w momencie utworzenia klasy:
package slogging
trait StrictLogging extends LoggerHolder {
protected val logger : Logger = LoggerFactory.getLogger(loggerName)
}
package pl.writeonly.re.shared
import slogging.StrictLogging
object StrictLoggingCore extends Core with StrictLogging {
def apply(arg: String): Unit = {
logger.info(s"Hello Scala $arg!")
}
}
Trait LazyLogging
inicjalizuje logger
w momencie pierwszego użycia loggera:
package slogging
trait LazyLogging extends LoggerHolder {
protected lazy val logger = LoggerFactory.getLogger(loggerName)
}
package pl.writeonly.re.shared
import slogging.LazyLogging
object LazyLoggingCore extends Core with LazyLogging {
def apply(arg: String): Unit = {
logger.info(s"Hello Scala $arg!")
}
}
Łączymy wszystko w obiekcie Core
:
package pl.writeonly.re.shared
trait Core {
def apply(arg: String): Unit
}
object Core extends Core {
override def apply(arg: String): Unit = {
StrictLoggingCore(arg)
LazyLoggingCore(arg)
}
}
Tworzymy test jednostkowy we frameworku uTest:
package pl.writeonly.re.shared
import utest._
object CoreTest extends TestSuite {
override val tests: Tests = Tests {
'core - {
Core("Awesome")
}
}
}
Wywołujemy:
sbt clean re/test
I wszystko działa!
Scribe
W pliku build.sbt
dodajemy bibliotekę do wspólnych zależności:
libraryDependencies ++= Seq(
"com.outr" %%% "scribe" % ScribeVersion,
),
Scribe
można używać na dwa sposoby. Za pomocą traitu Logging
oraz za pomocą obiektu pakietu scribe
.
Trait Logging
w prostu sposób tworzy logger
dla każdej instancji klasy:
trait Logging {
protected def loggerName: String = getClass.getName
protected def logger: Logger = Logger(loggerName)
}
package pl.writeonly.re.shared
import scribe.Logging
object LoggingCore extends Core with Logging {
override def apply(arg: String): Unit = {
logger.info(s"Hello Scala $arg!")
}
}
Obiekt pakietu scribe
zawiera magię opartą na makrach, dlatego dziedziczenie nie jest potrzebne:
package object scribe extends LoggerSupport {
lazy val lineSeparator: String = System.getProperty("line.separator")
protected[scribe] var disposables = Set.empty[() => Unit]
override def log[M](record: LogRecord[M]): Unit = Logger(record.className).log(record)
def dispose(): Unit = disposables.foreach(d => d())
implicit class AnyLogging(value: Any) {
def logger: Logger = Logger(value.getClass.getName)
}
def async[Return](f: => Return): Return = macro Macros.async[Return]
def future[Return](f: => Return): Future[Return] = macro Macros.future[Return]
object Execution {
implicit def global: ExecutionContext = macro Macros.executionContext
}
}
package pl.writeonly.re.shared
object ScribeCore extends Core {
def apply(arg: String): Unit = {
scribe.info(s"Hello Scala $arg!")
}
}
Łączymy wszystko obiektem Core
:
package pl.writeonly.re.shared
trait Core {
def apply(arg: String): Unit
}
object Core extends Core {
override def apply(arg: String): Unit = {
LoggingCore(arg)
ScribeCore(arg)
}
}
Tworzymy test:
package pl.writeonly.re.shared
import utest._
object CoreTest extends TestSuite {
override val tests: Tests = Tests {
'core - {
Core("Awesome")
}
}
}
Wywołujemy:
sbt clean re/test
Niestety pojawia się błąd:
[error] cannot link: @java.util.Calendar$::getInstance_java.util.Calendar
[error] cannot link: @java.util.Calendar::setTimeInMillis_i64_unit
[error] unable to link
[error] (re / Nativetest / nativeLink) unable to link
Podsumowanie
Jak zwykle składnia Scali pozwala zapisać te same rzeczy prościej niż w Javie, jednocześnie dzięki temu można wymusić konwencję tworzenia loggerów na etapie kompilacji. Dzięki temu nie mamy w kodzie loggerów o nazwach innych niż loggger
jak np. LOGGER
lub LOG
.
Zostaw komentarz