From 4b8f336d0cd284a6ab8cc599ce4a25fbcbd8283a Mon Sep 17 00:00:00 2001 From: Kevin Lee Date: Sat, 8 Jul 2023 22:32:55 +1000 Subject: [PATCH 1/2] Close #441 - Add MdcAdapter for Cats Effect 3 to properly share context through MDC with IO and IOLocal from Cats Effect 3 --- build.sbt | 25 + changelogs/2.2.0-beta1.md | 4 + .../cats/testing/ConcurrentSupport.scala | 5 +- .../logger/logback/Ce3MdcAdapter.scala | 85 +++ .../logger/logback/Ce3MdcAdapterSpec.scala | 493 ++++++++++++++++++ .../logger/logback/Ce3MdcAdapterSpec2.scala | 482 +++++++++++++++++ .../scala/loggerf/logger/logback/Gens.scala | 38 ++ .../scala/loggerf/logger/logback/types.scala | 20 + .../logger/logback/Monix3MdcAdapter.scala | 4 +- .../logger/logback/Monix3MdcAdapterSpec.scala | 2 +- 10 files changed, 1154 insertions(+), 4 deletions(-) create mode 100644 changelogs/2.2.0-beta1.md create mode 100644 modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapter.scala create mode 100644 modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec.scala create mode 100644 modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec2.scala create mode 100644 modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Gens.scala create mode 100644 modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/types.scala diff --git a/build.sbt b/build.sbt index 5a1c97a0..b790b1d6 100644 --- a/build.sbt +++ b/build.sbt @@ -84,6 +84,7 @@ lazy val loggerF = (project in file(".")) slf4jMdcNative, logbackMdcMonix3Jvm, testLogbackMdcMonix3Jvm, + logbackMdcCatsEffect3Jvm, doobie1Jvm, testKitJvm, testKitJs, @@ -342,6 +343,29 @@ lazy val testLogbackMdcMonix3 = testProject(ProjectName("logback-mdc-monix3") ) lazy val testLogbackMdcMonix3Jvm = testLogbackMdcMonix3.jvm +lazy val logbackMdcCatsEffect3 = module(ProjectName("logback-mdc-cats-effect3"), crossProject(JVMPlatform)) + .settings( + description := "Logger for F[_] - logback MDC context map support for Cats Effect 3", + libraryDependencies ++= Seq( + libs.logbackClassic, + libs.logbackScalaInterop, + libs.catsEffect3Eap, + libs.tests.effectieCatsEffect3, + libs.tests.extrasHedgehogCatsEffect3, + ) ++ libs.tests.hedgehogLibs, + libraryDependencies := libraryDependenciesRemoveScala3Incompatible( + scalaVersion.value, + libraryDependencies.value, + ), + javaOptions += "-Dcats.effect.ioLocalPropagation=true", + ) + .dependsOn( + core, + monix % Test, + slf4jLogger % Test, + ) +lazy val logbackMdcCatsEffect3Jvm = logbackMdcCatsEffect3.jvm + lazy val doobie1 = module(ProjectName("doobie1"), crossProject(JVMPlatform)) .settings( description := "Logger for F[_] - for Doobie v1", @@ -890,6 +914,7 @@ def projectCommonSettings(projectName: String, crossProject: CrossProject.Builde // , Compile / compile / wartremoverErrors ++= commonWarts((update / scalaBinaryVersion).value) // , Test / compile / wartremoverErrors ++= commonWarts((update / scalaBinaryVersion).value) wartremoverErrors ++= commonWarts((update / scalaBinaryVersion).value), + fork := true, Compile / console / wartremoverErrors := List.empty, Compile / console / wartremoverWarnings := List.empty, Compile / console / scalacOptions := diff --git a/changelogs/2.2.0-beta1.md b/changelogs/2.2.0-beta1.md new file mode 100644 index 00000000..1f78f0b1 --- /dev/null +++ b/changelogs/2.2.0-beta1.md @@ -0,0 +1,4 @@ +## [2.2.0-beta1](https://github.com/Kevin-Lee/logger-f/issues?q=is%3Aissue+milestone%3Av2-m3) - 2025-01-03 + +* Add `MdcAdapter` for Cats Effect 3 to properly share context through `MDC` with `IO` and `IOLocal` from Cats Effect 3 (#441) + * This is a beta released for testing the new feature with cats-effect `3.6-02a43a6`. diff --git a/modules/logger-f-cats-effect3/shared/src/test/scala/loggerf/cats/testing/ConcurrentSupport.scala b/modules/logger-f-cats-effect3/shared/src/test/scala/loggerf/cats/testing/ConcurrentSupport.scala index 9b2d1e6e..259f8721 100644 --- a/modules/logger-f-cats-effect3/shared/src/test/scala/loggerf/cats/testing/ConcurrentSupport.scala +++ b/modules/logger-f-cats-effect3/shared/src/test/scala/loggerf/cats/testing/ConcurrentSupport.scala @@ -25,7 +25,10 @@ trait ConcurrentSupport { val stringWriter = new StringWriter() val printWriter = new PrintWriter(stringWriter) th.printStackTrace(printWriter) - logger(s"⚠️ Error in Executor: ${stringWriter.toString}") + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + val message = stringWriter.toString + logger(s"⚠️ Error in Executor: $message") }, ) diff --git a/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapter.scala b/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapter.scala new file mode 100644 index 00000000..d723cce4 --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapter.scala @@ -0,0 +1,85 @@ +package loggerf.logger.logback + +import cats.effect.unsafe.IOLocals +import cats.effect.{IOLocal, SyncIO} +import cats.syntax.all._ +import ch.qos.logback.classic.LoggerContext +import logback_scala_interop.JLoggerFMdcAdapter +import org.slf4j.{LoggerFactory, MDC} + +import java.util.{Map => JMap, Set => JSet} +import scala.jdk.CollectionConverters._ +import scala.util.control.NonFatal + +/** @author Kevin Lee + * @since 2023-07-07 + */ +class Ce3MdcAdapter extends JLoggerFMdcAdapter { + + private[this] val localContext: IOLocal[Map[String, String]] = + IOLocal[Map[String, String]](Map.empty[String, String]) + .syncStep(1) + .flatMap( + _.leftMap(_ => + new Error( + "Failed to initialize the local context of the Ce3MdcAdapter." + ) + ).liftTo[SyncIO] + ) + .unsafeRunSync() + + override def put(key: String, `val`: String): Unit = + IOLocals.update(localContext)(_ + (key -> `val`)) + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + override def get(key: String): String = + IOLocals.get(localContext).getOrElse(key, null) // scalafix:ok DisableSyntax.null + + override def remove(key: String): Unit = IOLocals.update(localContext)(_ - key) + + override def clear(): Unit = IOLocals.reset(localContext) + + override def getCopyOfContextMap: JMap[String, String] = getPropertyMap0 + + override def setContextMap0(contextMap: JMap[String, String]): Unit = + IOLocals.set(localContext, contextMap.asScala.toMap) + + private def getPropertyMap0: JMap[String, String] = IOLocals.get(localContext).asJava + + override def getPropertyMap: JMap[String, String] = getPropertyMap0 + + override def getKeys: JSet[String] = IOLocals.get(localContext).keySet.asJava + +} +object Ce3MdcAdapter { + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + private def initialize0(): Ce3MdcAdapter = { + val field = classOf[MDC].getDeclaredField("mdcAdapter") + field.setAccessible(true) + val adapter = new Ce3MdcAdapter + field.set(null, adapter) // scalafix:ok DisableSyntax.null + field.setAccessible(false) + adapter + } + + @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf", "scalafix:DisableSyntax.asInstanceOf")) + def initialize(): Ce3MdcAdapter = { + val loggerContext = + LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] + initializeWithLoggerContext(loggerContext) + } + + def initializeWithLoggerContext(loggerContext: LoggerContext): Ce3MdcAdapter = { + val adapter = initialize0() + try { + val field = classOf[LoggerContext].getDeclaredField("mdcAdapter") + field.setAccessible(true) + field.set(loggerContext, adapter) + field.setAccessible(false) + adapter + } catch { + case NonFatal(_) => adapter + } + } +} diff --git a/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec.scala b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec.scala new file mode 100644 index 00000000..6ec02a62 --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec.scala @@ -0,0 +1,493 @@ +package loggerf.logger.logback + +import cats.effect._ +import cats.effect.unsafe.IORuntime +import cats.syntax.all._ +import hedgehog._ +import hedgehog.runner._ +import org.slf4j.MDC + +import java.time.Instant +import scala.jdk.CollectionConverters._ + +/** @author Kevin Lee + * @since 2023-07-07 + */ +object Ce3MdcAdapterSpec extends Properties { + + implicit val ioRuntime: IORuntime = cats.effect.unsafe.implicits.global + + private val ce3MdcAdapter: Ce3MdcAdapter = Ce3MdcAdapter.initialize() + + override def tests: List[Test] = List( + property("IO - MDC should be able to put and get a value", testPutAndGet), + property("IO - MDC should be able to put and get multiple values concurrently", testPutAndGetMultiple), + property( + "IO - MDC should be able to put and get with isolated nested modifications", + testPutAndGetMultipleIsolatedNestedModifications, + ), + property("IO - MDC: It should be able to set a context map", testSetContextMap), + property("IO - MDC should be able to remove the value for the existing key", testRemove), + property("IO - MDC should be able to remove the multiple values for the existing keys", testRemoveMultiple), + property( + "IO - MDC should be able to remove with isolated nested modifications", + testRemoveMultipleIsolatedNestedModifications, + ), + property("IO - MDC: It should return context map for getCopyOfContextMap", testGetCopyOfContextMap), + property("IO - MDC: It should return context map for getPropertyMap", testGetPropertyMap), + property("IO - MDC: It should return context map for getKeys", testGetKeys), + ) + + def before(): Unit = MDC.clear() + + def putAndGet(key: String, value: String): IO[String] = + for { + _ <- IO(MDC.put(key, value)) + got <- IO(MDC.get(key)) + } yield got + + def testPutAndGet: Property = + for { + keyValuePair <- Gens.genKeyValuePair.log("keyValuePair") + } yield { + before() + + val io = putAndGet(keyValuePair.key, keyValuePair.value) + io.map(_ ==== keyValuePair.value) + .unsafeRunSync() + } + + def testPutAndGetMultiple: Property = for { + keyValuePairs <- Gens.genKeyValuePairs.log("keyValuePairs") + + } yield { + before() + + val ios = keyValuePairs.keyValuePairs.traverse { keyValue => + putAndGet(keyValue.key, keyValue.value).start + } + + (for { + fibers <- ios + retrievedKeyValues <- fibers.traverse(_.joinWithNever) + } yield { + Result.all( + List( + retrievedKeyValues.length ==== keyValuePairs.keyValuePairs.length, + retrievedKeyValues ==== keyValuePairs.keyValuePairs.map(_.value), + ) + ) + }) + .unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testPutAndGetMultipleIsolatedNestedModifications: Property = + for { + a <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("1:" + _).log("a") + b <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("2:" + _).log("b") + c <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("3:" + _).log("c") + } yield { + + before() + + val beforeSet = (MDC.get("key-1") ==== null).log("before set") // scalafix:ok DisableSyntax.null + MDC.put("key-1", a) + + val test = for { + before <- IO((MDC.get("key-1") ==== a).log("before")) + beforeIsolated <- IO((MDC.get("key-1") ==== a).log("beforeIsolated")) + .start + .flatMap(_.joinWithNever) + + isolated1 <- ( + IO((MDC.get("key-1") ==== a).log("isolated1Before")).flatMap { isolated1Before => + IO(MDC.put("key-1", b)) *> IO( + (isolated1Before, (MDC.get("key-1") ==== b).log("isolated1After")) + ) + } + ).start + isolated2 <- ( + IO((MDC.get("key-1") ==== a).log("isolated2Before")).flatMap { isolated2Before => + IO(MDC.put("key-2", c)) *> IO( + (isolated2Before, (MDC.get("key-2") ==== c).log("isolated2After")) + ) + } + ).start + + joinedIsolated1 <- isolated1.joinWithNever + joinedIsolated2 <- isolated2.joinWithNever + (isolated1Before, isolated1After) = joinedIsolated1 + (isolated2Before, isolated2After) = joinedIsolated2 + key1Result <- IO((MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""")) + key2Result <- IO( + (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null""") + ) // scalafix:ok DisableSyntax.null + } yield Result.all( + List( + beforeSet, + before, + beforeIsolated, + isolated1Before, + isolated1After, + isolated2Before, + isolated2After, + key1Result, + key2Result, +// (MDC.get("key-1") ==== a).log(s"""${Thread.currentThread().getName}:After: MDC.get("key-1") is not $a"""), +// (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null"""), // scalafix:ok DisableSyntax.null + ) + ) + + test.unsafeRunSync() + } + + def testSetContextMap: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + val result = { + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + before <- IO { + Result.all( + List( + (Option(MDC.get("uuid")) ==== none).log("uuid should not be found"), + (Option(MDC.get("idNum")) ==== none).log("idNum should not be found"), + (Option(MDC.get("timestamp")) ==== none).log("timestamp should not be found"), + (Option(MDC.get("someValue")) ==== none).log("someValue should not be found"), + (MDC.get(staticFieldName) ==== staticValueName) + .log(s"staticFieldName is not $staticValueName"), + (Option(MDC.get("random")) ==== none).log("random should not be found"), + ) + ) + } + _ <- IO(MDC.setContextMap(productToMap(someContext).asJava)) + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + result <- IO { + Result.all( + List( + before, + (MDC.get("uuid") ==== someContext.uuid.toString).log("uuid doesn't match"), + (MDC.get("idNum") ==== someContext.idNum.toString).log("idNum doesn't match"), + (MDC.get("timestamp") ==== now).log("timestamp doesn't match"), + (MDC.get("someValue") ==== someContext.someValue.toString).log("someValue doesn't match"), + (Option(MDC.get(staticFieldName)) ==== none).log("staticFieldName should not be found"), + (Option(MDC.get("random")) ==== none).log("random should not be found"), + ) + ) + } + } yield result + } + result.unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testRemove: Property = + for { + keyValuePair <- Gens.genKeyValuePair.log("keyValuePair") + } yield { + before() + + (for { + valueBefore <- putAndGet(keyValuePair.key, keyValuePair.value) + + _ <- IO(MDC.remove(keyValuePair.key)) + valueAfter <- IO(MDC.get(keyValuePair.key)) + } yield Result + .all( + List( + valueBefore ==== keyValuePair.value, + valueAfter ==== null, // scalafix:ok DisableSyntax.null + ) + )) + .unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testRemoveMultiple: Property = for { + keyValuePairs <- Gens.genKeyValuePairs.log("keyValuePairs") + } yield { + + before() + + val ios = keyValuePairs.keyValuePairs.traverse { keyValue => + putAndGet(keyValue.key, keyValue.value).start + } + + (for { + fibers <- ios + retrievedKeyValues <- fibers.traverse(_.joinWithNever) + retrievedKeyValuesBeforeRemove <- keyValuePairs.keyValuePairs.traverse { keyValue => + IO(MDC.get(keyValue.key)) + } + fibers4Removal <- keyValuePairs.keyValuePairs.traverse { keyValue => + IO(MDC.remove(keyValue.key)).start + } + _ <- fibers4Removal.traverse_(_.joinWithNever) + retrievedKeyValuesAfterRemove <- keyValuePairs.keyValuePairs.traverse { keyValue => + IO(MDC.get(keyValue.key)) + } + } yield { + Result.all( + List( + (retrievedKeyValues.length ==== keyValuePairs.keyValuePairs.length).log("retrievedKeyValues.length check"), + (retrievedKeyValues ==== keyValuePairs.keyValuePairs.map(_.value)).log("retrievedKeyValues value check"), + (retrievedKeyValuesBeforeRemove.length ==== keyValuePairs.keyValuePairs.length) + .log("retrievedKeyValuesBeforeRemove.length check"), + (retrievedKeyValuesBeforeRemove ==== keyValuePairs.keyValuePairs.as(null)) // scalafix:ok DisableSyntax.null + .log("retrievedKeyValuesBeforeRemove value check"), + (retrievedKeyValuesAfterRemove.length ==== keyValuePairs.keyValuePairs.length) + .log("retrievedKeyValuesAfterRemove.length check"), + (retrievedKeyValuesAfterRemove ==== keyValuePairs.keyValuePairs.as(null)) // scalafix:ok DisableSyntax.null + .log("retrievedKeyValuesAfterRemove value check"), + ) + ) + }) + .unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testRemoveMultipleIsolatedNestedModifications: Property = + for { + a <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("1:" + _).log("a") + b <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("2:" + _).log("b") + c <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("3:" + _).log("c") + } yield { + + before() + + val test = for { + _ <- IO(MDC.put("key-1", a)) + before <- IO((MDC.get("key-1") ==== a).log("before")) + beforeIsolated <- IO((MDC.get("key-1") ==== a).log("beforeIsolated")).start.flatMap(_.joinWithNever) + + isolated1 <- (IO((MDC.get("key-1") ==== a).log("isolated1Before")) + .flatMap { isolated1Before => + IO(MDC.put("key-1", b)) *> IO( + (isolated1Before, (MDC.get("key-1") ==== b).log("isolated1After")) + ) + }) + .start + .flatMap(_.joinWithNever) + (isolated1Before, isolated1After) = isolated1 + isolated2 <- (IO((MDC.get("key-1") ==== a).log("isolated2Before")) + .flatMap { isolated2Before => + IO(MDC.put("key-2", c)) *> IO( + (isolated2Before, (MDC.get("key-2") ==== c).log("isolated2After")) + ) + }) + .start + .flatMap(_.joinWithNever) + (isolated2Before, isolated2After) = isolated2 + isolated3 <- (for { + isolated2Key1Before <- IO(MDC.get("key-1")).map(_ ==== a) + isolated2Key2Before <- + IO(MDC.get("key-2")).map(_ ==== null) // scalafix:ok DisableSyntax.null + _ <- IO(MDC.put("key-2", c)) + isolated2Key2After <- IO(MDC.get("key-2")).map(_ ==== c) + _ <- IO(MDC.remove("key-2")) + isolated2Key2AfterRemove <- + IO(MDC.get("key-2")).map(_ ==== null) // scalafix:ok DisableSyntax.null + } yield ( + isolated2Key1Before, + isolated2Key2Before, + isolated2Key2After, + isolated2Key2AfterRemove, + )).start.flatMap(_.joinWithNever) + (isolated2Key1Before, isolated2Key2Before, isolated2Key2After, isolated2Key2AfterRemove) = isolated3 + key1After <- IO((MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""")) + key2After <- IO( + (MDC.get("key-2") ==== null) // scalafix:ok DisableSyntax.null + .log("""After: MDC.get("key-2") is not null""") + ) + + _ <- IO(MDC.remove("key-1")) + key1AfterRemove = (MDC.get("key-1") ==== null) + .log("""After Remove: MDC.get("key-1") is not null""") // scalafix:ok DisableSyntax.null + } yield Result.all( + List( + before, + beforeIsolated, + isolated1Before, + isolated1After, + isolated2Before, + isolated2After, + isolated2Key1Before, + isolated2Key2Before, + isolated2Key2After, + isolated2Key2AfterRemove, + key1After, + key2After, + key1AfterRemove, + ) + ) + + test.unsafeRunSync() + } + + def testGetCopyOfContextMap: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + val result = { + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + mapBefore <- IO(MDC.getCopyOfContextMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== Map(staticFieldName -> staticValueName)) + .log("propertyMap Before") + } + expectedPropertyMap = productToMap(someContext) + _ <- IO(MDC.setContextMap(expectedPropertyMap.asJava)) + mapAfter <- IO(MDC.getCopyOfContextMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap).log("propertyMap After") + } + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + mapAfter2 <- IO(MDC.getCopyOfContextMap) + .map(propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap.updated("timestamp", now)) + .log("propertyMap After 2") + ) + } yield Result.all( + List( + mapBefore, + mapAfter, + mapAfter2, + ) + ) + } + result.unsafeRunSync() + } + + def testGetPropertyMap: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + val result = { + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + mapBefore <- IO(ce3MdcAdapter.getPropertyMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== Map(staticFieldName -> staticValueName)) + .log("propertyMap Before") + } + expectedPropertyMap = productToMap(someContext) + _ <- IO(MDC.setContextMap(expectedPropertyMap.asJava)) + mapAfter <- IO(ce3MdcAdapter.getPropertyMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap).log("propertyMap After") + } + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + mapAfter2 <- IO(ce3MdcAdapter.getPropertyMap) + .map(propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap.updated("timestamp", now)) + .log("propertyMap After 2") + ) + } yield Result.all( + List( + mapBefore, + mapAfter, + mapAfter2, + ) + ) + } + result.unsafeRunSync() + } + + def testGetKeys: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + val result = { + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + keySetBefore <- IO(ce3MdcAdapter.getKeys) + .map { keySet => + (keySet.asScala.toSet ==== Set(staticFieldName)) + .log("keySet Before") + } + expectedPropertyMap = productToMap(someContext) + expectedKeySet = expectedPropertyMap.keySet + _ <- IO(MDC.setContextMap(expectedPropertyMap.asJava)) + keySetAfter <- IO(ce3MdcAdapter.getKeys) + .map { keySet => + (keySet.asScala.toSet ==== expectedKeySet).log("keySet After") + } + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + keySetAfter2 <- IO(ce3MdcAdapter.getKeys) + .map(keySet => + (keySet.asScala.toSet ==== expectedKeySet) + .log("keySet After 2") + ) + } yield Result.all( + List( + keySetBefore, + keySetAfter, + keySetAfter, + keySetAfter2, + ) + ) + } + result.unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + private def productToMap[A <: Product](product: A): Map[String, String] = { + // This doesn't work for Scala 2.12 +// val fields = product.productElementNames.toVector + + val fields = product.getClass.getDeclaredFields.map(_.getName) + val length = product.productArity + + val fieldAndValueParis = for { + n <- 0 until length + maybeValue = product.productElement(n) match { + case maybe @ (Some(_) | None) => maybe + case value => Option(value) + } + } yield (fields(n), maybeValue) + fieldAndValueParis.collect { + case (name, Some(value)) => + name -> value.toString + }.toMap + } + +} diff --git a/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec2.scala b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec2.scala new file mode 100644 index 00000000..b99d0022 --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec2.scala @@ -0,0 +1,482 @@ +package loggerf.logger.logback + +import cats.effect._ +import cats.effect.unsafe.IORuntime +import cats.syntax.all._ +import extras.hedgehog.ce3.syntax.runner._ +import hedgehog._ +import hedgehog.runner._ +import org.slf4j.MDC + +import java.time.Instant +import scala.jdk.CollectionConverters._ + +/** @author Kevin Lee + * @since 2023-07-07 + */ +object Ce3MdcAdapterSpec2 extends Properties { + + implicit val ioRuntime: IORuntime = cats.effect.unsafe.implicits.global + + private val ce3MdcAdapter: Ce3MdcAdapter = Ce3MdcAdapter.initialize() + + override def tests: List[Test] = List( + property("IO - MDC should be able to put and get a value", testPutAndGet), + property("IO - MDC should be able to put and get multiple values concurrently", testPutAndGetMultiple), + property( + "IO - MDC should be able to put and get with isolated nested modifications", + testPutAndGetMultipleIsolatedNestedModifications, + ), + property("IO - MDC: It should be able to set a context map", testSetContextMap), + property("IO - MDC should be able to remove the value for the existing key", testRemove), + property("IO - MDC should be able to remove the multiple values for the existing keys", testRemoveMultiple), + property( + "IO - MDC should be able to remove with isolated nested modifications", + testRemoveMultipleIsolatedNestedModifications, + ), + property("IO - MDC: It should return context map for getCopyOfContextMap", testGetCopyOfContextMap), + property("IO - MDC: It should return context map for getPropertyMap", testGetPropertyMap), + property("IO - MDC: It should return context map for getKeys", testGetKeys), + ) + + def before(): Unit = MDC.clear() + + def putAndGet(key: String, value: String): IO[String] = + for { + _ <- IO(MDC.put(key, value)) + got <- IO(MDC.get(key)) + } yield got + + def testPutAndGet: Property = + for { + keyValuePair <- Gens.genKeyValuePair.log("keyValuePair") + } yield runIO { + before() + + val io = putAndGet(keyValuePair.key, keyValuePair.value) + io.map(_ ==== keyValuePair.value) + } + + def testPutAndGetMultiple: Property = + for { + keyValuePairs <- Gens.genKeyValuePairs.log("keyValuePairs") + } yield runIO { + before() + + val ios = keyValuePairs.keyValuePairs.traverse { keyValue => + putAndGet(keyValue.key, keyValue.value).start + } + + (for { + fibers <- ios + retrievedKeyValues <- fibers.traverse(_.joinWithNever) + } yield { + Result.all( + List( + retrievedKeyValues.length ==== keyValuePairs.keyValuePairs.length, + retrievedKeyValues ==== keyValuePairs.keyValuePairs.map(_.value), + ) + ) + }) + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testPutAndGetMultipleIsolatedNestedModifications: Property = + for { + a <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("1:" + _).log("a") + b <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("2:" + _).log("b") + c <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("3:" + _).log("c") + } yield runIO { + + before() + + val beforeSet = (MDC.get("key-1") ==== null).log("before set") // scalafix:ok DisableSyntax.null + MDC.put("key-1", a) + + for { + before <- IO((MDC.get("key-1") ==== a).log("before")) + beforeIsolated <- IO((MDC.get("key-1") ==== a).log("beforeIsolated")) + .start + .flatMap(_.joinWithNever) + + isolated1 <- ( + IO((MDC.get("key-1") ==== a).log("isolated1Before")).flatMap { isolated1Before => + IO(MDC.put("key-1", b)) *> IO( + (isolated1Before, (MDC.get("key-1") ==== b).log("isolated1After")) + ) + } + ).start + isolated2 <- ( + IO((MDC.get("key-1") ==== a).log("isolated2Before")).flatMap { isolated2Before => + IO(MDC.put("key-2", c)) *> IO( + (isolated2Before, (MDC.get("key-2") ==== c).log("isolated2After")) + ) + } + ).start + + joinedIsolated1 <- isolated1.joinWithNever + joinedIsolated2 <- isolated2.joinWithNever + (isolated1Before, isolated1After) = joinedIsolated1 + (isolated2Before, isolated2After) = joinedIsolated2 + key1Result <- IO((MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""")) + key2Result <- IO( + (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null""") + ) // scalafix:ok DisableSyntax.null + } yield Result.all( + List( + beforeSet, + before, + beforeIsolated, + isolated1Before, + isolated1After, + isolated2Before, + isolated2After, + key1Result, + key2Result, +// (MDC.get("key-1") ==== a).log(s"""${Thread.currentThread().getName}:After: MDC.get("key-1") is not $a"""), +// (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null"""), // scalafix:ok DisableSyntax.null + ) + ) + + } + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + def testSetContextMap: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield runIO { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + { + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + before <- IO { + Result.all( + List( + (Option(MDC.get("uuid")) ==== none).log("uuid should not be found"), + (Option(MDC.get("idNum")) ==== none).log("idNum should not be found"), + (Option(MDC.get("timestamp")) ==== none).log("timestamp should not be found"), + (Option(MDC.get("someValue")) ==== none).log("someValue should not be found"), + (MDC.get(staticFieldName) ==== staticValueName) + .log(s"staticFieldName is not $staticValueName"), + (Option(MDC.get("random")) ==== none).log("random should not be found"), + ) + ) + } + _ <- IO(MDC.setContextMap(productToMap(someContext).asJava)) + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + result <- IO { + val allResults = Result.all( + List( + before, + (MDC.get("uuid") ==== someContext.uuid.toString).log("uuid doesn't match"), + (MDC.get("idNum") ==== someContext.idNum.toString).log("idNum doesn't match"), + (MDC.get("timestamp") ==== now).log("timestamp doesn't match"), + (MDC.get("someValue") ==== someContext.someValue.toString).log("someValue doesn't match"), + (Option(MDC.get(staticFieldName)) ==== none).log("staticFieldName should not be found"), + (Option(MDC.get("random")) ==== none).log("random should not be found"), + ) + ) + allResults + } + } yield result + } + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testRemove: Property = + for { + keyValuePair <- Gens.genKeyValuePair.log("keyValuePair") + } yield runIO { + before() + + (for { + valueBefore <- putAndGet(keyValuePair.key, keyValuePair.value) + + _ <- IO(MDC.remove(keyValuePair.key)) + valueAfter <- IO(MDC.get(keyValuePair.key)) + } yield Result + .all( + List( + valueBefore ==== keyValuePair.value, + valueAfter ==== null, // scalafix:ok DisableSyntax.null + ) + )) + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testRemoveMultiple: Property = + for { + keyValuePairs <- Gens.genKeyValuePairs.log("keyValuePairs") + } yield runIO { + + before() + + val ios = keyValuePairs.keyValuePairs.traverse { keyValue => + putAndGet(keyValue.key, keyValue.value).start + } + + (for { + fibers <- ios + retrievedKeyValues <- fibers.traverse(_.joinWithNever) + retrievedKeyValuesBeforeRemove <- keyValuePairs.keyValuePairs.traverse { keyValue => + IO(MDC.get(keyValue.key)) + } + fibers4Removal <- keyValuePairs.keyValuePairs.traverse { keyValue => + IO(MDC.remove(keyValue.key)).start + } + _ <- fibers4Removal.traverse_(_.joinWithNever) + retrievedKeyValuesAfterRemove <- keyValuePairs.keyValuePairs.traverse { keyValue => + IO(MDC.get(keyValue.key)) + } + } yield { + Result.all( + List( + (retrievedKeyValues.length ==== keyValuePairs.keyValuePairs.length).log("retrievedKeyValues.length check"), + (retrievedKeyValues ==== keyValuePairs.keyValuePairs.map(_.value)).log("retrievedKeyValues value check"), + (retrievedKeyValuesBeforeRemove.length ==== keyValuePairs.keyValuePairs.length) + .log("retrievedKeyValuesBeforeRemove.length check"), + (retrievedKeyValuesBeforeRemove ==== keyValuePairs.keyValuePairs.as(null)) // scalafix:ok DisableSyntax.null + .log("retrievedKeyValuesBeforeRemove value check"), + (retrievedKeyValuesAfterRemove.length ==== keyValuePairs.keyValuePairs.length) + .log("retrievedKeyValuesAfterRemove.length check"), + (retrievedKeyValuesAfterRemove ==== keyValuePairs.keyValuePairs.as(null)) // scalafix:ok DisableSyntax.null + .log("retrievedKeyValuesAfterRemove value check"), + ) + ) + }) + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testRemoveMultipleIsolatedNestedModifications: Property = + for { + a <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("1:" + _).log("a") + b <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("2:" + _).log("b") + c <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("3:" + _).log("c") + } yield runIO { + + before() + + for { + _ <- IO(MDC.put("key-1", a)) + before <- IO((MDC.get("key-1") ==== a).log("before")) + beforeIsolated <- IO((MDC.get("key-1") ==== a).log("beforeIsolated")).start.flatMap(_.joinWithNever) + + isolated1 <- (IO((MDC.get("key-1") ==== a).log("isolated1Before")) + .flatMap { isolated1Before => + IO(MDC.put("key-1", b)) *> IO( + (isolated1Before, (MDC.get("key-1") ==== b).log("isolated1After")) + ) + }) + .start + .flatMap(_.joinWithNever) + (isolated1Before, isolated1After) = isolated1 + isolated2 <- (IO((MDC.get("key-1") ==== a).log("isolated2Before")) + .flatMap { isolated2Before => + IO(MDC.put("key-2", c)) *> IO( + (isolated2Before, (MDC.get("key-2") ==== c).log("isolated2After")) + ) + }) + .start + .flatMap(_.joinWithNever) + (isolated2Before, isolated2After) = isolated2 + isolated3 <- (for { + isolated2Key1Before <- IO(MDC.get("key-1")).map(_ ==== a) + isolated2Key2Before <- + IO(MDC.get("key-2")).map(_ ==== null) // scalafix:ok DisableSyntax.null + _ <- IO(MDC.put("key-2", c)) + isolated2Key2After <- IO(MDC.get("key-2")).map(_ ==== c) + _ <- IO(MDC.remove("key-2")) + isolated2Key2AfterRemove <- + IO(MDC.get("key-2")).map(_ ==== null) // scalafix:ok DisableSyntax.null + } yield ( + isolated2Key1Before, + isolated2Key2Before, + isolated2Key2After, + isolated2Key2AfterRemove, + )).start.flatMap(_.joinWithNever) + (isolated2Key1Before, isolated2Key2Before, isolated2Key2After, isolated2Key2AfterRemove) = isolated3 + key1After <- IO((MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""")) + key2After <- IO( + (MDC.get("key-2") ==== null) // scalafix:ok DisableSyntax.null + .log("""After: MDC.get("key-2") is not null""") + ) + + _ <- IO(MDC.remove("key-1")) + key1AfterRemove = (MDC.get("key-1") ==== null) + .log("""After Remove: MDC.get("key-1") is not null""") // scalafix:ok DisableSyntax.null + } yield Result.all( + List( + before, + beforeIsolated, + isolated1Before, + isolated1After, + isolated2Before, + isolated2After, + isolated2Key1Before, + isolated2Key2Before, + isolated2Key2After, + isolated2Key2AfterRemove, + key1After, + key2After, + key1AfterRemove, + ) + ) + + } + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + def testGetCopyOfContextMap: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield runIO { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + mapBefore <- IO(MDC.getCopyOfContextMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== Map(staticFieldName -> staticValueName)) + .log("propertyMap Before") + } + expectedPropertyMap = productToMap(someContext) + _ <- IO(MDC.setContextMap(expectedPropertyMap.asJava)) + mapAfter <- IO(MDC.getCopyOfContextMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap).log("propertyMap After") + } + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + mapAfter2 <- IO(MDC.getCopyOfContextMap) + .map(propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap.updated("timestamp", now)) + .log("propertyMap After 2") + ) + } yield Result.all( + List( + mapBefore, + mapAfter, + mapAfter2, + ) + ) + + } + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + def testGetPropertyMap: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield runIO { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + mapBefore <- IO(ce3MdcAdapter.getPropertyMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== Map(staticFieldName -> staticValueName)) + .log("propertyMap Before") + } + expectedPropertyMap = productToMap(someContext) + _ <- IO(MDC.setContextMap(expectedPropertyMap.asJava)) + mapAfter <- IO(ce3MdcAdapter.getPropertyMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap).log("propertyMap After") + } + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + mapAfter2 <- IO(ce3MdcAdapter.getPropertyMap) + .map(propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap.updated("timestamp", now)) + .log("propertyMap After 2") + ) + } yield Result.all( + List( + mapBefore, + mapAfter, + mapAfter2, + ) + ) + } + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + def testGetKeys: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield runIO { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + keySetBefore <- IO(ce3MdcAdapter.getKeys) + .map { keySet => + (keySet.asScala.toSet ==== Set(staticFieldName)) + .log("keySet Before") + } + expectedPropertyMap = productToMap(someContext) + expectedKeySet = expectedPropertyMap.keySet + _ <- IO(MDC.setContextMap(expectedPropertyMap.asJava)) + keySetAfter <- IO(ce3MdcAdapter.getKeys) + .map { keySet => + (keySet.asScala.toSet ==== expectedKeySet).log("keySet After") + } + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + keySetAfter2 <- IO(ce3MdcAdapter.getKeys) + .map(keySet => + (keySet.asScala.toSet ==== expectedKeySet) + .log("keySet After 2") + ) + } yield Result.all( + List( + keySetBefore, + keySetAfter, + keySetAfter, + keySetAfter2, + ) + ) + + } + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + private def productToMap[A <: Product](product: A): Map[String, String] = { + // This doesn't work for Scala 2.12 +// val fields = product.productElementNames.toVector + + val fields = product.getClass.getDeclaredFields.map(_.getName) + val length = product.productArity + + val fieldAndValueParis = for { + n <- 0 until length + maybeValue = product.productElement(n) match { + case maybe @ (Some(_) | None) => maybe + case value => Option(value) + } + } yield (fields(n), maybeValue) + fieldAndValueParis.collect { + case (name, Some(value)) => + name -> value.toString + }.toMap + } + +} diff --git a/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Gens.scala b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Gens.scala new file mode 100644 index 00000000..4c1a2907 --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Gens.scala @@ -0,0 +1,38 @@ +package loggerf.logger.logback + +import hedgehog._ +import loggerf.logger.logback.types.{KeyValuePair, KeyValuePairs, SomeContext} + +import java.time.Instant +import java.util.UUID + +/** @author Kevin Lee + * @since 2023-07-03 + */ +object Gens { + def genKeyValuePair: Gen[KeyValuePair] = + for { + key <- Gen.string(Gen.alpha, Range.linear(1, 10)) + value <- Gen.string(Gen.alpha, Range.linear(1, 10)) + } yield KeyValuePair(key, value) + + def genKeyValuePairs: Gen[KeyValuePairs] = + genKeyValuePair.list(Range.linear(2, 10)).map { keyValuePairs => + KeyValuePairs(keyValuePairs.zipWithIndex.map { + case (keyValuePair, index) => + val prefix = index.toString + keyValuePair + .copy(key = s"$prefix:${keyValuePair.key}") + .copy(value = s"$prefix:${keyValuePair.value}") + }) + } + + val genSomeContext: Gen[SomeContext] = { + for { + uuid <- Gen.constant(UUID.randomUUID()) + idNum <- Gen.int(Range.linear(1, 9999)) + other <- Gen.string(Gen.unicode, Range.linear(1, 10)) + } yield SomeContext(uuid, idNum, Instant.now.minusSeconds(1000L), SomeContext.SomeValue(other)) + } + +} diff --git a/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/types.scala b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/types.scala new file mode 100644 index 00000000..68351089 --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/types.scala @@ -0,0 +1,20 @@ +package loggerf.logger.logback + +import java.time.Instant +import java.util.UUID + +/** @author Kevin Lee + * @since 2023-07-03 + */ +object types { + final case class KeyValuePair(key: String, value: String) + + final case class KeyValuePairs(keyValuePairs: List[KeyValuePair]) + + final case class SomeContext(uuid: UUID, idNum: Int, timestamp: Instant, someValue: SomeContext.SomeValue) + object SomeContext { + final case class SomeValue(value: String) + + } + +} diff --git a/modules/logger-f-logback-mdc-monix3/shared/src/main/scala/loggerf/logger/logback/Monix3MdcAdapter.scala b/modules/logger-f-logback-mdc-monix3/shared/src/main/scala/loggerf/logger/logback/Monix3MdcAdapter.scala index 53f1ef28..a708c42a 100644 --- a/modules/logger-f-logback-mdc-monix3/shared/src/main/scala/loggerf/logger/logback/Monix3MdcAdapter.scala +++ b/modules/logger-f-logback-mdc-monix3/shared/src/main/scala/loggerf/logger/logback/Monix3MdcAdapter.scala @@ -49,9 +49,9 @@ trait Monix3MdcAdapterOps { monix3MdcAdapter } - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) + @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf", "scalafix:DisableSyntax.asInstanceOf")) protected def getLoggerContext(): LoggerContext = - LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] // scalafix:ok DisableSyntax.asInstanceOf + LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] def initialize(): Monix3MdcAdapter = initializeWithMonix3MdcAdapterAndLoggerContext(new Monix3MdcAdapter, getLoggerContext()) diff --git a/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala b/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala index ac00de1c..ce98b3ac 100644 --- a/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala +++ b/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala @@ -24,7 +24,7 @@ trait Monix3MdcAdapterSpecsOnly { */ implicit val opts: Task.Options = Task.defaultOptions.enableLocalContextPropagation - @SuppressWarnings(Array("org.wartremover.warts.Throw")) + @SuppressWarnings(Array("org.wartremover.warts.Throw", "org.wartremover.warts.ToString")) private val monixMdcAdapter: Monix3MdcAdapter = try { Monix3MdcAdapter.initialize() From c50956245966f6448781ba6d84d0df61fdf649ad Mon Sep 17 00:00:00 2001 From: Kevin Lee Date: Sat, 8 Jul 2023 22:32:55 +1000 Subject: [PATCH 2/2] 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 --- build.sbt | 5 +- changelogs/2.2.0-beta2.md | 4 + .../logger/logback/Ce3MdcAdapter.scala | 83 ++- .../logback/Ce3MdcAdapterWithIoRuntime.scala | 100 +++ .../logger/logback/Ce3MdcAdapterSpec.scala | 191 ++++- .../logger/logback/Ce3MdcAdapterSpec2.scala | 10 +- .../Ce3MdcAdapterWithIoRuntimeSpec.scala | 681 ++++++++++++++++++ .../logger/logback/Monix3MdcAdapterSpec.scala | 176 ++++- 8 files changed, 1212 insertions(+), 38 deletions(-) create mode 100644 changelogs/2.2.0-beta2.md create mode 100644 modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapterWithIoRuntime.scala create mode 100644 modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterWithIoRuntimeSpec.scala diff --git a/build.sbt b/build.sbt index b790b1d6..ab210761 100644 --- a/build.sbt +++ b/build.sbt @@ -349,7 +349,7 @@ lazy val logbackMdcCatsEffect3 = module(ProjectName("logback-mdc-cats-effect3 libraryDependencies ++= Seq( libs.logbackClassic, libs.logbackScalaInterop, - libs.catsEffect3Eap, + libs.catsEffect3, libs.tests.effectieCatsEffect3, libs.tests.extrasHedgehogCatsEffect3, ) ++ libs.tests.hedgehogLibs, @@ -357,10 +357,11 @@ lazy val logbackMdcCatsEffect3 = module(ProjectName("logback-mdc-cats-effect3 scalaVersion.value, libraryDependencies.value, ), - javaOptions += "-Dcats.effect.ioLocalPropagation=true", + javaOptions += "-Dcats.effect.trackFiberContext=true", ) .dependsOn( core, + slf4jMdc, monix % Test, slf4jLogger % Test, ) diff --git a/changelogs/2.2.0-beta2.md b/changelogs/2.2.0-beta2.md new file mode 100644 index 00000000..e675cfe7 --- /dev/null +++ b/changelogs/2.2.0-beta2.md @@ -0,0 +1,4 @@ +## [2.2.0-beta2](https://github.com/Kevin-Lee/logger-f/issues?q=is%3Aissue+milestone%3Av2-m3) - 2025-01-03 + +* Add `MdcAdapter` for Cats Effect 3 to properly share context through `MDC` with `IO` and `IOLocal` from Cats Effect 3 (#441) + * This is a beta released for testing the new feature with cats-effect `3.6.0-RC1`. diff --git a/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapter.scala b/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapter.scala index d723cce4..e39f52b0 100644 --- a/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapter.scala +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapter.scala @@ -1,15 +1,13 @@ package loggerf.logger.logback -import cats.effect.unsafe.IOLocals import cats.effect.{IOLocal, SyncIO} import cats.syntax.all._ import ch.qos.logback.classic.LoggerContext import logback_scala_interop.JLoggerFMdcAdapter -import org.slf4j.{LoggerFactory, MDC} +import org.slf4j.LoggerFactory import java.util.{Map => JMap, Set => JSet} import scala.jdk.CollectionConverters._ -import scala.util.control.NonFatal /** @author Kevin Lee * @since 2023-07-07 @@ -18,7 +16,7 @@ class Ce3MdcAdapter extends JLoggerFMdcAdapter { private[this] val localContext: IOLocal[Map[String, String]] = IOLocal[Map[String, String]](Map.empty[String, String]) - .syncStep(1) + .syncStep(100) .flatMap( _.leftMap(_ => new Error( @@ -28,58 +26,79 @@ class Ce3MdcAdapter extends JLoggerFMdcAdapter { ) .unsafeRunSync() - override def put(key: String, `val`: String): Unit = - IOLocals.update(localContext)(_ + (key -> `val`)) + override def put(key: String, `val`: String): Unit = { + val unsafeThreadLocal = localContext.unsafeThreadLocal() + unsafeThreadLocal.set(unsafeThreadLocal.get + (key -> `val`)) + } - @SuppressWarnings(Array("org.wartremover.warts.Null")) + @SuppressWarnings(Array("org.wartremover.warts.Null", "org.wartremover.warts.StringPlusAny")) override def get(key: String): String = - IOLocals.get(localContext).getOrElse(key, null) // scalafix:ok DisableSyntax.null + localContext.unsafeThreadLocal().get.getOrElse(key, null) // scalafix:ok DisableSyntax.null - override def remove(key: String): Unit = IOLocals.update(localContext)(_ - key) + override def remove(key: String): Unit = { + val unsafeThreadLocal = localContext.unsafeThreadLocal() + unsafeThreadLocal.set(unsafeThreadLocal.get - key) + } - override def clear(): Unit = IOLocals.reset(localContext) + override def clear(): Unit = localContext.unsafeThreadLocal().set(Map.empty[String, String]) override def getCopyOfContextMap: JMap[String, String] = getPropertyMap0 override def setContextMap0(contextMap: JMap[String, String]): Unit = - IOLocals.set(localContext, contextMap.asScala.toMap) + localContext.unsafeThreadLocal().set(contextMap.asScala.toMap) - private def getPropertyMap0: JMap[String, String] = IOLocals.get(localContext).asJava + private def getPropertyMap0: JMap[String, String] = localContext.unsafeThreadLocal().get.asJava override def getPropertyMap: JMap[String, String] = getPropertyMap0 - override def getKeys: JSet[String] = IOLocals.get(localContext).keySet.asJava + override def getKeys: JSet[String] = localContext.unsafeThreadLocal().get.keySet.asJava } -object Ce3MdcAdapter { +object Ce3MdcAdapter extends Ce3MdcAdapterOps + +trait Ce3MdcAdapterOps { @SuppressWarnings(Array("org.wartremover.warts.Null")) - private def initialize0(): Ce3MdcAdapter = { - val field = classOf[MDC].getDeclaredField("mdcAdapter") - field.setAccessible(true) - val adapter = new Ce3MdcAdapter - field.set(null, adapter) // scalafix:ok DisableSyntax.null - field.setAccessible(false) - adapter + protected def initialize0(ce3MdcAdapter: Ce3MdcAdapter): Ce3MdcAdapter = { + org.slf4j.SetMdcAdapter(ce3MdcAdapter) + ce3MdcAdapter } @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf", "scalafix:DisableSyntax.asInstanceOf")) - def initialize(): Ce3MdcAdapter = { - val loggerContext = - LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - initializeWithLoggerContext(loggerContext) - } + protected def getLoggerContext(): LoggerContext = + LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - def initializeWithLoggerContext(loggerContext: LoggerContext): Ce3MdcAdapter = { - val adapter = initialize0() - try { - val field = classOf[LoggerContext].getDeclaredField("mdcAdapter") + def initialize(): Ce3MdcAdapter = + initializeWithCe3MdcAdapterAndLoggerContext(new Ce3MdcAdapter, getLoggerContext()) + + def initializeWithCe3MdcAdapter(ce3MdcAdapter: Ce3MdcAdapter): Ce3MdcAdapter = + initializeWithCe3MdcAdapterAndLoggerContext(ce3MdcAdapter, getLoggerContext()) + + def initializeWithLoggerContext(loggerContext: LoggerContext): Ce3MdcAdapter = + initializeWithCe3MdcAdapterAndLoggerContext(new Ce3MdcAdapter, loggerContext) + + @SuppressWarnings(Array("org.wartremover.warts.Equals")) + def initializeWithCe3MdcAdapterAndLoggerContext( + ce3MdcAdapter: Ce3MdcAdapter, + loggerContext: LoggerContext, + ): Ce3MdcAdapter = { + val adapter = initialize0(ce3MdcAdapter) + + loggerContext.setMDCAdapter(adapter) + if (loggerContext.getMDCAdapter == adapter) { + // println("[LoggerContext] It's set by setMDCAdapter.") + adapter + } else { + // println( + // "[LoggerContext] The old setMDCAdapter doesn't replace `mdcAdapter` if it has already been set, " + + // "so it will use reflection to set it in the `mdcAdapter` field." + // ) + val loggerContextClass = classOf[LoggerContext] + val field = loggerContextClass.getDeclaredField("mdcAdapter") field.setAccessible(true) field.set(loggerContext, adapter) field.setAccessible(false) adapter - } catch { - case NonFatal(_) => adapter } } } diff --git a/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapterWithIoRuntime.scala b/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapterWithIoRuntime.scala new file mode 100644 index 00000000..fb2a7688 --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapterWithIoRuntime.scala @@ -0,0 +1,100 @@ +package loggerf.logger.logback + +import cats.effect.{IOLocal, unsafe} +import ch.qos.logback.classic.LoggerContext +import logback_scala_interop.JLoggerFMdcAdapter +import org.slf4j.LoggerFactory + +import java.util.{Map => JMap, Set => JSet} +import scala.jdk.CollectionConverters._ + +/** @author Kevin Lee + * @since 2023-07-07 + */ +class Ce3MdcAdapterWithIoRuntime(private val ioRuntime: unsafe.IORuntime) extends JLoggerFMdcAdapter { + + private[this] val localContext: IOLocal[Map[String, String]] = + IOLocal[Map[String, String]](Map.empty[String, String]) + .unsafeRunSync()(ioRuntime) + + override def put(key: String, `val`: String): Unit = { + val unsafeThreadLocal = localContext.unsafeThreadLocal() + unsafeThreadLocal.set(unsafeThreadLocal.get + (key -> `val`)) + } + + @SuppressWarnings(Array("org.wartremover.warts.Null", "org.wartremover.warts.StringPlusAny")) + override def get(key: String): String = + localContext.unsafeThreadLocal().get.getOrElse(key, null) // scalafix:ok DisableSyntax.null + + override def remove(key: String): Unit = { + val unsafeThreadLocal = localContext.unsafeThreadLocal() + unsafeThreadLocal.set(unsafeThreadLocal.get - key) + } + + override def clear(): Unit = localContext.unsafeThreadLocal().set(Map.empty[String, String]) + + override def getCopyOfContextMap: JMap[String, String] = getPropertyMap0 + + override def setContextMap0(contextMap: JMap[String, String]): Unit = + localContext.unsafeThreadLocal().set(contextMap.asScala.toMap) + + private def getPropertyMap0: JMap[String, String] = localContext.unsafeThreadLocal().get.asJava + + override def getPropertyMap: JMap[String, String] = getPropertyMap0 + + override def getKeys: JSet[String] = localContext.unsafeThreadLocal().get.keySet.asJava + +} +object Ce3MdcAdapterWithIoRuntime extends Ce3MdcAdapterWithIoRuntimeOps + +trait Ce3MdcAdapterWithIoRuntimeOps { + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + protected def initialize0(ce3MdcAdapter: Ce3MdcAdapterWithIoRuntime): Ce3MdcAdapterWithIoRuntime = { + org.slf4j.SetMdcAdapter(ce3MdcAdapter) + ce3MdcAdapter + } + + @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf", "scalafix:DisableSyntax.asInstanceOf")) + protected def getLoggerContext(): LoggerContext = + LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] + + def initialize()(implicit ioRuntime: unsafe.IORuntime): Ce3MdcAdapterWithIoRuntime = + initializeWithCe3MdcAdapterWithIoRuntimeAndLoggerContext( + new Ce3MdcAdapterWithIoRuntime(ioRuntime), + getLoggerContext(), + ) + + def initializeWithCe3MdcAdapterWithIoRuntime(ce3MdcAdapter: Ce3MdcAdapterWithIoRuntime): Ce3MdcAdapterWithIoRuntime = + initializeWithCe3MdcAdapterWithIoRuntimeAndLoggerContext(ce3MdcAdapter, getLoggerContext()) + + def initializeWithLoggerContext(loggerContext: LoggerContext)( + implicit ioRuntime: unsafe.IORuntime + ): Ce3MdcAdapterWithIoRuntime = + initializeWithCe3MdcAdapterWithIoRuntimeAndLoggerContext(new Ce3MdcAdapterWithIoRuntime(ioRuntime), loggerContext) + + @SuppressWarnings(Array("org.wartremover.warts.Equals")) + def initializeWithCe3MdcAdapterWithIoRuntimeAndLoggerContext( + ce3MdcAdapter: Ce3MdcAdapterWithIoRuntime, + loggerContext: LoggerContext, + ): Ce3MdcAdapterWithIoRuntime = { + val adapter = initialize0(ce3MdcAdapter) + + loggerContext.setMDCAdapter(adapter) + if (loggerContext.getMDCAdapter == adapter) { + // println("[LoggerContext] It's set by setMDCAdapter.") + adapter + } else { + // println( + // "[LoggerContext] The old setMDCAdapter doesn't replace `mdcAdapter` if it has already been set, " + + // "so it will use reflection to set it in the `mdcAdapter` field." + // ) + val loggerContextClass = classOf[LoggerContext] + val field = loggerContextClass.getDeclaredField("mdcAdapter") + field.setAccessible(true) + field.set(loggerContext, adapter) + field.setAccessible(false) + adapter + } + } +} diff --git a/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec.scala b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec.scala index 6ec02a62..869969c5 100644 --- a/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec.scala +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec.scala @@ -14,6 +14,13 @@ import scala.jdk.CollectionConverters._ * @since 2023-07-07 */ object Ce3MdcAdapterSpec extends Properties { + private val oldValue = sys.props.put("cats.effect.trackFiberContext", "true") + println( + s"${this.getClass.getSimpleName.stripSuffix("$")}[B] cats.effect.trackFiberContext=${oldValue.getOrElse("")}" + ) + println( + s"${this.getClass.getSimpleName.stripSuffix("$")}[A] cats.effect.trackFiberContext=${sys.props.getOrElse("cats.effect.trackFiberContext", "")}" + ) implicit val ioRuntime: IORuntime = cats.effect.unsafe.implicits.global @@ -26,6 +33,10 @@ object Ce3MdcAdapterSpec extends Properties { "IO - MDC should be able to put and get with isolated nested modifications", testPutAndGetMultipleIsolatedNestedModifications, ), + property( + "IO - MDC should be able to put and get with isolated nested modifications - more complex case", + testPutAndGetMultipleIsolatedNestedModifications2, + ), property("IO - MDC: It should be able to set a context map", testSetContextMap), property("IO - MDC should be able to remove the value for the existing key", testRemove), property("IO - MDC should be able to remove the multiple values for the existing keys", testRemoveMultiple), @@ -94,7 +105,10 @@ object Ce3MdcAdapterSpec extends Properties { val beforeSet = (MDC.get("key-1") ==== null).log("before set") // scalafix:ok DisableSyntax.null MDC.put("key-1", a) + val afterBeforeSetBeforeBefore = (MDC.get("key-1") ==== a).log("after beforeSet and before before") + val test = for { +// _ <- IO(MDC.put("key-1", a)) before <- IO((MDC.get("key-1") ==== a).log("before")) beforeIsolated <- IO((MDC.get("key-1") ==== a).log("beforeIsolated")) .start @@ -126,6 +140,7 @@ object Ce3MdcAdapterSpec extends Properties { } yield Result.all( List( beforeSet, + afterBeforeSetBeforeBefore, before, beforeIsolated, isolated1Before, @@ -134,14 +149,186 @@ object Ce3MdcAdapterSpec extends Properties { isolated2After, key1Result, key2Result, -// (MDC.get("key-1") ==== a).log(s"""${Thread.currentThread().getName}:After: MDC.get("key-1") is not $a"""), -// (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null"""), // scalafix:ok DisableSyntax.null + // (MDC.get("key-1") ==== a).log(s"""${Thread.currentThread().getName}:After: MDC.get("key-1") is not $a"""), + // (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null"""), // scalafix:ok DisableSyntax.null ) ) test.unsafeRunSync() } + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testPutAndGetMultipleIsolatedNestedModifications2: Property = + for { + a <- Gen.string(Gen.alpha, Range.linear(1, 2)).map("a:" + _).log("a") + a2 <- Gen.string(Gen.alpha, Range.linear(1, 2)).map("a2:" + a + _).log("a2") + b1 <- Gen.string(Gen.alpha, Range.linear(3, 4)).map("b1:" + _).log("b1") + c2 <- Gen.string(Gen.alpha, Range.linear(5, 6)).map("c1:" + _).log("c1") + a3 <- Gen.string(Gen.alpha, Range.linear(7, 8)).map("a3:" + _).log("a3") + b3 <- Gen.string(Gen.alpha, Range.linear(9, 10)).map("b3:" + _).log("b3") + c3 <- Gen.string(Gen.alpha, Range.linear(11, 12)).map("c3:" + _).log("c3") + } yield { + + before() + + val beforeSet = (MDC.get("key-1") ==== null).log("before set") // scalafix:ok DisableSyntax.null + MDC.put("key-1", a2) + val afterBeforeSet = + (MDC.get("key-1") ==== a2) + .log(s"""after beforeSet: MDC.get("key-1") should be $a2""") // scalafix:ok DisableSyntax.null + + val test = for { +// beforeSet2 <- IO((MDC.get("key-1") ==== a2).log(s"""before set2: MDC.get("key-1") should be $a2""")) // scalafix:ok DisableSyntax.null +// _ <- IO(MDC.put("key-1", a)) + beforeSet2 <- IO( + (MDC.get("key-1") ==== a).log(s"""before set2: MDC.get("key-1") should be $a""") + ) // scalafix:ok DisableSyntax.null + before <- + IO { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + List( + (actual1 ==== a).log(s"""before: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== null).log( + s"""before: MDC.get("key-2") should be null, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""before: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + } + beforeIsolated <- IO { + val actual = MDC.get("key-1") + (actual ==== a).log(s"""beforeIsolated: MDC.get("key-1") should be $a, but it is $actual""") + } + .start + .flatMap(_.joinWithNever) + + isolated1 <- + ( + IO { + val actual = MDC.get("key-1") + (actual ==== a).log(s"""isolated1Before: MDC.get("key-1") should be $a, but it is $actual""") + }.flatMap { isolated1Before => + IO( + MDC.put("key-1", b1) + ) *> IO { + val actual = MDC.get("key-1") + List( + isolated1Before, + (actual ==== b1).log(s"""isolated1After: MDC.get("key-1") should be $b1, but it is $actual"""), + ) + } + } + ).start + isolated2 <- + ( + IO { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + List( + (actual1 ==== a).log(s"""isolated2Before: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== null).log( + s"""isolated2Before: MDC.get("key-2") should be null, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""isolated2Before: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + }.flatMap { isolated2Before => + IO( + MDC.put("key-2", c2) + ) *> IO { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + isolated2Before ++ + List( + (actual1 ==== a).log(s"""isolated2After: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== c2).log( + s"""isolated2After: MDC.get("key-2") should be $c2, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""isolated2After: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + } + } + ).start + isolated3 <- + ( + IO { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + List( + (actual1 ==== a).log(s"""isolated3Before: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== null).log( + s"""isolated3Before: MDC.get("key-2") should be null, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""isolated3Before: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + }.flatMap { isolated3Before => + IO( + MDC.put("key-1", b3) + ) *> IO( + MDC.put("key-2", c3) + ) *> IO( + MDC.put("key-3", a3) + ) *> IO { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + isolated3Before ++ List( + (actual1 ==== b3).log(s"""isolated3After: MDC.get("key-1") should be $b3, but it is $actual1"""), + (actual2 ==== c3).log(s"""isolated3After: MDC.get("key-2") should be $c3, but it is $actual2"""), + (actual3 ==== a3).log(s"""isolated3After: MDC.get("key-3") should be $a3, but it is $actual3"""), + ) + } + } + ).start + + joinedIsolated1 <- isolated1.joinWithNever + joinedIsolated2 <- isolated2.joinWithNever + joinedIsolated3 <- isolated3.joinWithNever + + key1Result <- IO((MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""")) + key2Result <- IO( + (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null""") + ) // scalafix:ok DisableSyntax.null + key3Result <- IO( + (MDC.get("key-3") ==== null).log("""After: MDC.get("key-3") is not null""") + ) // scalafix:ok DisableSyntax.null + } yield List( + beforeSet, + afterBeforeSet, + beforeSet2, + ) ++ before ++ List( + beforeIsolated + ) ++ + joinedIsolated1 ++ + joinedIsolated2 ++ + joinedIsolated3 ++ List( + key1Result, + key2Result, + key3Result, +// (MDC.get("key-1") ==== a).log(s"""${Thread.currentThread().getName}:After: MDC.get("key-1") is not $a"""), +// (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null"""), // scalafix:ok DisableSyntax.null + ) + +// val afterIo = (MDC.get("key-1") ==== a2).log(s"""after IO: MDC.get("key-1") should be $a2""") + Result.all( + test.unsafeRunSync() + ++ List( +// afterIo + ) + ) + } + def testSetContextMap: Property = for { someContext <- Gens.genSomeContext.log("someContext") diff --git a/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec2.scala b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec2.scala index b99d0022..e52f4cd5 100644 --- a/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec2.scala +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec2.scala @@ -15,6 +15,13 @@ import scala.jdk.CollectionConverters._ * @since 2023-07-07 */ object Ce3MdcAdapterSpec2 extends Properties { + private val oldValue = sys.props.put("cats.effect.trackFiberContext", "true") + println( + s"${this.getClass.getSimpleName.stripSuffix("$")}[B] cats.effect.trackFiberContext=${oldValue.getOrElse("")}" + ) + println( + s"${this.getClass.getSimpleName.stripSuffix("$")}[A] cats.effect.trackFiberContext=${sys.props.getOrElse("cats.effect.trackFiberContext", "")}" + ) implicit val ioRuntime: IORuntime = cats.effect.unsafe.implicits.global @@ -91,9 +98,10 @@ object Ce3MdcAdapterSpec2 extends Properties { before() val beforeSet = (MDC.get("key-1") ==== null).log("before set") // scalafix:ok DisableSyntax.null - MDC.put("key-1", a) +// MDC.put("key-1", a) for { + _ <- IO(MDC.put("key-1", a)) before <- IO((MDC.get("key-1") ==== a).log("before")) beforeIsolated <- IO((MDC.get("key-1") ==== a).log("beforeIsolated")) .start diff --git a/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterWithIoRuntimeSpec.scala b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterWithIoRuntimeSpec.scala new file mode 100644 index 00000000..ce6da93a --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterWithIoRuntimeSpec.scala @@ -0,0 +1,681 @@ +package loggerf.logger.logback + +import cats.effect._ +import cats.effect.unsafe.IORuntime +import cats.syntax.all._ +import hedgehog._ +import hedgehog.runner._ +import org.slf4j.MDC + +import java.time.Instant +import scala.jdk.CollectionConverters._ + +/** @author Kevin Lee + * @since 2023-07-07 + */ +object Ce3MdcAdapterWithIoRuntimeSpec extends Properties { + private val oldValue = sys.props.put("cats.effect.trackFiberContext", "true") + println( + s"${this.getClass.getSimpleName.stripSuffix("$")}[B] cats.effect.trackFiberContext=${oldValue.getOrElse("")}" + ) + println( + s"${this.getClass.getSimpleName.stripSuffix("$")}[A] cats.effect.trackFiberContext=${sys.props.getOrElse("cats.effect.trackFiberContext", "")}" + ) + + implicit val ioRuntime: IORuntime = cats.effect.unsafe.implicits.global + + private val ce3MdcAdapter: Ce3MdcAdapterWithIoRuntime = Ce3MdcAdapterWithIoRuntime.initialize() + + override def tests: List[Test] = List( + property("IO - MDC should be able to put and get a value", testPutAndGet), + property("IO - MDC should be able to put and get multiple values concurrently", testPutAndGetMultiple), + property( + "IO - MDC should be able to put and get with isolated nested modifications", + testPutAndGetMultipleIsolatedNestedModifications, + ), + property( + "IO - MDC should be able to put and get with isolated nested modifications - more complex case", + testPutAndGetMultipleIsolatedNestedModifications2, + ), + property("IO - MDC: It should be able to set a context map", testSetContextMap), + property("IO - MDC should be able to remove the value for the existing key", testRemove), + property("IO - MDC should be able to remove the multiple values for the existing keys", testRemoveMultiple), + property( + "IO - MDC should be able to remove with isolated nested modifications", + testRemoveMultipleIsolatedNestedModifications, + ), + property("IO - MDC: It should return context map for getCopyOfContextMap", testGetCopyOfContextMap), + property("IO - MDC: It should return context map for getPropertyMap", testGetPropertyMap), + property("IO - MDC: It should return context map for getKeys", testGetKeys), + ) + + def before(): Unit = MDC.clear() + + def putAndGet(key: String, value: String): IO[String] = + for { + _ <- IO(MDC.put(key, value)) + got <- IO(MDC.get(key)) + } yield got + + def testPutAndGet: Property = + for { + keyValuePair <- Gens.genKeyValuePair.log("keyValuePair") + } yield { + before() + + val io = putAndGet(keyValuePair.key, keyValuePair.value) + io.map(_ ==== keyValuePair.value) + .unsafeRunSync() + } + + def testPutAndGetMultiple: Property = for { + keyValuePairs <- Gens.genKeyValuePairs.log("keyValuePairs") + + } yield { + before() + + val ios = keyValuePairs.keyValuePairs.traverse { keyValue => + putAndGet(keyValue.key, keyValue.value).start + } + + (for { + fibers <- ios + retrievedKeyValues <- fibers.traverse(_.joinWithNever) + } yield { + Result.all( + List( + retrievedKeyValues.length ==== keyValuePairs.keyValuePairs.length, + retrievedKeyValues ==== keyValuePairs.keyValuePairs.map(_.value), + ) + ) + }) + .unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testPutAndGetMultipleIsolatedNestedModifications: Property = + for { + a <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("1:" + _).log("a") + b <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("2:" + _).log("b") + c <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("3:" + _).log("c") + } yield { + + before() + + val beforeSet = (MDC.get("key-1") ==== null).log("before set") // scalafix:ok DisableSyntax.null + MDC.put("key-1", a) + val afterBeforeSet = (MDC.get("key-1") ==== a).log("""after beforeSet: `MDC.get("key-1") ==== a` failed""") + + val test = for { +// _ <- IO(MDC.put("key-1", a)) + before <- IO((MDC.get("key-1") ==== a).log("before")) + beforeIsolated <- IO((MDC.get("key-1") ==== a).log("beforeIsolated")) + .start + .flatMap(_.joinWithNever) + + isolated1 <- ( + IO((MDC.get("key-1") ==== a).log("isolated1Before")).flatMap { isolated1Before => + IO(MDC.put("key-1", b)) *> IO( + (isolated1Before, (MDC.get("key-1") ==== b).log("isolated1After")) + ) + } + ).start + isolated2 <- ( + IO((MDC.get("key-1") ==== a).log("isolated2Before")).flatMap { isolated2Before => + IO(MDC.put("key-2", c)) *> IO( + (isolated2Before, (MDC.get("key-2") ==== c).log("isolated2After")) + ) + } + ).start + + joinedIsolated1 <- isolated1.joinWithNever + joinedIsolated2 <- isolated2.joinWithNever + (isolated1Before, isolated1After) = joinedIsolated1 + (isolated2Before, isolated2After) = joinedIsolated2 + key1Result <- IO((MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""")) + key2Result <- IO( + (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null""") + ) // scalafix:ok DisableSyntax.null + } yield Result.all( + List( + beforeSet, + afterBeforeSet, + before, + beforeIsolated, + isolated1Before, + isolated1After, + isolated2Before, + isolated2After, + key1Result, + key2Result, + // (MDC.get("key-1") ==== a).log(s"""${Thread.currentThread().getName}:After: MDC.get("key-1") is not $a"""), + // (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null"""), // scalafix:ok DisableSyntax.null + ) + ) + + test.unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testPutAndGetMultipleIsolatedNestedModifications2: Property = + for { + a <- Gen.string(Gen.alpha, Range.linear(1, 2)).map("a:" + _).log("a") + a2 <- Gen.string(Gen.alpha, Range.linear(1, 2)).map("a2:" + a + _).log("a2") + b1 <- Gen.string(Gen.alpha, Range.linear(3, 4)).map("b1:" + _).log("b1") + c2 <- Gen.string(Gen.alpha, Range.linear(5, 6)).map("c1:" + _).log("c1") + a3 <- Gen.string(Gen.alpha, Range.linear(7, 8)).map("a3:" + _).log("a3") + b3 <- Gen.string(Gen.alpha, Range.linear(9, 10)).map("b3:" + _).log("b3") + c3 <- Gen.string(Gen.alpha, Range.linear(11, 12)).map("c3:" + _).log("c3") + } yield { + + before() + + val beforeSet = (MDC.get("key-1") ==== null).log("before set") // scalafix:ok DisableSyntax.null + MDC.put("key-1", a2) + val afterBeforeSet = + (MDC.get("key-1") ==== a2) + .log(s"""after beforeSet: MDC.get("key-1") should be $a2""") // scalafix:ok DisableSyntax.null + + val test = for { + beforeSet1 <- IO((MDC.get("key-1") ==== a2).log("before set2")) // scalafix:ok DisableSyntax.null + _ <- IO(MDC.put("key-1", a)) + beforeSet2 <- IO( + (MDC.get("key-1") ==== a).log(s"""before set2: MDC.get("key-1") should be $a""") + ) // scalafix:ok DisableSyntax.null + _ <- IO(MDC.put("key-1", a)) + before <- + IO { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + List( + (actual1 ==== a).log(s"""before: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== null).log( + s"""before: MDC.get("key-2") should be null, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""before: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + } + beforeIsolated <- IO { + val actual = MDC.get("key-1") + (actual ==== a).log(s"""beforeIsolated: MDC.get("key-1") should be $a, but it is $actual""") + } + .start + .flatMap(_.joinWithNever) + + isolated1 <- + ( + IO { + val actual = MDC.get("key-1") + (actual ==== a).log(s"""isolated1Before: MDC.get("key-1") should be $a, but it is $actual""") + }.flatMap { isolated1Before => + IO( + MDC.put("key-1", b1) + ) *> IO { + val actual = MDC.get("key-1") + List( + isolated1Before, + (actual ==== b1).log(s"""isolated1After: MDC.get("key-1") should be $b1, but it is $actual"""), + ) + } + } + ).start + isolated2 <- + ( + IO { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + List( + (actual1 ==== a).log(s"""isolated2Before: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== null).log( + s"""isolated2Before: MDC.get("key-2") should be null, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""isolated2Before: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + }.flatMap { isolated2Before => + IO( + MDC.put("key-2", c2) + ) *> IO { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + isolated2Before ++ + List( + (actual1 ==== a).log(s"""isolated2After: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== c2).log( + s"""isolated2After: MDC.get("key-2") should be $c2, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""isolated2After: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + } + } + ).start + isolated3 <- + ( + IO { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + List( + (actual1 ==== a).log(s"""isolated3Before: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== null).log( + s"""isolated3Before: MDC.get("key-2") should be null, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""isolated3Before: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + }.flatMap { isolated3Before => + IO( + MDC.put("key-1", b3) + ) *> IO( + MDC.put("key-2", c3) + ) *> IO( + MDC.put("key-3", a3) + ) *> IO { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + isolated3Before ++ List( + (actual1 ==== b3).log(s"""isolated3After: MDC.get("key-1") should be $b3, but it is $actual1"""), + (actual2 ==== c3).log(s"""isolated3After: MDC.get("key-2") should be $c3, but it is $actual2"""), + (actual3 ==== a3).log(s"""isolated3After: MDC.get("key-3") should be $a3, but it is $actual3"""), + ) + } + } + ).start + + joinedIsolated1 <- isolated1.joinWithNever + joinedIsolated2 <- isolated2.joinWithNever + joinedIsolated3 <- isolated3.joinWithNever + + key1Result <- IO((MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""")) + key2Result <- IO( + (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null""") + ) // scalafix:ok DisableSyntax.null + key3Result <- IO( + (MDC.get("key-3") ==== null).log("""After: MDC.get("key-3") is not null""") + ) // scalafix:ok DisableSyntax.null + } yield List( + beforeSet, + afterBeforeSet, + beforeSet1, + beforeSet2, + ) ++ before ++ List( + beforeIsolated + ) ++ + joinedIsolated1 ++ + joinedIsolated2 ++ + joinedIsolated3 ++ List( + key1Result, + key2Result, + key3Result, +// (MDC.get("key-1") ==== a).log(s"""${Thread.currentThread().getName}:After: MDC.get("key-1") is not $a"""), +// (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null"""), // scalafix:ok DisableSyntax.null + ) + +// val afterIo = (MDC.get("key-1") ==== a2).log(s"""after IO: MDC.get("key-1") should be $a2""") + Result.all( + test.unsafeRunSync() ++ + List( +// afterIo + ) + ) + } + + def testSetContextMap: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + val result = { + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + before <- IO { + Result.all( + List( + (Option(MDC.get("uuid")) ==== none).log("uuid should not be found"), + (Option(MDC.get("idNum")) ==== none).log("idNum should not be found"), + (Option(MDC.get("timestamp")) ==== none).log("timestamp should not be found"), + (Option(MDC.get("someValue")) ==== none).log("someValue should not be found"), + (MDC.get(staticFieldName) ==== staticValueName) + .log(s"staticFieldName is not $staticValueName"), + (Option(MDC.get("random")) ==== none).log("random should not be found"), + ) + ) + } + _ <- IO(MDC.setContextMap(productToMap(someContext).asJava)) + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + result <- IO { + Result.all( + List( + before, + (MDC.get("uuid") ==== someContext.uuid.toString).log("uuid doesn't match"), + (MDC.get("idNum") ==== someContext.idNum.toString).log("idNum doesn't match"), + (MDC.get("timestamp") ==== now).log("timestamp doesn't match"), + (MDC.get("someValue") ==== someContext.someValue.toString).log("someValue doesn't match"), + (Option(MDC.get(staticFieldName)) ==== none).log("staticFieldName should not be found"), + (Option(MDC.get("random")) ==== none).log("random should not be found"), + ) + ) + } + } yield result + } + result.unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testRemove: Property = + for { + keyValuePair <- Gens.genKeyValuePair.log("keyValuePair") + } yield { + before() + + (for { + valueBefore <- putAndGet(keyValuePair.key, keyValuePair.value) + + _ <- IO(MDC.remove(keyValuePair.key)) + valueAfter <- IO(MDC.get(keyValuePair.key)) + } yield Result + .all( + List( + valueBefore ==== keyValuePair.value, + valueAfter ==== null, // scalafix:ok DisableSyntax.null + ) + )) + .unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testRemoveMultiple: Property = for { + keyValuePairs <- Gens.genKeyValuePairs.log("keyValuePairs") + } yield { + + before() + + val ios = keyValuePairs.keyValuePairs.traverse { keyValue => + putAndGet(keyValue.key, keyValue.value).start + } + + (for { + fibers <- ios + retrievedKeyValues <- fibers.traverse(_.joinWithNever) + retrievedKeyValuesBeforeRemove <- keyValuePairs.keyValuePairs.traverse { keyValue => + IO(MDC.get(keyValue.key)) + } + fibers4Removal <- keyValuePairs.keyValuePairs.traverse { keyValue => + IO(MDC.remove(keyValue.key)).start + } + _ <- fibers4Removal.traverse_(_.joinWithNever) + retrievedKeyValuesAfterRemove <- keyValuePairs.keyValuePairs.traverse { keyValue => + IO(MDC.get(keyValue.key)) + } + } yield { + Result.all( + List( + (retrievedKeyValues.length ==== keyValuePairs.keyValuePairs.length).log("retrievedKeyValues.length check"), + (retrievedKeyValues ==== keyValuePairs.keyValuePairs.map(_.value)).log("retrievedKeyValues value check"), + (retrievedKeyValuesBeforeRemove.length ==== keyValuePairs.keyValuePairs.length) + .log("retrievedKeyValuesBeforeRemove.length check"), + (retrievedKeyValuesBeforeRemove ==== keyValuePairs.keyValuePairs.as(null)) // scalafix:ok DisableSyntax.null + .log("retrievedKeyValuesBeforeRemove value check"), + (retrievedKeyValuesAfterRemove.length ==== keyValuePairs.keyValuePairs.length) + .log("retrievedKeyValuesAfterRemove.length check"), + (retrievedKeyValuesAfterRemove ==== keyValuePairs.keyValuePairs.as(null)) // scalafix:ok DisableSyntax.null + .log("retrievedKeyValuesAfterRemove value check"), + ) + ) + }) + .unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testRemoveMultipleIsolatedNestedModifications: Property = + for { + a <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("1:" + _).log("a") + b <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("2:" + _).log("b") + c <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("3:" + _).log("c") + } yield { + + before() + + val test = for { + _ <- IO(MDC.put("key-1", a)) + before <- IO((MDC.get("key-1") ==== a).log("before")) + beforeIsolated <- IO((MDC.get("key-1") ==== a).log("beforeIsolated")).start.flatMap(_.joinWithNever) + + isolated1 <- (IO((MDC.get("key-1") ==== a).log("isolated1Before")) + .flatMap { isolated1Before => + IO(MDC.put("key-1", b)) *> IO( + (isolated1Before, (MDC.get("key-1") ==== b).log("isolated1After")) + ) + }) + .start + .flatMap(_.joinWithNever) + (isolated1Before, isolated1After) = isolated1 + isolated2 <- (IO((MDC.get("key-1") ==== a).log("isolated2Before")) + .flatMap { isolated2Before => + IO(MDC.put("key-2", c)) *> IO( + (isolated2Before, (MDC.get("key-2") ==== c).log("isolated2After")) + ) + }) + .start + .flatMap(_.joinWithNever) + (isolated2Before, isolated2After) = isolated2 + isolated3 <- (for { + isolated2Key1Before <- IO(MDC.get("key-1")).map(_ ==== a) + isolated2Key2Before <- + IO(MDC.get("key-2")).map(_ ==== null) // scalafix:ok DisableSyntax.null + _ <- IO(MDC.put("key-2", c)) + isolated2Key2After <- IO(MDC.get("key-2")).map(_ ==== c) + _ <- IO(MDC.remove("key-2")) + isolated2Key2AfterRemove <- + IO(MDC.get("key-2")).map(_ ==== null) // scalafix:ok DisableSyntax.null + } yield ( + isolated2Key1Before, + isolated2Key2Before, + isolated2Key2After, + isolated2Key2AfterRemove, + )).start.flatMap(_.joinWithNever) + (isolated2Key1Before, isolated2Key2Before, isolated2Key2After, isolated2Key2AfterRemove) = isolated3 + key1After <- IO((MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""")) + key2After <- IO( + (MDC.get("key-2") ==== null) // scalafix:ok DisableSyntax.null + .log("""After: MDC.get("key-2") is not null""") + ) + + _ <- IO(MDC.remove("key-1")) + key1AfterRemove = (MDC.get("key-1") ==== null) + .log("""After Remove: MDC.get("key-1") is not null""") // scalafix:ok DisableSyntax.null + } yield Result.all( + List( + before, + beforeIsolated, + isolated1Before, + isolated1After, + isolated2Before, + isolated2After, + isolated2Key1Before, + isolated2Key2Before, + isolated2Key2After, + isolated2Key2AfterRemove, + key1After, + key2After, + key1AfterRemove, + ) + ) + + test.unsafeRunSync() + } + + def testGetCopyOfContextMap: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + val result = { + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + mapBefore <- IO(MDC.getCopyOfContextMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== Map(staticFieldName -> staticValueName)) + .log("propertyMap Before") + } + expectedPropertyMap = productToMap(someContext) + _ <- IO(MDC.setContextMap(expectedPropertyMap.asJava)) + mapAfter <- IO(MDC.getCopyOfContextMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap).log("propertyMap After") + } + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + mapAfter2 <- IO(MDC.getCopyOfContextMap) + .map(propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap.updated("timestamp", now)) + .log("propertyMap After 2") + ) + } yield Result.all( + List( + mapBefore, + mapAfter, + mapAfter2, + ) + ) + } + result.unsafeRunSync() + } + + def testGetPropertyMap: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + val result = { + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + mapBefore <- IO(ce3MdcAdapter.getPropertyMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== Map(staticFieldName -> staticValueName)) + .log("propertyMap Before") + } + expectedPropertyMap = productToMap(someContext) + _ <- IO(MDC.setContextMap(expectedPropertyMap.asJava)) + mapAfter <- IO(ce3MdcAdapter.getPropertyMap) + .map { propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap).log("propertyMap After") + } + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + mapAfter2 <- IO(ce3MdcAdapter.getPropertyMap) + .map(propertyMap => + (propertyMap.asScala.toMap ==== expectedPropertyMap.updated("timestamp", now)) + .log("propertyMap After 2") + ) + } yield Result.all( + List( + mapBefore, + mapAfter, + mapAfter2, + ) + ) + } + result.unsafeRunSync() + } + + def testGetKeys: Property = + for { + someContext <- Gens.genSomeContext.log("someContext") + } yield { + + before() + + val staticFieldName = "staticFieldName" + val staticValueName = "staticValueName" + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + val result = { + for { + _ <- IO(MDC.put(staticFieldName, staticValueName)) + keySetBefore <- IO(ce3MdcAdapter.getKeys) + .map { keySet => + (keySet.asScala.toSet ==== Set(staticFieldName)) + .log("keySet Before") + } + expectedPropertyMap = productToMap(someContext) + expectedKeySet = expectedPropertyMap.keySet + _ <- IO(MDC.setContextMap(expectedPropertyMap.asJava)) + keySetAfter <- IO(ce3MdcAdapter.getKeys) + .map { keySet => + (keySet.asScala.toSet ==== expectedKeySet).log("keySet After") + } + now = Instant.now().toString + _ <- IO(MDC.put("timestamp", now)) + _ <- IO(MDC.put("idNum", "ABC")).start.flatMap(_.joinWithNever) + keySetAfter2 <- IO(ce3MdcAdapter.getKeys) + .map(keySet => + (keySet.asScala.toSet ==== expectedKeySet) + .log("keySet After 2") + ) + } yield Result.all( + List( + keySetBefore, + keySetAfter, + keySetAfter, + keySetAfter2, + ) + ) + } + result.unsafeRunSync() + } + + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + private def productToMap[A <: Product](product: A): Map[String, String] = { + // This doesn't work for Scala 2.12 +// val fields = product.productElementNames.toVector + + val fields = product.getClass.getDeclaredFields.map(_.getName) + val length = product.productArity + + val fieldAndValueParis = for { + n <- 0 until length + maybeValue = product.productElement(n) match { + case maybe @ (Some(_) | None) => maybe + case value => Option(value) + } + } yield (fields(n), maybeValue) + fieldAndValueParis.collect { + case (name, Some(value)) => + name -> value.toString + }.toMap + } + +} diff --git a/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala b/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala index ce98b3ac..31154b13 100644 --- a/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala +++ b/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala @@ -49,6 +49,10 @@ trait Monix3MdcAdapterSpecsOnly { "Task - MDC should be able to put and get with isolated nested modifications", testPutAndGetMultipleIsolatedNestedModifications, ), + property( + "Task - MDC should be able to put and get with isolated nested modifications - more complex case", + testPutAndGetMultipleIsolatedNestedModifications2, + ), property("Task - MDC: It should be able to set a context map", testSetContextMap), property("Task - MDC should be able to remove the value for the existing key", testRemove), property("Task - MDC should be able to remove the multiple values for the existing keys", testRemoveMultiple), @@ -119,8 +123,10 @@ trait Monix3MdcAdapterSpecsOnly { implicit val scheduler: monix.execution.Scheduler = monix.execution.Scheduler.traced before() + val beforeSet = (MDC.get("key-1") ==== null).log("before set") // scalafix:ok DisableSyntax.null + MDC.put("key-1", a) + val test = for { - _ <- Task(MDC.put("key-1", a)) before <- Task(MDC.get("key-1") ==== a) beforeIsolated <- TaskLocal.isolate(Task(MDC.get("key-1") ==== a)) @@ -144,6 +150,7 @@ trait Monix3MdcAdapterSpecsOnly { (isolated2Before, isolated2After) = isolated2 } yield Result.all( List( + beforeSet, before.log("before"), beforeIsolated.log("beforeIsolated"), isolated1Before.log("isolated1Before"), @@ -158,6 +165,173 @@ trait Monix3MdcAdapterSpecsOnly { test.runSyncUnsafe() } + @SuppressWarnings(Array("org.wartremover.warts.Null")) + def testPutAndGetMultipleIsolatedNestedModifications2: Property = + for { + a <- Gen.string(Gen.alpha, Range.linear(1, 2)).map("a:" + _).log("a") + a2 <- Gen.string(Gen.alpha, Range.linear(1, 2)).map("a2:" + a + _).log("a2") + b1 <- Gen.string(Gen.alpha, Range.linear(3, 4)).map("b1:" + _).log("b1") + c2 <- Gen.string(Gen.alpha, Range.linear(5, 6)).map("c1:" + _).log("c1") + a3 <- Gen.string(Gen.alpha, Range.linear(7, 8)).map("a3:" + _).log("a3") + b3 <- Gen.string(Gen.alpha, Range.linear(9, 10)).map("b3:" + _).log("b3") + c3 <- Gen.string(Gen.alpha, Range.linear(11, 12)).map("c3:" + _).log("c3") + } yield { + implicit val scheduler: monix.execution.Scheduler = monix.execution.Scheduler.traced + + before() + + val beforeSet = (MDC.get("key-1") ==== null).log("before set") // scalafix:ok DisableSyntax.null + MDC.put("key-1", a2) + val beforeAndAfterSet = + (MDC.get("key-1") ==== a2).log(s"""before: MDC.get("key-1") should be $a2""") // scalafix:ok DisableSyntax.null + + val test = for { + beforeSet2 <- Task((MDC.get("key-1") ==== a2).log("before set2")) // scalafix:ok DisableSyntax.null + _ <- Task(MDC.put("key-1", a)) + before <- + Task { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + List( + (actual1 ==== a).log(s"""before: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== null).log( + s"""before: MDC.get("key-2") should be null, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""before: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + } + beforeIsolated <- TaskLocal.isolate(Task { + val actual = MDC.get("key-1") + (actual ==== a).log(s"""beforeIsolated: MDC.get("key-1") should be $a, but it is $actual""") + }) + + isolated1 <- + TaskLocal.isolate( + Task { + val actual = MDC.get("key-1") + (actual ==== a).log(s"""isolated1Before: MDC.get("key-1") should be $a, but it is $actual""") + }.flatMap { isolated1Before => + Task( + MDC.put("key-1", b1) + ) *> Task { + val actual = MDC.get("key-1") + List( + isolated1Before, + (actual ==== b1).log(s"""isolated1After: MDC.get("key-1") should be $b1, but it is $actual"""), + ) + } + } + ) + isolated2 <- + TaskLocal.isolate( + Task { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + List( + (actual1 ==== a).log(s"""isolated2Before: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== null).log( + s"""isolated2Before: MDC.get("key-2") should be null, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""isolated2Before: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + }.flatMap { isolated2Before => + Task( + MDC.put("key-2", c2) + ) *> Task { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + isolated2Before ++ + List( + (actual1 ==== a).log(s"""isolated2After: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== c2).log( + s"""isolated2After: MDC.get("key-2") should be $c2, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""isolated2After: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + } + } + ) + isolated3 <- + TaskLocal.isolate( + Task { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + List( + (actual1 ==== a).log(s"""isolated3Before: MDC.get("key-1") should be $a, but it is $actual1"""), + (actual2 ==== null).log( + s"""isolated3Before: MDC.get("key-2") should be null, but it is $actual2""" + ), // scalafix:ok DisableSyntax.null + (actual3 ==== null).log( + s"""isolated3Before: MDC.get("key-3") should be null, but it is $actual3""" + ), // scalafix:ok DisableSyntax.null + ) + }.flatMap { isolated3Before => + Task( + MDC.put("key-1", b3) + ) *> Task( + MDC.put("key-2", c3) + ) *> Task( + MDC.put("key-3", a3) + ) *> Task { + val actual1 = MDC.get("key-1") + val actual2 = MDC.get("key-2") + val actual3 = MDC.get("key-3") + isolated3Before ++ List( + (actual1 ==== b3).log(s"""isolated3After: MDC.get("key-1") should be $b3, but it is $actual1"""), + (actual2 ==== c3).log(s"""isolated3After: MDC.get("key-2") should be $c3, but it is $actual2"""), + (actual3 ==== a3).log(s"""isolated3After: MDC.get("key-3") should be $a3, but it is $actual3"""), + ) + } + } + ) + + joinedIsolated1 = isolated1 + joinedIsolated2 = isolated2 + joinedIsolated3 = isolated3 + + key1Result <- Task((MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""")) + key2Result <- Task( + (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null""") + ) // scalafix:ok DisableSyntax.null + key3Result <- Task( + (MDC.get("key-3") ==== null).log("""After: MDC.get("key-3") is not null""") + ) // scalafix:ok DisableSyntax.null + } yield List( + beforeSet, + beforeAndAfterSet, + beforeSet2, + ) ++ before ++ List( + beforeIsolated + ) ++ + joinedIsolated1 ++ + joinedIsolated2 ++ + joinedIsolated3 ++ List( + key1Result, + key2Result, + key3Result, + // (MDC.get("key-1") ==== a).log(s"""${Thread.currentThread().getName}:After: MDC.get("key-1") is not $a"""), + // (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null"""), // scalafix:ok DisableSyntax.null + ) + + val afterIo = MDC.get("key-1") ==== a2 + Result.all( + test.runSyncUnsafe() ++ + List( + afterIo + ) + ) + } + def testSetContextMap: Property = for { someContext <- Gens.genSomeContext.log("someContext")