From 61626fc832c205d7c4d547612c3d647103a32647 Mon Sep 17 00:00:00 2001 From: Kevin Lee Date: Sat, 8 Jul 2023 22:32:55 +1000 Subject: [PATCH] Close #441 - Add MdcAdapter for Cats Effect 3 to properly share context through MDC with IO and IOLocal from Cats Effect 3 --- build.sbt | 61 ++- .../logger/logback/Ce3MdcAdapter.scala | 84 +++ .../logger/logback/Ce3MdcAdapterSpec.scala | 496 ++++++++++++++++++ .../logger/logback/Ce3MdcAdapterSpec2.scala | 484 +++++++++++++++++ .../scala/loggerf/logger/logback/Gens.scala | 38 ++ .../scala/loggerf/logger/logback/types.scala | 20 + 6 files changed, 1172 insertions(+), 11 deletions(-) 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 dcf2e200..b94d4cf0 100644 --- a/build.sbt +++ b/build.sbt @@ -81,6 +81,8 @@ lazy val loggerF = (project in file(".")) catsJs, logbackMdcMonix3Jvm, logbackMdcMonix3Js, + logbackMdcCatsEffect3Jvm, + logbackMdcCatsEffect3Js, testKitJvm, testKitJs, catsEffectJvm, @@ -107,7 +109,7 @@ lazy val core = ), ) lazy val coreJvm = core.jvm -lazy val coreJs = core.js +lazy val coreJs = core.js.settings(commonJsSettings) lazy val slf4jLogger = module(ProjectName("slf4j"), crossProject(JVMPlatform, JSPlatform)) .settings( @@ -122,7 +124,7 @@ lazy val slf4jLogger = module(ProjectName("slf4j"), crossProject(JVMPlatform, ) .dependsOn(core) lazy val slf4jLoggerJvm = slf4jLogger.jvm -lazy val slf4jLoggerJs = slf4jLogger.js +lazy val slf4jLoggerJs = slf4jLogger.js.settings(commonJsSettings) lazy val log4sLogger = module(ProjectName("log4s"), crossProject(JVMPlatform, JSPlatform)) @@ -138,7 +140,7 @@ lazy val log4sLogger = ) .dependsOn(core) lazy val log4sLoggerJvm = log4sLogger.jvm -lazy val log4sLoggerJs = log4sLogger.js +lazy val log4sLoggerJs = log4sLogger.js.settings(commonJsSettings) lazy val log4jLogger = module(ProjectName("log4j"), crossProject(JVMPlatform, JSPlatform)) @@ -198,7 +200,7 @@ lazy val log4jLogger = ) .dependsOn(core) lazy val log4jLoggerJvm = log4jLogger.jvm -lazy val log4jLoggerJs = log4jLogger.js +lazy val log4jLoggerJs = log4jLogger.js.settings(commonJsSettings) lazy val sbtLogging = module(ProjectName("sbt-logging"), crossProject(JVMPlatform, JSPlatform)) @@ -230,7 +232,7 @@ lazy val sbtLogging = ) .dependsOn(core) lazy val sbtLoggingJvm = sbtLogging.jvm -lazy val sbtLoggingJs = sbtLogging.js +lazy val sbtLoggingJs = sbtLogging.js.settings(commonJsSettings) lazy val cats = module(ProjectName("cats"), crossProject(JVMPlatform, JSPlatform)) @@ -248,7 +250,7 @@ lazy val cats = ) .dependsOn(core % props.IncludeTest) lazy val catsJvm = cats.jvm -lazy val catsJs = cats.js +lazy val catsJs = cats.js.settings(commonJsSettings) lazy val logbackMdcMonix3 = module(ProjectName("logback-mdc-monix3"), crossProject(JVMPlatform, JSPlatform)) .settings( @@ -271,7 +273,31 @@ lazy val logbackMdcMonix3 = module(ProjectName("logback-mdc-monix3"), crossPr slf4jLogger % Test, ) lazy val logbackMdcMonix3Jvm = logbackMdcMonix3.jvm -lazy val logbackMdcMonix3Js = logbackMdcMonix3.js +lazy val logbackMdcMonix3Js = logbackMdcMonix3.js.settings(commonJsSettings) + +lazy val logbackMdcCatsEffect3 = module(ProjectName("logback-mdc-cats-effect3"), crossProject(JVMPlatform, JSPlatform)) + .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 logbackMdcCatsEffect3Js = logbackMdcCatsEffect3.js.settings(commonJsSettings) lazy val testKit = module(ProjectName("test-kit"), crossProject(JVMPlatform, JSPlatform)) @@ -289,7 +315,7 @@ lazy val testKit = ) .dependsOn(core % props.IncludeTest) lazy val testKitJvm = testKit.jvm -lazy val testKitJs = testKit.js +lazy val testKitJs = testKit.js.settings(commonJsSettings) lazy val catsEffect = module(ProjectName("cats-effect"), crossProject(JVMPlatform, JSPlatform)) @@ -304,7 +330,7 @@ lazy val catsEffect = .settings(noPublish) .dependsOn(core % props.IncludeTest, cats) lazy val catsEffectJvm = catsEffect.jvm -lazy val catsEffectJs = catsEffect.js +lazy val catsEffectJs = catsEffect.js.settings(commonJsSettings) lazy val catsEffect3 = module(ProjectName("cats-effect3"), crossProject(JVMPlatform, JSPlatform)) @@ -322,7 +348,7 @@ lazy val catsEffect3 = .settings(noPublish) .dependsOn(core % props.IncludeTest, cats) lazy val catsEffect3Jvm = catsEffect3.jvm -lazy val catsEffect3Js = catsEffect3.js +lazy val catsEffect3Js = catsEffect3.js.settings(commonJsSettings) lazy val monix = module(ProjectName("monix"), crossProject(JVMPlatform, JSPlatform)) @@ -337,7 +363,7 @@ lazy val monix = .settings(noPublish) .dependsOn(core % props.IncludeTest, cats) lazy val monixJvm = monix.jvm -lazy val monixJs = monix.js +lazy val monixJs = monix.js.settings(commonJsSettings) lazy val testCatsEffectWithSlf4jLogger = testProject( @@ -576,6 +602,8 @@ lazy val libs = lazy val catsEffect3 = "org.typelevel" %% "cats-effect" % props.CatsEffect3Version + lazy val catsEffect3Eap = "org.typelevel" %% "cats-effect" % "3.6-02a43a6" + lazy val monix3Execution = "io.monix" %% "monix-execution" % props.Monix3Version lazy val effectieCore: ModuleID = "io.kevinlee" %% "effectie-core" % props.EffectieVersion @@ -605,6 +633,8 @@ lazy val libs = lazy val extrasCats = "io.kevinlee" %% "extras-cats" % props.ExtrasVersion % Test + lazy val effectieCatsEffect3 = "io.kevinlee" %% "effectie-cats-effect3" % props.EffectieVersion + lazy val extrasConcurrent = "io.kevinlee" %% "extras-concurrent" % props.ExtrasVersion % Test lazy val extrasConcurrentTesting = "io.kevinlee" %% "extras-concurrent-testing" % props.ExtrasVersion % Test @@ -655,6 +685,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 := @@ -681,3 +712,11 @@ def projectCommonSettings(projectName: String, crossProject: CrossProject.Builde .settings( mavenCentralPublishSettings ) + +lazy val commonJsSettings: SettingsDefinition = List( + Test / fork := false, +// Test / scalacOptions ++= (if (scalaVersion.value.startsWith("3")) List.empty +// else List("-P:scalajs:nowarnGlobalExecutionContext")), +// Test / compile / scalacOptions ++= (if (scalaVersion.value.startsWith("3")) List.empty +// else List("-P:scalajs:nowarnGlobalExecutionContext")), +) 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..081ef683 --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/main/scala/loggerf/logger/logback/Ce3MdcAdapter.scala @@ -0,0 +1,84 @@ +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")) + def initialize(): Ce3MdcAdapter = { + val loggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] // scalafix:ok DisableSyntax.asInstanceOf + 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..80ac873c --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec.scala @@ -0,0 +1,496 @@ +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 { + println(s"cats.effect.ioLocalPropagation=${sys.props.getOrElse("cats.effect.ioLocalPropagation", "")}") + sys.props.put("cats.effect.ioLocalPropagation", "true") + println(s"cats.effect.ioLocalPropagation=${sys.props.getOrElse("cats.effect.ioLocalPropagation", "")}") + + 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..bf0b3b4e --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec2.scala @@ -0,0 +1,484 @@ +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 { + println(s"cats.effect.ioLocalPropagation=${sys.props.getOrElse("cats.effect.ioLocalPropagation", "")}") + sys.props.put("cats.effect.ioLocalPropagation", "true") + println(s"cats.effect.ioLocalPropagation=${sys.props.getOrElse("cats.effect.ioLocalPropagation", "")}") + + 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 { + 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 + } + } + + @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) + + } + +}