From abbaa38f41ed5528845e735fdca59a095d0c305f 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 --- .github/workflows/build.yml | 6 +- .github/workflows/coverage.yml | 2 +- .github/workflows/release.yml | 6 +- build.sbt | 74 ++- .../main/scala-3/loggerf/instances/show.scala | 4 +- .../src/main/scala-3/loggerf/core/ToLog.scala | 4 +- .../logger/logback/Ce3MdcAdapter.scala | 85 +++ .../logger/logback/Ce3MdcAdapterSpec.scala | 496 ++++++++++++++++++ .../logger/logback/Ce3MdcAdapterSpec2.scala | 485 +++++++++++++++++ .../scala/loggerf/logger/logback/Gens.scala | 38 ++ .../scala/loggerf/logger/logback/types.scala | 20 + .../logger/logback/Monix3MdcAdapter.scala | 4 +- .../logger/logback/Monix3MdcAdapterSpec.scala | 10 +- project/plugins.sbt | 16 +- 14 files changed, 1205 insertions(+), 45 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/.github/workflows/build.yml b/.github/workflows/build.yml index 98e9c905..89a1b1bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,9 +19,9 @@ jobs: strategy: matrix: scala: - - { name: "Scala 2", version: "2.12.13", binary-version: "2.12", java-version: "11", java-distribution: "temurin", report: "" } - - { name: "Scala 2", version: "2.13.6", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "report" } - - { name: "Scala 3", version: "3.0.2", binary-version: "3", java-version: "11", java-distribution: "temurin", report: "" } + - { name: "Scala 2", version: "2.12.18", binary-version: "2.12", java-version: "11", java-distribution: "temurin", report: "" } + - { name: "Scala 2", version: "2.13.11", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "report" } + - { name: "Scala 3", version: "3.3.0", binary-version: "3", java-version: "11", java-distribution: "temurin", report: "" } steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index aae17477..75dcc50c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: scala: - - { name: "Scala 2", version: "2.13.6", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "report" } + - { name: "Scala 2", version: "2.13.11", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "report" } steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1f7bafa..7f887c16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,9 +22,9 @@ jobs: strategy: matrix: scala: - - { name: "Scala 2", version: "2.12.13", binary-version: "2.12", java-version: "11", java-distribution: "temurin", report: "" } - - { name: "Scala 2", version: "2.13.6", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "" } - - { name: "Scala 3", version: "3.0.2", binary-version: "3", java-version: "11", java-distribution: "temurin", report: "" } + - { name: "Scala 2", version: "2.12.18", binary-version: "2.12", java-version: "11", java-distribution: "temurin", report: "" } + - { name: "Scala 2", version: "2.13.11", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "" } + - { name: "Scala 3", version: "3.3.0", binary-version: "3", java-version: "11", java-distribution: "temurin", report: "" } steps: - uses: actions/checkout@v4 diff --git a/build.sbt b/build.sbt index 9bf094c5..52bcd642 100644 --- a/build.sbt +++ b/build.sbt @@ -83,6 +83,8 @@ lazy val loggerF = (project in file(".")) catsJs, logbackMdcMonix3Jvm, logbackMdcMonix3Js, + logbackMdcCatsEffect3Jvm, + logbackMdcCatsEffect3Js, testKitJvm, testKitJs, catsEffectJvm, @@ -109,7 +111,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( @@ -124,7 +126,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)) @@ -140,7 +142,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)) @@ -200,7 +202,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)) @@ -232,7 +234,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)) @@ -251,7 +253,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( @@ -274,7 +276,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)) @@ -292,7 +318,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)) @@ -307,7 +333,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)) @@ -325,7 +351,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)) @@ -340,7 +366,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( @@ -514,8 +540,8 @@ lazy val props = final val GitHubUsername = "Kevin-Lee" final val RepoName = "logger-f" - final val Scala3Versions = List("3.0.2") - final val Scala2Versions = List("2.13.6", "2.12.13") + final val Scala3Versions = List("3.3.0") + final val Scala2Versions = List("2.13.11", "2.12.18") // final val ProjectScalaVersion = Scala3Versions.head final val ProjectScalaVersion = Scala2Versions.head @@ -581,6 +607,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 @@ -611,6 +639,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 @@ -625,14 +655,7 @@ def prefixedProjectName(name: String) = s"${props.RepoName}${if (name.isEmpty) " def libraryDependenciesRemoveScala3Incompatible( scalaVersion: String, libraries: Seq[ModuleID], -): Seq[ModuleID] = - ( - if (scalaVersion.startsWith("3.")) - libraries - .filterNot(props.removeDottyIncompatible) - else - libraries - ) +): Seq[ModuleID] = libraries lazy val mavenCentralPublishSettings: SettingsDefinition = List( /* Publish to Maven Central { */ @@ -667,6 +690,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 := @@ -693,3 +717,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-cats/shared/src/main/scala-3/loggerf/instances/show.scala b/modules/logger-f-cats/shared/src/main/scala-3/loggerf/instances/show.scala index 4b17a820..aa0288be 100644 --- a/modules/logger-f-cats/shared/src/main/scala-3/loggerf/instances/show.scala +++ b/modules/logger-f-cats/shared/src/main/scala-3/loggerf/instances/show.scala @@ -7,7 +7,9 @@ import loggerf.core.ToLog * @since 2022-02-19 */ trait show { - inline given showToLog[A: Show]: ToLog[A] = Show[A].show(_) + given showToLog[A: Show]: ToLog[A] with { + inline def toLogMessage(a: A): String = Show[A].show(a) + } } object show extends show diff --git a/modules/logger-f-core/shared/src/main/scala-3/loggerf/core/ToLog.scala b/modules/logger-f-core/shared/src/main/scala-3/loggerf/core/ToLog.scala index aeba91a0..68d22a48 100644 --- a/modules/logger-f-core/shared/src/main/scala-3/loggerf/core/ToLog.scala +++ b/modules/logger-f-core/shared/src/main/scala-3/loggerf/core/ToLog.scala @@ -15,5 +15,7 @@ object ToLog { @SuppressWarnings(Array("org.wartremover.warts.ToString")) def fromToString[A]: ToLog[A] = _.toString - inline given stringToLog: ToLog[String] = identity(_) + given stringToLog: ToLog[String] with { + inline def toLogMessage(a: String): String = a + } } 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..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..c1791cd2 --- /dev/null +++ b/modules/logger-f-logback-mdc-cats-effect3/shared/src/test/scala/loggerf/logger/logback/Ce3MdcAdapterSpec2.scala @@ -0,0 +1,485 @@ +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 { + 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 00ae59af..d48b08c7 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 @@ -53,9 +53,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 a420484e..c8decd75 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 @@ -249,7 +249,7 @@ object Monix3MdcAdapterSpec extends Properties { .runSyncUnsafe() } - @SuppressWarnings(Array("org.wartremover.warts.Null")) + @SuppressWarnings(Array("org.wartremover.warts.Null", "scalafix:DisableSyntax.null")) def testRemoveMultipleIsolatedNestedModifications: Property = for { a <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("1:" + _).log("a") @@ -286,12 +286,12 @@ object Monix3MdcAdapterSpec extends Properties { for { isolated2Key1Before <- Task(MDC.get("key-1")).map(_ ==== a) isolated2Key2Before <- - Task(MDC.get("key-2")).map(_ ==== null) // scalafix:ok DisableSyntax.null + Task(MDC.get("key-2")).map(_ ==== null) _ <- Task(MDC.put("key-2", c)) isolated2Key2After <- Task(MDC.get("key-2")).map(_ ==== c) _ <- Task(MDC.remove("key-2")) isolated2Key2AfterRemove <- - Task(MDC.get("key-2")).map(_ ==== null) // scalafix:ok DisableSyntax.null + Task(MDC.get("key-2")).map(_ ==== null) } yield ( isolated2Key1Before, isolated2Key2Before, @@ -303,11 +303,11 @@ object Monix3MdcAdapterSpec extends Properties { (isolated2Key1Before, isolated2Key2Before, isolated2Key2After, isolated2Key2AfterRemove) = isolated3 key1After = (MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""") key2After = - (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null""") // scalafix:ok DisableSyntax.null + (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null""") _ <- Task(MDC.remove("key-1")) key1AfterRemove = (MDC.get("key-1") ==== null) - .log("""After Remove: MDC.get("key-1") is not null""") // scalafix:ok DisableSyntax.null + .log("""After Remove: MDC.get("key-1") is not null""") } yield Result.all( List( before.log("before"), diff --git a/project/plugins.sbt b/project/plugins.sbt index 9da7e607..26e232e9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,18 +2,18 @@ logLevel := sbt.Level.Warn addDependencyTreePlugin -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") -addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.4.15") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") +addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.1.3") //addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.0.5") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.4") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.6") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.8") addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.2") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.2") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") addSbtPlugin("io.kevinlee" % "sbt-docusaur" % "0.15.0") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") val sbtDevOopsVersion = "3.0.0" addSbtPlugin("io.kevinlee" % "sbt-devoops-scala" % sbtDevOopsVersion)