logging in scala

2020-05-14 18:35发布

问题:

In Java, the standard idiom for logging is to create a static variable for a logger object and use that in the various methods.

In Scala, it looks like the idiom is to create a Logging trait with a logger member and mixin the trait in concrete classes. This means that each time an object is created it calls the logging framework to get a logger and also the object is bigger due to the additional reference.

Is there an alternative that allows the ease of use of "with Logging" while still using a per-class logger instance?

EDIT: My question is not about how one can write a logging framework in Scala, but rather how to use an existing one (log4j) without incurring an overhead of performance (getting a reference for each instance) or code complexity. Also, yes, I want to use log4j, simply because I'll use 3rd party libraries written in Java that are likely to use log4j.

回答1:

I'd just stick to the "with Logging" approach. Clean design wins every time - if you get the boilerplate out the way then chances are that you can find far more useful gains achievable in other areas.

Keep in mind that the logging framework will cache loggers, so you still have one per class, even if every instance of that class happens to hold a (inexpensive) reference.

Without proof that logger references are harming your heap, this smells a lot like premature optimization... Just relax and don't worry about it, unless a profiler tells you otherwise.

On an unrelated note, you might also want to look into using slf4j and logback instead of log4j. slf4j has a cleaner design that fits better with idiomatic scala.



回答2:

I used log4j with Scala by creating a trait and having the logger by per-instances not per-class. With some Scala magic and manifests, you might be able to change the logger to be static (internal object), but I'm not 100% sure. Personally, I agree with @KevinWright that making the logger static is a premature optimization.

Also note that the code below has the log messages as by-name, meaning that your logger calls don't need to be wrapped in `if (log.isDebugEnabled()); complex log messages created via string concatenation won't be evaluated unless the log level is appropriate. See this link for more info: http://www.naildrivin5.com/scalatour/wiki_pages/TypeDependentClosures

http://github.com/davetron5000/shorty/blob/master/src/main/scala/shorty/Logs.scala

package shorty

import org.apache.log4j._

trait Logs {
  private[this] val logger = Logger.getLogger(getClass().getName());

  import org.apache.log4j.Level._

  def debug(message: => String) = if (logger.isEnabledFor(DEBUG)) logger.debug(message)
  def debug(message: => String, ex:Throwable) = if (logger.isEnabledFor(DEBUG)) logger.debug(message,ex)
  def debugValue[T](valueName: String, value: => T):T = {
    val result:T = value
    debug(valueName + " == " + result.toString)
    result
  }

  def info(message: => String) = if (logger.isEnabledFor(INFO)) logger.info(message)
  def info(message: => String, ex:Throwable) = if (logger.isEnabledFor(INFO)) logger.info(message,ex)

  def warn(message: => String) = if (logger.isEnabledFor(WARN)) logger.warn(message)
  def warn(message: => String, ex:Throwable) = if (logger.isEnabledFor(WARN)) logger.warn(message,ex)

  def error(ex:Throwable) = if (logger.isEnabledFor(ERROR)) logger.error(ex.toString,ex)
  def error(message: => String) = if (logger.isEnabledFor(ERROR)) logger.error(message)
  def error(message: => String, ex:Throwable) = if (logger.isEnabledFor(ERROR)) logger.error(message,ex)

  def fatal(ex:Throwable) = if (logger.isEnabledFor(FATAL)) logger.fatal(ex.toString,ex)
  def fatal(message: => String) = if (logger.isEnabledFor(FATAL)) logger.fatal(message)
  def fatal(message: => String, ex:Throwable) = if (logger.isEnabledFor(FATAL)) logger.fatal(message,ex)
}

class Foo extends SomeBaseClass with Logs {
  def doit(s:Option[String]) = {
    debug("Got param " + s)
    s match {
      case Some(string) => info(string)
      case None => error("Expected something!")
    } 
  }
}


回答3:

If you are truly concerned about space overhead and/or extra time in object initializers, a good strategy can be to have a logging trait that leaves the logger abstract as in


trait Logging {
  def logger: Logger
  def debug(message: String) { logger.debug(message) }
  def warn(message: String) { logger.warn(message) }
}

For classes which need to be as lightweight as possible then you can do


object MustBeLightweight {
  val logger = Logger.getLog(classOf[MustBeLightweight])
}
class MustBeLightWeight extends Logging {
  final def logger = MustBeLightweight.logger
}

The JIT might even inline debug warn and logger in this case.

You can also have a trait to mix in for classes where the overhead of an additional field is not a problem


trait PerInstanceLog {
  val logger = Logger.getLog(this.getClass())
}

A further option is to leave logging out of the class and put it completely in an object as in


object Foo {
  object log extends Logging {
    override val logger = Logger.getLogger(classOf[Foo])
  } 
}

class Foo {
  import Foo.log._
  def someMethod() = { warn("xyz") }
}

I agree with Kevin though, don't add the complexity unless you need it.



