Skip to content

Commit 5f2cde7

Browse files
committed
Close #441 - Bump cats-effect to 3.6.3 - Add MdcAdapter for Cats Effect 3 to properly share context through MDC with IO and IOLocal from Cats Effect 3
1 parent 711ce5c commit 5f2cde7

File tree

8 files changed

+1213
-41
lines changed

8 files changed

+1213
-41
lines changed

build.sbt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,18 +326,19 @@ lazy val logbackMdcCatsEffect3 = module(ProjectName("logback-mdc-cats-effect3
326326
libraryDependencies ++= Seq(
327327
libs.logbackClassic,
328328
libs.logbackScalaInterop,
329-
libs.catsEffect3Eap,
329+
libs.catsEffect3,
330330
libs.tests.effectieCatsEffect3,
331331
libs.tests.extrasHedgehogCatsEffect3,
332332
) ++ libs.tests.hedgehogLibs,
333333
libraryDependencies := libraryDependenciesRemoveScala3Incompatible(
334334
scalaVersion.value,
335335
libraryDependencies.value,
336336
),
337-
javaOptions += "-Dcats.effect.ioLocalPropagation=true",
337+
javaOptions += "-Dcats.effect.trackFiberContext=true",
338338
)
339339
.dependsOn(
340340
core,
341+
slf4jMdc,
341342
monix % Test,
342343
slf4jLogger % Test,
343344
)
@@ -663,7 +664,7 @@ lazy val props =
663664

664665
final val CatsVersion = "2.7.0"
665666

666-
val CatsEffect3Version = "3.3.14"
667+
val CatsEffect3Version = "3.6.3"
667668

668669
val Monix3Version = "3.4.0"
669670

@@ -707,8 +708,6 @@ lazy val libs =
707708

708709
lazy val catsEffect3 = "org.typelevel" %% "cats-effect" % props.CatsEffect3Version
709710

710-
lazy val catsEffect3Eap = "org.typelevel" %% "cats-effect" % "3.6-02a43a6"
711-
712711
lazy val monix3Execution = "io.monix" %% "monix-execution" % props.Monix3Version
713712

714713
lazy val effectieCore: ModuleID = "io.kevinlee" %% "effectie-core" % props.EffectieVersion

changelogs/2.2.0-beta2.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## [2.2.0-beta2](https://github.com/Kevin-Lee/logger-f/issues?q=is%3Aissue+milestone%3Av2-m3) - 2025-01-03
2+
3+
* Add `MdcAdapter` for Cats Effect 3 to properly share context through `MDC` with `IO` and `IOLocal` from Cats Effect 3 (#441)
4+
* This is a beta released for testing the new feature with cats-effect `3.6.0-RC1`.
Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package loggerf.logger.logback
22

3-
import cats.effect.unsafe.IOLocals
43
import cats.effect.{IOLocal, SyncIO}
54
import cats.syntax.all._
65
import ch.qos.logback.classic.LoggerContext
76
import logback_scala_interop.JLoggerFMdcAdapter
8-
import org.slf4j.{LoggerFactory, MDC}
7+
import org.slf4j.LoggerFactory
98

109
import java.util.{Map => JMap, Set => JSet}
1110
import scala.jdk.CollectionConverters._
12-
import scala.util.control.NonFatal
1311

1412
/** @author Kevin Lee
1513
* @since 2023-07-07
@@ -18,7 +16,7 @@ class Ce3MdcAdapter extends JLoggerFMdcAdapter {
1816

1917
private[this] val localContext: IOLocal[Map[String, String]] =
2018
IOLocal[Map[String, String]](Map.empty[String, String])
21-
.syncStep(1)
19+
.syncStep(100)
2220
.flatMap(
2321
_.leftMap(_ =>
2422
new Error(
@@ -28,58 +26,79 @@ class Ce3MdcAdapter extends JLoggerFMdcAdapter {
2826
)
2927
.unsafeRunSync()
3028

31-
override def put(key: String, `val`: String): Unit =
32-
IOLocals.update(localContext)(_ + (key -> `val`))
29+
override def put(key: String, `val`: String): Unit = {
30+
val unsafeThreadLocal = localContext.unsafeThreadLocal()
31+
unsafeThreadLocal.set(unsafeThreadLocal.get + (key -> `val`))
32+
}
3333

34-
@SuppressWarnings(Array("org.wartremover.warts.Null"))
34+
@SuppressWarnings(Array("org.wartremover.warts.Null", "org.wartremover.warts.StringPlusAny"))
3535
override def get(key: String): String =
36-
IOLocals.get(localContext).getOrElse(key, null) // scalafix:ok DisableSyntax.null
36+
localContext.unsafeThreadLocal().get.getOrElse(key, null) // scalafix:ok DisableSyntax.null
3737

38-
override def remove(key: String): Unit = IOLocals.update(localContext)(_ - key)
38+
override def remove(key: String): Unit = {
39+
val unsafeThreadLocal = localContext.unsafeThreadLocal()
40+
unsafeThreadLocal.set(unsafeThreadLocal.get - key)
41+
}
3942

40-
override def clear(): Unit = IOLocals.reset(localContext)
43+
override def clear(): Unit = localContext.unsafeThreadLocal().set(Map.empty[String, String])
4144

4245
override def getCopyOfContextMap: JMap[String, String] = getPropertyMap0
4346

4447
override def setContextMap0(contextMap: JMap[String, String]): Unit =
45-
IOLocals.set(localContext, contextMap.asScala.toMap)
48+
localContext.unsafeThreadLocal().set(contextMap.asScala.toMap)
4649

47-
private def getPropertyMap0: JMap[String, String] = IOLocals.get(localContext).asJava
50+
private def getPropertyMap0: JMap[String, String] = localContext.unsafeThreadLocal().get.asJava
4851

4952
override def getPropertyMap: JMap[String, String] = getPropertyMap0
5053

51-
override def getKeys: JSet[String] = IOLocals.get(localContext).keySet.asJava
54+
override def getKeys: JSet[String] = localContext.unsafeThreadLocal().get.keySet.asJava
5255

5356
}
54-
object Ce3MdcAdapter {
57+
object Ce3MdcAdapter extends Ce3MdcAdapterOps
58+
59+
trait Ce3MdcAdapterOps {
5560

5661
@SuppressWarnings(Array("org.wartremover.warts.Null"))
57-
private def initialize0(): Ce3MdcAdapter = {
58-
val field = classOf[MDC].getDeclaredField("mdcAdapter")
59-
field.setAccessible(true)
60-
val adapter = new Ce3MdcAdapter
61-
field.set(null, adapter) // scalafix:ok DisableSyntax.null
62-
field.setAccessible(false)
63-
adapter
62+
protected def initialize0(ce3MdcAdapter: Ce3MdcAdapter): Ce3MdcAdapter = {
63+
org.slf4j.SetMdcAdapter(ce3MdcAdapter)
64+
ce3MdcAdapter
6465
}
6566

6667
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf", "scalafix:DisableSyntax.asInstanceOf"))
67-
def initialize(): Ce3MdcAdapter = {
68-
val loggerContext =
69-
LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
70-
initializeWithLoggerContext(loggerContext)
71-
}
68+
protected def getLoggerContext(): LoggerContext =
69+
LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
7270

73-
def initializeWithLoggerContext(loggerContext: LoggerContext): Ce3MdcAdapter = {
74-
val adapter = initialize0()
75-
try {
76-
val field = classOf[LoggerContext].getDeclaredField("mdcAdapter")
71+
def initialize(): Ce3MdcAdapter =
72+
initializeWithCe3MdcAdapterAndLoggerContext(new Ce3MdcAdapter, getLoggerContext())
73+
74+
def initializeWithCe3MdcAdapter(ce3MdcAdapter: Ce3MdcAdapter): Ce3MdcAdapter =
75+
initializeWithCe3MdcAdapterAndLoggerContext(ce3MdcAdapter, getLoggerContext())
76+
77+
def initializeWithLoggerContext(loggerContext: LoggerContext): Ce3MdcAdapter =
78+
initializeWithCe3MdcAdapterAndLoggerContext(new Ce3MdcAdapter, loggerContext)
79+
80+
@SuppressWarnings(Array("org.wartremover.warts.Equals"))
81+
def initializeWithCe3MdcAdapterAndLoggerContext(
82+
ce3MdcAdapter: Ce3MdcAdapter,
83+
loggerContext: LoggerContext,
84+
): Ce3MdcAdapter = {
85+
val adapter = initialize0(ce3MdcAdapter)
86+
87+
loggerContext.setMDCAdapter(adapter)
88+
if (loggerContext.getMDCAdapter == adapter) {
89+
// println("[LoggerContext] It's set by setMDCAdapter.")
90+
adapter
91+
} else {
92+
// println(
93+
// "[LoggerContext] The old setMDCAdapter doesn't replace `mdcAdapter` if it has already been set, " +
94+
// "so it will use reflection to set it in the `mdcAdapter` field."
95+
// )
96+
val loggerContextClass = classOf[LoggerContext]
97+
val field = loggerContextClass.getDeclaredField("mdcAdapter")
7798
field.setAccessible(true)
7899
field.set(loggerContext, adapter)
79100
field.setAccessible(false)
80101
adapter
81-
} catch {
82-
case NonFatal(_) => adapter
83102
}
84103
}
85104
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package loggerf.logger.logback
2+
3+
import cats.effect.{IOLocal, unsafe}
4+
import ch.qos.logback.classic.LoggerContext
5+
import logback_scala_interop.JLoggerFMdcAdapter
6+
import org.slf4j.LoggerFactory
7+
8+
import java.util.{Map => JMap, Set => JSet}
9+
import scala.jdk.CollectionConverters._
10+
11+
/** @author Kevin Lee
12+
* @since 2023-07-07
13+
*/
14+
class Ce3MdcAdapterWithIoRuntime(private val ioRuntime: unsafe.IORuntime) extends JLoggerFMdcAdapter {
15+
16+
private[this] val localContext: IOLocal[Map[String, String]] =
17+
IOLocal[Map[String, String]](Map.empty[String, String])
18+
.unsafeRunSync()(ioRuntime)
19+
20+
override def put(key: String, `val`: String): Unit = {
21+
val unsafeThreadLocal = localContext.unsafeThreadLocal()
22+
unsafeThreadLocal.set(unsafeThreadLocal.get + (key -> `val`))
23+
}
24+
25+
@SuppressWarnings(Array("org.wartremover.warts.Null", "org.wartremover.warts.StringPlusAny"))
26+
override def get(key: String): String =
27+
localContext.unsafeThreadLocal().get.getOrElse(key, null) // scalafix:ok DisableSyntax.null
28+
29+
override def remove(key: String): Unit = {
30+
val unsafeThreadLocal = localContext.unsafeThreadLocal()
31+
unsafeThreadLocal.set(unsafeThreadLocal.get - key)
32+
}
33+
34+
override def clear(): Unit = localContext.unsafeThreadLocal().set(Map.empty[String, String])
35+
36+
override def getCopyOfContextMap: JMap[String, String] = getPropertyMap0
37+
38+
override def setContextMap0(contextMap: JMap[String, String]): Unit =
39+
localContext.unsafeThreadLocal().set(contextMap.asScala.toMap)
40+
41+
private def getPropertyMap0: JMap[String, String] = localContext.unsafeThreadLocal().get.asJava
42+
43+
override def getPropertyMap: JMap[String, String] = getPropertyMap0
44+
45+
override def getKeys: JSet[String] = localContext.unsafeThreadLocal().get.keySet.asJava
46+
47+
}
48+
object Ce3MdcAdapterWithIoRuntime extends Ce3MdcAdapterWithIoRuntimeOps
49+
50+
trait Ce3MdcAdapterWithIoRuntimeOps {
51+
52+
@SuppressWarnings(Array("org.wartremover.warts.Null"))
53+
protected def initialize0(ce3MdcAdapter: Ce3MdcAdapterWithIoRuntime): Ce3MdcAdapterWithIoRuntime = {
54+
org.slf4j.SetMdcAdapter(ce3MdcAdapter)
55+
ce3MdcAdapter
56+
}
57+
58+
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf", "scalafix:DisableSyntax.asInstanceOf"))
59+
protected def getLoggerContext(): LoggerContext =
60+
LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
61+
62+
def initialize()(implicit ioRuntime: unsafe.IORuntime): Ce3MdcAdapterWithIoRuntime =
63+
initializeWithCe3MdcAdapterWithIoRuntimeAndLoggerContext(
64+
new Ce3MdcAdapterWithIoRuntime(ioRuntime),
65+
getLoggerContext(),
66+
)
67+
68+
def initializeWithCe3MdcAdapterWithIoRuntime(ce3MdcAdapter: Ce3MdcAdapterWithIoRuntime): Ce3MdcAdapterWithIoRuntime =
69+
initializeWithCe3MdcAdapterWithIoRuntimeAndLoggerContext(ce3MdcAdapter, getLoggerContext())
70+
71+
def initializeWithLoggerContext(loggerContext: LoggerContext)(
72+
implicit ioRuntime: unsafe.IORuntime
73+
): Ce3MdcAdapterWithIoRuntime =
74+
initializeWithCe3MdcAdapterWithIoRuntimeAndLoggerContext(new Ce3MdcAdapterWithIoRuntime(ioRuntime), loggerContext)
75+
76+
@SuppressWarnings(Array("org.wartremover.warts.Equals"))
77+
def initializeWithCe3MdcAdapterWithIoRuntimeAndLoggerContext(
78+
ce3MdcAdapter: Ce3MdcAdapterWithIoRuntime,
79+
loggerContext: LoggerContext,
80+
): Ce3MdcAdapterWithIoRuntime = {
81+
val adapter = initialize0(ce3MdcAdapter)
82+
83+
loggerContext.setMDCAdapter(adapter)
84+
if (loggerContext.getMDCAdapter == adapter) {
85+
// println("[LoggerContext] It's set by setMDCAdapter.")
86+
adapter
87+
} else {
88+
// println(
89+
// "[LoggerContext] The old setMDCAdapter doesn't replace `mdcAdapter` if it has already been set, " +
90+
// "so it will use reflection to set it in the `mdcAdapter` field."
91+
// )
92+
val loggerContextClass = classOf[LoggerContext]
93+
val field = loggerContextClass.getDeclaredField("mdcAdapter")
94+
field.setAccessible(true)
95+
field.set(loggerContext, adapter)
96+
field.setAccessible(false)
97+
adapter
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)