scalabeginner

Structured Logging Patterns

Implement structured logging in Scala: MDC context, log levels, JSON formatting, and async logging.

scala
import java.time.{Instant, ZoneOffset}
import java.time.format.DateTimeFormatter

// Simple structured logger
enum Level(val value: Int, val label: String):
  case DEBUG extends Level(0, "DEBUG")
  case INFO  extends Level(1, "INFO")
  case WARN  extends Level(2, "WARN")
  case ERROR extends Level(3, "ERROR")

case class LogEntry(
  timestamp: Instant,
  level: Level,
  message: String,
  context: Map[String, String] = Map.empty,
  error: Option[Throwable] = None
):
  def toJson: String =
    val base = Map(
      "timestamp" -> timestamp.toString,
      "level" -> level.label,
      "message" -> message
    )
    val withCtx = if context.nonEmpty then
      base ++ context.map((k, v) => s"ctx.$k" -> v)
    else base
    val withErr = error.map(e =>
      withCtx + ("error" -> e.getMessage) + ("error.type" -> e.getClass.getSimpleName)
    ).getOrElse(withCtx)
    withErr.map((k, v) => s""""$k":"$v"""").mkString("{", ",", "}")

  def toText: String =
    val ts = DateTimeFormatter.ofPattern("HH:mm:ss.SSS")
      .format(timestamp.atOffset(ZoneOffset.UTC))
    val ctx = if context.nonEmpty then
      context.map((k, v) => s"$k=$v").mkString(" [", ", ", "]")
    else ""
    val err = error.map(e => s" | ${e.getClass.getSimpleName}: ${e.getMessage}").getOrElse("")
    s"$ts ${level.label.padTo(5, ' ')} $message$ctx$err"

// Logger with context
class Logger(name: String, minLevel: Level = Level.DEBUG):
  private var context = Map.empty[String, String]

  def withContext(ctx: (String, String)*): Logger =
    val l = Logger(name, minLevel)
    l.context = this.context ++ ctx.toMap
    l

  private def log(level: Level, msg: String, error: Option[Throwable] = None): Unit =
    if level.value >= minLevel.value then
      val entry = LogEntry(Instant.now(), level, msg, context + ("logger" -> name), error)
      println(entry.toText)

  def debug(msg: String): Unit = log(Level.DEBUG, msg)
  def info(msg: String): Unit = log(Level.INFO, msg)
  def warn(msg: String): Unit = log(Level.WARN, msg)
  def error(msg: String): Unit = log(Level.ERROR, msg)
  def error(msg: String, cause: Throwable): Unit = log(Level.ERROR, msg, Some(cause))

// MDC-style context
object MDC:
  private val context = ThreadLocal.withInitial[Map[String, String]](() => Map.empty)

  def put(key: String, value: String): Unit =
    context.set(context.get() + (key -> value))

  def get(key: String): Option[String] = context.get().get(key)
  def getAll: Map[String, String] = context.get()
  def clear(): Unit = context.set(Map.empty)

  def withContext[A](ctx: (String, String)*)(block: => A): A =
    val old = context.get()
    context.set(old ++ ctx.toMap)
    try block
    finally context.set(old)

@main def run(): Unit =
  val log = Logger("App")

  log.info("Application starting")
  log.debug("Loading configuration")
  log.warn("Config file not found, using defaults")

  // With context
  val reqLog = log.withContext("requestId" -> "req-123", "userId" -> "user-42")
  reqLog.info("Processing request")
  reqLog.debug("Fetching data")
  reqLog.info("Request completed")

  // Error logging
  try
    throw RuntimeException("Connection refused")
  catch case e: RuntimeException =>
    log.error("Database connection failed", e)

  // MDC
  MDC.withContext("traceId" -> "abc-123") {
    println(s"\nMDC traceId: ${MDC.get("traceId")}")
    MDC.put("spanId", "span-1")
    println(s"MDC all: ${MDC.getAll}")
  }
  println(s"MDC after: ${MDC.getAll}")  // cleaned up

  // JSON format demo
  val entry = LogEntry(
    Instant.now(), Level.ERROR, "Something went wrong",
    Map("service" -> "api", "endpoint" -> "/users"),
    Some(RuntimeException("timeout"))
  )
  println(s"\nJSON: ${entry.toJson}")

Use Cases

  • Application observability
  • Request tracing with context
  • Structured log aggregation

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.