Skip to content

5. Logging That Finds You ​

A log line that reads save failed is almost useless if your codebase has twenty save methods. The first question you ask is always the same: where did this come from? This chapter is about a logger that answers that for you, for free.

Logging in one line ​

Mix in LogSupport and call a level method:

scala
import wvlet.uni.log.LogSupport

class OrderService extends LogSupport:
  def placeOrder(id: String): Unit =
    info(s"Placing order ${id}")
    // ... do the work ...
bash
$ sbt run
2024-01-15 10:30:45.123+0900  info [OrderService] Placing order A-100 - (OrderService.scala:5)

Look at the end of that line: (OrderService.scala:5). The log tells you the exact file and line that produced it. You did not pass it; you did not configure it. That is the whole idea.

Walking the code ​

LogSupport is a mixin. Extending it gives the class trace, debug, info, warn, and error methods and a logger named after the class ([OrderService]). There is no private val logger = LoggerFactory.getLogger(getClass) line to copy-paste and get wrong.

The five levels are ordered. From quietest to loudest: trace, debug, info, warn, error. You set a threshold and everything at or above it prints. The levels reference has the full table.

The source location is captured at compile time. (file:line) is not found by walking a stack trace at runtime — that would be slow and would break under inlining. Uni captures it with a Scala 3 macro when your code compiles, so it is exact and free. This is the "finds you" payoff: in a real incident, the location turns "something failed" into "this line failed," and you skip the search.

You don't guard log calls ​

In many logging frameworks you learn to write this, because building the message is expensive and you don't want to pay for it when debug is off:

scala
// The defensive pattern you do NOT need here:
if logger.isDebugEnabled then
  logger.debug(s"State dump: ${expensiveSnapshot()}")

With Uni you write the obvious thing:

scala
debug(s"State dump: ${expensiveSnapshot()}")

The level methods are inline macros, so the argument is only evaluated when the level is enabled. If debug is off, expensiveSnapshot() never runs and there is no allocation — the call compiles down to nothing. The guard is built in, so the readable form is also the fast form. See zero-overhead logging for what the macro expands to.

Errors carry their cause ​

Pass the exception as a second argument and the stack trace rides along:

scala
import wvlet.uni.log.LogSupport

class PaymentService extends LogSupport:
  def charge(amount: Int): Unit =
    try
      gateway.charge(amount)
    catch
      case e: Exception =>
        error(s"Charge of ${amount} failed", e)  // message + full stack trace
        throw e

The log keeps your message and the exception's stack trace together, so you see the human-readable context and the machine detail in one record.

Why the message is a sentence, not a struct ​

Some ecosystems push you toward structured logs — every line a bag of key/value fields, meant to be parsed by a machine. Uni's logger is deliberately not that. A log line here is a sentence written for a person reading it during an incident: prose, with the source location and any exception attached.

That is a choice, not an omission. Uni's position is that logs are developer context, not your metrics or analytics pipeline. When you need machine-readable records — counters, traces, events to aggregate — emit them explicitly to a system built for it, and keep logs for the human who is paging through them at 2 a.m. Mixing the two jobs tends to serve neither well.

Tuning what prints ​

You usually leave the defaults alone, but two knobs cover most needs:

scala
import wvlet.uni.log.{Logger, LogLevel}

Logger.setDefaultLogLevel(LogLevel.DEBUG)          // global threshold
Logger("OrderService").setLogLevel(LogLevel.TRACE) // one logger, louder

A class that mixes in LogSupport already has a logger named after it, so you can dial one noisy component up or down without touching the others. Outside a class, Logger("name") gives you a named logger directly. To send logs to a rotating file — on the JVM, Node.js, or Native — point a FileLogHandler at a path.

What you have, what comes next ​

Logging is now a one-line, zero-ceremony tool:

  • extends LogSupport gives any class trace/debug/info/warn/error and a logger named after it.
  • Every message carries its (file:line), captured at compile time.
  • Level methods are macros, so unguarded debug(...) calls cost nothing when disabled.
  • Logs are human context by design; error(msg, e) keeps the cause attached.

Next, Chapter 6 turns to data crossing your program's edges — parsing JSON, writing MessagePack, and deriving both from your case classes with one line.

← 4. Testing with UniTest | Next → 6. Data In, Data Out

Released under the Apache 2.0 License.