v2.13.0
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.jarWith scala-cli,
//> using javaOpt -Dcats.effect.trackFiberContext=trueInitialize 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
MDCvalue 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),Ce3MdcAdapterfalls back to a plain per-thread map, so fiber-unaware code
keeps the usualLogbackMDCAdapterbehaviour instead of having itsMDCwrites 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
MdcAdapterfor Cats Effect 3 to properly share context throughMDCwithIOandIOLocalfrom 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