Skip to content

Logging

uni provides a comprehensive logging framework with structured logging support.

LogSupport Trait

The easiest way to add logging is to extend LogSupport:

scala
import wvlet.uni.log.LogSupport

class MyService extends LogSupport:
  def process(data: String): Unit =
    info(s"Processing: ${data}")
    debug("Detailed information")

    try
      riskyOperation()
    catch
      case e: Exception =>
        error("Operation failed", e)

Log Levels

Available log levels in order of severity:

LevelMethodUse Case
ERRORerror()Errors requiring attention
WARNwarn()Potential issues
INFOinfo()General information
DEBUGdebug()Debugging details
TRACEtrace()Fine-grained tracing

Logger Configuration

Setting Log Levels

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

// Set global default
Logger.setDefaultLogLevel(LogLevel.DEBUG)

// Set for specific logger
Logger("MyService").setLogLevel(LogLevel.TRACE)

Creating Named Loggers

scala
val logger = Logger("MyApp")
logger.info("Application started")
logger.debug("Configuration loaded")

Logging with Exceptions

Include stack traces in error logs:

scala
try
  riskyOperation()
catch
  case e: IOException =>
    error("IO operation failed", e)
  case e: Exception =>
    error(s"Unexpected error: ${e.getMessage}", e)

Zero-Overhead Logging with Scala Macros

The logging methods use Scala 3 inline macros, which means:

  1. Automatic lazy evaluation: The message is only evaluated if the log level is enabled
  2. Zero overhead: If the log level is disabled, there is no runtime cost for creating log messages
scala
// expensiveComputation() is only called if DEBUG is enabled
debug(s"Result: ${expensiveComputation()}")

Unlike traditional logging frameworks, you don't need to wrap expensive computations with level checks.

Source Location

Log messages automatically include source location at the end of the message:

2024-01-15 10:30:45.123+0900  info [MyService] Processing: data - (MyService.scala:14)

The source code location (file:line) is captured at compile time using Scala macros.

Writing Logs to a File

FileLogHandler writes log records to a file with automatic rotation. It runs unchanged on JVM, Scala.js (Node.js), and Scala Native, using the FileSystem abstraction underneath.

scala
import wvlet.uni.log.{FileLogHandler, FileLogHandlerConfig, Logger}

val handler = FileLogHandler("app.log")
Logger.setDefaultHandler(handler)

By default, FileLogHandler rotates daily and whenever the active file exceeds 100 MB, keeps the most recent 100 rotated files, and gzips each rotated file. Rotated files are named {stem}-YYYY-MM-DD.{index}.log.gz and sit next to the active log.

Tuning rotation

Configure the handler through FileLogHandlerConfig:

scala
import wvlet.uni.log.{FileLogHandler, FileLogHandlerConfig}

val config =
  FileLogHandlerConfig("app.log")
    .withMaxSizeInBytes(10 * 1024 * 1024) // 10 MB per file
    .withMaxNumberOfFiles(30)             // keep 30 rotated files
    .withCompressRotated(true)            // gzip rotated files

val handler = FileLogHandler(config)

FileLogHandlerConfig ships with both withXxx setters and these escape hatches:

BuilderEffect
withPath(p)Change the active log file path.
withMaxSizeInBytes(n)Rotate once the active file exceeds n bytes. Default 100 MB.
withMaxNumberOfFiles(n)Keep at most n rotated files; oldest are deleted on rotation. Default 100.
withFormatter(f)Override the log formatter. Default AppLogFormatter.
withLogFileExt(ext)Override the active-log extension (default ".log").
withCompressRotated(b)Toggle gzip of rotated files. Default true.
noCompressionDisable gzip; rotated files keep their configured extension (default .log).
noRotationDisable both size and count limits — write a single file forever.

Writing without rotation

When you just want a plain file sink (for example, a short-lived CLI that appends to a debug log), use noRotation:

scala
val handler = FileLogHandler(
  FileLogHandlerConfig("debug.log").noRotation
)

Combining Multiple Handlers

A Logger can fan log records out to several handlers at once — each with its own formatter and (optionally) its own level. Use this when you want, say, terminal-friendly output for humans and a rotated file for forensics on the same logger.

scala
import wvlet.uni.log.{
  ConsoleLogHandler, FileLogHandler, FileLogHandlerConfig,
  LogFormatter, LogLevel, Logger
}

val console = ConsoleLogHandler(LogFormatter.AppLogFormatter)
val file = FileLogHandler(
  FileLogHandlerConfig("app.log").withFormatter(LogFormatter.SourceCodeLogFormatter)
)

val root = Logger.rootLogger
root.clearHandlers
root.addHandler(console)
root.addHandler(file)

addHandler accepts any java.util.logging.Handler, so the same pattern wires up BufferedLogHandler (for tests), NullHandler, or any custom handler you write.

Different formats per sink

Each handler carries its own LogFormatter. The bundled ones are:

FormatterShapeTypical use
AppLogFormatterColor, timestamp, level, loggerDefault for interactive consoles.
SourceCodeLogFormatterAdds the (file:line) source pinFiles / forensic logs where you'll grep later.
IntelliJLogFormatterRenders source as an IntelliJ-clickable linkLocal dev runs inside IntelliJ.
PlainSourceCodeLogFormatterLike SourceCodeLogFormatter without ANSI colorFiles / CI logs that mangle ANSI.
SimpleLogFormatterLevel + message onlySmoke tests, prototypes.
BareFormatterMessage onlyWhen the surrounding tool already adds context.
TSVLogFormatterTab-separated fieldsQuick offline analysis with cut / awk.
ThreadLogFormatterAdds the thread nameDebugging concurrency.

Different levels per sink

Handlers respect java.util.logging.Handler#setLevel, so you can let the logger itself be permissive and let each handler decide what to keep. LogLevel.jlLevel bridges to the underlying java.util.logging level:

scala
root.setLogLevel(LogLevel.DEBUG)   // permissive at the source

console.setLevel(LogLevel.INFO.jlLevel)   // console stays quiet
file.setLevel(LogLevel.DEBUG.jlLevel)     // file captures everything

This is the right way to keep a noisy DEBUG trail on disk without flooding the terminal a human is reading.

Best Practices

  1. Use appropriate levels - Don't log everything as INFO
  2. Include context - Log relevant data for debugging
  3. Avoid sensitive data - Don't log passwords, tokens, etc.
  4. Use structured data - Include key-value pairs for parsing
  5. Keep messages concise - Clear, actionable messages

Example: Service with Logging

scala
import wvlet.uni.log.LogSupport

class OrderService(db: Database) extends LogSupport:

  def createOrder(userId: String, items: List[Item]): Order =
    info(s"Creating order for user: ${userId}, items: ${items.size}")

    val order = try
      val order = db.createOrder(userId, items)
      info(s"Order created: ${order.id}")
      order
    catch
      case e: DatabaseException =>
        error(s"Failed to create order for user: ${userId}", e)
        throw e

    debug(s"Order details: ${order}")
    order

Released under the Apache 2.0 License.