回答4:

object Log {
    def log(message: String) = {
        .....
    }
}

No?



回答5:

Sometimes logging at the package level is the right answer. Scala makes this easier than java because objects can be defined directly in a package. If you defined a Log like this:

package example 
object Log extends au.com.langdale.util.PackageLogger 

This Log is available everywhere in package example. To get more fine-grained logging, you can scatter similar definitions around the package hierarchy. Or you can defined all the package loggers together like this:

package example {
  import au.com.langdale.util.PackageLogger

  object Log extends PackageLogger 

  package frobber {
    object Log extends PackageLogger 

    package undulater {
      object Log extends PackageLogger
    } 
  }
}

The PackageLogger class could be defined as follows (assuming SLF4J):

package au.com.langdale.util
import org.slf4j.LoggerFactory

class PackageLogger {
  val name = { val c = getClass.getName; c.substring(0, c.lastIndexOf('.')) }
  val inner = LoggerFactory.getLogger(name)

  // various convenient logging methods follow....
  def apply( mesg: => Any ) = inner.info(mesg.toString)
  def info( mesg: String ) = inner.info(mesg)
  def warn( mesg: String ) = inner.warn(mesg)
  def error( mesg: String ) = inner.error(mesg)
}


回答6:

Here's a quick hack (which I haven't actually been using, honest ;@)

object LogLevel extends Enumeration {
  val Error   = Value(" ERROR   ")
  val Warning = Value(" WARNING ")                                                                                                      
  val Info    = Value(" INFO    ")
  val Debug   = Value(" DEBUG   ")
}

trait Identity {
  val id: String
}

trait Logging extends Identity {
  import LogLevel._

  abstract class LogWriter {
    protected val writer: Actor
    protected val tstampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ")

    def tstamp = tstampFormat.format(new Date)

    def log(id: String, logLevel: LogLevel.Value, msg: String) {
      writer ! (tstamp + id + logLevel + msg)
    }
  }

  object NullLogWriter extends LogWriter {
    protected val writer = actor{loop {react{case msg: String =>}}}
  }

  object ConsoleWriter extends LogWriter {
    protected val writer = actor{loop {react {case msg: String => Console.out.println(msg); Console.flush case _ =>}}}
  }

  class FileWriter(val file: File) extends LogWriter {
    require(file != null)
    require(file.canWrite)

    protected val writer = actor{loop {react {case msg: String => destFile.println(msg); destFile.flush case _ =>}}}

    private val destFile = {
      try {new PrintStream(new FileOutputStream(file))}
      catch {case e => ConsoleWriter.log("FileWriter", LogLevel.Error, "Unable to create FileWriter for file " + file +
                                         " exception was: " + e); Console.out}
    }
  }

  protected var logWriter: LogWriter = ConsoleWriter
  protected var logLevel             = Info

  def setLoggingLevel(level: LogLevel.Value) {logLevel = level}

  def setLogWriter(lw: LogWriter) {if (lw != null) logWriter = lw}

  def logError(msg: => String) {if (logLevel <= Error) logWriter.log(id, Error, msg)}

  def logWarning(msg: => String) {if (logLevel <= Warning) logWriter.log(id, Warning, msg)}

  def logInfo(msg: => String) {if (logLevel <= Info) logWriter.log(id, Info, msg)}

  def logDebug(msg: => String) {if (logLevel <= Debug) logWriter.log(id, Debug, msg)}
}

Hope it's of some use.



回答7:

The typesafe logging API (https://github.com/typesafehub/scalalogging) has a trait for adding a logger val to a class but its use is optional. It initializes the variable using getClass getName which half the time will be worthless because frequently your actual class name will be gobbledygook.

So if you don't want the trait adding the extra variable to each instance you certainly don't need to use it and can simply put the logger val in your companion object and do an import in the class of the companion objects members so you don't need to qualify it.



回答8:

One way is to extends the Logger to the companion object:

object A extends LoggerSupport

class A {
    import A._
    log("hi")
}

trait LoggerSupport{
    val logger = LoggerFactory.getLogger(this.getClass)
    def log(msg : String)= logger.log(msg)
}

//classes of the logging framework
trait Logger{
    def log(msg : String) : Unit
}

object LoggerFactory{
    def getLogger(classOfLogger : Class[_]) : Logger = ...
}

Alternatively you can cache the logger instances:

import collection.mutable.Map
object LoggerCache{
    var cache : Map[Class[_], Logger] = Map()
    def logger(c : Class[_]) = cache.getOrElseUpdate(c, LoggerFactory.getLogger(c))
}

trait LoggerSupport{
    def log(msg : String) = LoggerCache.logger(this.getClass).log(msg)
}

class A extends LoggerSupport{
    log("hi")
}

This is easier to use but will have worse performance. Performance will be really bad when you're go to discard most of the log messages (because of the log level configuration).



标签: logging scala