Skip to content

v2.13.0

Choose a tag to compare

@kevin-lee kevin-lee released this 26 Jul 06:34
· 2 commits to main since this release

2.13.0 - 2026-07-26

New Features

Add MdcAdapter for Cats Effect 3 to properly share context through MDC with IO and IOLocal from Cats Effect 3 (3.7.0) (#441)

logger-f-logback-mdc-cats-effect3 provides loggerf.logger.logback.Ce3MdcAdapter, an
SLF4J / Logback MDCAdapter backed by Cats Effect 3's IOLocal, so MDC values put
inside an IO travel with the fiber instead of the thread.

Get it

libraryDependencies += "io.kevinlee" %% "logger-f-logback-mdc-cats-effect3" % "2.13.0"

It requires cats-effect 3.7.0 and logback-classic, and it is available for the JVM only.

Required JVM system property

Ce3MdcAdapter is backed by IOLocal.unsafeThreadLocal(), which Cats Effect enables only
when the cats.effect.trackFiberContext system property is true. Without it, Cats Effect
throws

java.lang.UnsupportedOperationException: IOLocal-ThreadLocal propagation is disabled.
Enable by setting cats.effect.trackFiberContext=true.

So the JVM must be started with

-Dcats.effect.trackFiberContext=true

Warning

It has to be a JVM command-line option. Cats Effect reads it once into a static final field when its classes are loaded, so calling System.setProperty(...) in your code is too late and does not work.

e.g.)

In build.sbt,

run / fork := true,
Test / fork := true,
javaOptions += "-Dcats.effect.trackFiberContext=true",

Running a jar,

java -Dcats.effect.trackFiberContext=true -jar my-app.jar

With scala-cli,

//> using javaOpt -Dcats.effect.trackFiberContext=true

Initialize it

Install the adapter once at start-up, before anything logs.

import cats.effect._
import loggerf.logger.logback.Ce3MdcAdapter

object MyApp extends IOApp.Simple {

  Ce3MdcAdapter.initialize()

  override def run: IO[Unit] = ???
}

initialize() sets Ce3MdcAdapter as the SLF4J MDCAdapter and as the Logback
LoggerContext's one. If you need more control, there are

Ce3MdcAdapter.initializeWithCe3MdcAdapter(ce3MdcAdapter)
Ce3MdcAdapter.initializeWithLoggerContext(loggerContext)
Ce3MdcAdapter.initializeWithCe3MdcAdapterAndLoggerContext(ce3MdcAdapter, loggerContext)

Use it

After that, just use org.slf4j.MDC as usual.

import cats.effect._
import org.slf4j.{LoggerFactory, MDC}

import scala.concurrent.duration._

val logger = LoggerFactory.getLogger("my-logger")

def handle(requestId: String): IO[Unit] =
  for {
    _ <- IO(MDC.put("requestId", requestId))
    _ <- IO.sleep(100.millis) // async boundary: the fiber may resume on another thread
    _ <- IO(logger.info("Handling the request")) // still logs with the same requestId
  } yield ()

and refer to the key in logback.xml,

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{requestId} - %msg%n</pattern>

What to expect

  • An MDC value put inside a fiber survives real async boundaries (IO.sleep, IO.cede,
    evalOn, IO.blocking) and thread hops, and concurrent fibers keep their own values
    even when they use the same key.
  • On a thread with no running fiber (e.g. a servlet filter, a Netty event-loop
    callback), Ce3MdcAdapter falls back to a plain per-thread map, so fiber-unaware code
    keeps the usual LogbackMDCAdapter behaviour instead of having its MDC writes dropped.
  • The fiber context and the per-thread fallback are never merged, so there is no leak in
    either direction on a thread that alternates between plain code and fiber segments.
    For the same reason, values put on a plain thread are not inherited by fibers started
    from that thread.

What's Changed

  • Close #441 - Add MdcAdapter for Cats Effect 3 to properly share context through MDC with IO and IOLocal from Cats Effect 3 by @kevin-lee in #564
  • logger-f v2.13.0 by @kevin-lee in #699

Full Changelog: v2.12.0...v2.13.0