tags:

views:

970

answers:

7
+7  Q: 

logging in scala

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.

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

No?

F0RR
I think this is the best solution, because I don't think it is smart to extend/implement a trait when there is not "is a" relation.
paradigmatic
It is a class that uses logging support. :-) The class/logging category is missing. It should be at least `Log.log(c : Class[_], message : String)`
Thomas Jung
+1  A: 

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.

Don Mackenzie
how does this answer my question? i didn't ask how you could write a logging framework in scala. i asked how you could use an existing one (say log4j)
IttayD
A: 

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).

Thomas Jung
yes, but then it is as cumbersome as the java equivalent...
IttayD
notice also that the logger will be categorized according to the object's class which in the underlying JVM is not the same class name as the actual class (a '$' at the end) which is confusing (someone wants to set up debug level and has to remember to set it for the class of the companion object)
IttayD
Yes, the object version is not nice. I've added an alternative solution that caches the Logger instances. But it's still not perfect.
Thomas Jung
+3  A: 

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.

Kevin Wright
then it looks to me this is a case where Java wins over Scala (esp. if you take IDE support where the static log variable decleration code can be generated by the IDE). Scala is still way ahead in the race, but I was hoping this use case would be solved by someone. And this is indeed premature optimization, but it touches every bit of code of my system. I don't see how I can optimize later.
IttayD
I've actually seen this per-instance approach used in Java, I've even used it myself. The main benefit is that you can specify `Logger.getLogger(this.class)` when constructing the logger - which avoids problems when this sort of preamble code is copied between class definitions
Kevin Wright
+1  A: 

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!")
    } 
  }
}
davetron5000
A: 

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.

Geoff Reedy
A: 

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)
}
Arnold deVos