swaldman / logadapter-scala   0.0.1

Apache License 2.0 Website GitHub

A zero-ish overhead logging facade for Scala

Scala versions: 3.x

logadapter-scala

Table of Contents

Quick Start

import logadapter.jul.Api.*

object MyObject extends SelfLogging:
  def doSomething() : Unit =
    INFO.log("Something has been done.")

Full Start

  1. Choose a logging backend.

    Currently jul (java.util.logging), scribe, mlog, slf4j, and stderr, a simple standard-error backend, are supported.

    For jul and stderr, the dependency you'll need is just

    • sbt: libraryDependencies += "com.mchange" %% "logadapter-scala" % "<version>"
    • mill: ivy"com.mchange::logadapter-scala:<version>"

    For scribe, mlog, log4j2, slf4j or other backends, you'll need a library-appropriate dependency, like

    • sbt: libraryDependencies += "com.mchange" %% "logadapter-scala-scribe" % "<version>"
    • mill: ivy"com.mchange::logadapter-scala-scribe:<version>"

    Look for the latest version (which will be the same across all backends).

Note

For logback, choose the slf4j backend, but make sure the latest version of logback-classic is in your CLASSPATH.

  1. Each backend has its own package, in which there is an object called Api. Import the full API from that object:

    import logadapter.jul.Api.*
    
    // you might have chosen `scribe`, `mlog`, `log4j2`, `slf4j`, or `stderr` instead of `jul`
  2. Mark your classes and objects with the trait SelfLogging (part of what you've imported). That brings in a LogAdapter as a given, enabling you to log inside the class or object at will:

    import logadapter.jul.Api.*
    
    object MyObject extends SelfLogging:
      def doSomething() : Int =
        TRACE.log("Entered "doingSomething()")
        INFO.log("I'm doing something!")
        WARNING.logDebug("Math is hard!")
        INFO.logEval("Computation")(3 + 7) // this logs "Computation: 10" and evaluates to 10
  3. If you wish to be able to log outside of a SelfLogging class or object, you can explicitly bring in a LogAdapter from your Api as a given:

    import logadapter.jul.Api.*
    
    given LogAdapter = logAdapterFor( "com.mchange.my.logger.name" )
    
    def sunset() : Unit =
      INFO.log("The sun is setting.")

    logAdapterFor accepts a String logger name, or any object or instance, in which case the log name becomes the class name. (If a Class object is provided, the log name is the classObject.getName())

    You can also use the source file name as your logger name, via

    given LogAdapter = logAdapterByFilename

Note

If you wish to log to the logger of a SelfLogging object outside of that object's scope, you can explicitly import MySelfLoggingObject.logAdapter, and the logging API will become available.

API

The API is very simple. Supported log levels are

  • CONFIG
  • DEBUG
  • ERROR
  • FATAL
  • FINE
  • FINER
  • FINEST
  • INFO
  • SEVERE
  • TRACE
  • WARNING

Note

These levels may not map exactly to the levels of your logging backend. They get remapped to the most appropriate level your backend supports.

Once you've established a context with a logger (see Steps 3 and 4 above), you simply log on levels:

INFO.log("The sun'll come up tomorrow.")

You can also attach a Throwable to your logs (usually resulting in its stack trace getting logged).

   try
     // do some scary stuff here
   catch
     case t : Throwable =>
       WARNING.log("Something really bad happened!", t)
       throw t

If you want more debugging information (like the filename and line number from which the message was logged). You can use logDebug instead if log:

WARNING.logDebug("No throwable.")
WARNING.logDebug("With throwable.", t)

Note

Some backends (e.g. scribe) bring in filename and line number information by default. logDebug may be less useful with these backends.

Sometime for debugging purposes, you want to quickly have the value of an expression logged. For that, there is the logEval method, or just apply your level:

val count = INFO(1 + 2 + 3) // 6 will be logged, and will become the value of count

If you want a prefix to help you interpret the printed expression, you can use

val count = INFO.logEval(prefix = "count")(1 + 2 + 3) // 6 will be logged, and will become the value of count

That's it!

Configuration

logadapter-scala does nothing to standardize configuration of backend libraries. Whatever backend you choose, you'll have to supply its library-specific config files or use its configuration API directly.

However, logadapter-scala Api objects are ordinary objects. If you are using programmatic configuration, one way to ensure your Api is configured before you begin to use it is define your own alias for it, and import that:

val LoggingApi =
  scribe.Logger
     .root
     .clearHandlers()
     .withHandler(minimumLevel = Some(scribe.Level.Info), formatter = scribe.format.Formatter.compact)
     .replace()
  logadapter.scribe.Api

Now, your configuration is centralized, easy to update or switch out. Elsewhere in your application, you just import from LoggingApi:

import LoggingApi.*

and you can be sure your logging has been configured before the API is accessed.

ZIO integration

In addition to your backend appropriate library, if you bring in...

  • sbt: libraryDependencies += "com.mchange" %% "logadapter-scala-zio" % "<version>"
  • mill: ivy"com.mchange::logadapter-scala-zio:<version>"

you can log using your backend of choice to ZIO effects, and have ZIO effects log errors and defects to your backend of choice.

(Yes, ZIO has its own native logging. But some of us don't love it.)

Setting up the API is a bit inelegant, due in part to a compiler bug that will hopefully get fixed soon. For now the setup looks like...

// workaround of nonexport of SelfLogging from logadapter, due to a compiler bug. hopefully unnecessary soon
object LoggingApi:
  val raw = logadapter.zio.ZApi( logadapter.jul.Api )
  type SelfLogging = raw.inner.SelfLogging
  export raw.*

then, as before, you can use in your application...

import LoggingApi.*

Note

As before, to log, you'll need either to be in a type that implements SelfLogging, or have explicitly brought in a given LogAdapter. See Full Start, items 3 and 4 above.

Now in addition to the logging API above, you have the following new methods of Level:

INFO.zlog("This is a message") // returns a ZIO effect, ZIO[Any,Nothing,Unit], or UIO[Unit]
SEVERE.zlog("This is a message with a Throwable", t) // returns a ZIO effect, ZIO[Any,Nothing,Unit], or UIO[Unit]

You also have available methods you can call directly on ZIO effects, which yield the same effect, but with the side effect of logging errors or defects to your backend of choice:

val someZioEffect = ...
someZioEffect.zlogError( WARNING ) // returns someZioEffect with the side effect of logging any errors
someZioEffect.zlogDefect( SEVERE ) // returns someZioEffect with the side effect of logging any defects
someZioEffect.zlogErrorDefect( SEVERE ) // returns someZioEffect with the side effect of logging both errors and defects

To get clearer information about where something went wrong if errors or defects are logged, you can add a tag to any of these methods:

someZioEffect.zlogError( WARNING, what = "Call to DB server" ) // any logged error will mention it was the Call to DB server what done it

History

Over the years, JVM applications have adopted a large menagerie of logging libraries. I've been partial to logging "facades", (pioneered by Apache Commons logging), by which you can learn and use a single logging API, then plug-in and configure any of the different logging libraries you might choose as a "back end".

Mostly in support of c3p0, I've built a very extensive and configurable logging facade. "mlog" (for mchange logging) lives in com.mchange.v2.log package of mchange-commons-java. See the c3p0's documentation for information on configuring and using that package.

mlog-scala is a Scala API and facade to mlog, which I've used extensively and successfully.

However, Scala 3 inlines open a path for a much simpler and lighter-weight sort of facade. mlog-scala is tried and true, but brings in a large java dependency for a very simple task, and imposes some overhead to the frequent operation of logging. (To be fair to my old self, mlog is built with some care, and its overhead is surprisingly small.)

This is an experiment in a much thinner, much simpler logging facade, that largely retains the mlog-scala API, which I've been happy with.

Note that mlog is itself supported by this project. It retains the virtue of letting the backend be chosen and substituted by external configuration, rather than committing to it in code. (In that sense, mlog is similar to slf4j.)

Acknowledgements

This project has been supported in part by external financial sponsorship. Many thanks to Chris Peel for his support!


© 2025 Machinery For Change LLC