WriteOnly.pl / Categories / Resentiment / Biblioteki do logowania dla języka Scala

Biblioteki do logowania dla języka Scala

12 minutes to read

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:

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

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:

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:

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.

Tagi: resentiment scala
Share on:
Follow
Follow
Follow