From a408fd07abd1e930603f8effa0f04c38e93216cf Mon Sep 17 00:00:00 2001 From: Artem Khoruzhenko Date: Mon, 9 Oct 2023 13:00:24 +0200 Subject: [PATCH 1/4] Implement lazy MDC --- .../com/evolutiongaming/catshelper/Log.scala | 56 +++++++++++++++---- .../evolutiongaming/catshelper/LogSpec.scala | 30 +++++----- .../catshelper/ResourceCounterSpec.scala | 2 +- .../catshelper/LogOfFromLogback.scala | 2 +- .../catshelper/LogOfFromLogbackSpec.scala | 2 +- .../catshelper/testkit/TestFrameworkApi.scala | 1 - version.sbt | 2 +- 7 files changed, 65 insertions(+), 30 deletions(-) diff --git a/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala b/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala index ebdae01a..7ad185c1 100644 --- a/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala +++ b/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala @@ -44,30 +44,66 @@ object Log { object Mdc { private object Empty extends Mdc - private final case class Context(values: NonEmptyMap[String, String]) extends Mdc { - override def toString: String = s"MDC(${values.toSortedMap.mkString(", ")})" - } + private final case class Context(getValues: () => Map[String, String]) extends Mdc { + lazy val values: SortedMap[String, String] = SortedMap(getValues().toSeq: _*) - val empty: Mdc = Empty + override def toString: String = s"MDC(${values.mkString(", ")})" - def apply(head: (String, String), tail: (String, String)*): Mdc = Context(NonEmptyMap.of(head, tail: _*)) + override def hashCode(): Int = values.hashCode() - def fromSeq(seq: Seq[(String, String)]): Mdc = - NonEmptyMap.fromMap(SortedMap(seq: _*)).fold(empty){ nem => Context(nem) } + override def equals(obj: Any): Boolean = obj match { + case that: Context => this.values.equals(that.values) + case _ => false + } + } - def fromMap(map: Map[String, String]): Mdc = fromSeq(map.toSeq) + val empty: Mdc = Empty + + type Record = (String, String) + + @deprecated("Use Mdc.Lazy.apply. If it's not enough - Mdc.Lazy.fromMap", "3.9.0") + def apply(head: Record, tail: Record*): Mdc = Lazy.fromMap( (head +: tail).toMap) + + @deprecated("Use Mdc.Lazy.fromSeq", "3.9.0") + def fromSeq(seq: Seq[Record]): Mdc = Lazy.fromSeq(seq) + + @deprecated("Use Mdc.Lazy.fromMap", "3.9.0") + def fromMap(map: Map[String, String]): Mdc = Lazy.fromMap(map) + + object Lazy { + def apply(v1: => Record): Mdc = fromMap(Map(v1)) + def apply(v1: => Record, v2: => Record): Mdc = fromMap(Map(v1, v2)) + def apply(v1: => Record, v2: => Record, v3: => Record): Mdc = fromMap(Map(v1, v2, v3)) + def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record): Mdc = fromMap(Map(v1, v2, v3, v4)) + def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record): Mdc = + fromMap(Map(v1, v2, v3, v4, v5)) + def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record): Mdc = + fromMap(Map(v1, v2, v3, v4, v5, v6)) + def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record, v7: => Record): Mdc = + fromMap(Map(v1, v2, v3, v4, v5, v6, v7)) + def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record, v7: => Record, v8: => Record): Mdc = + fromMap(Map(v1, v2, v3, v4, v5, v6, v7, v8)) + def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record, v7: => Record, v8: => Record, v9: => Record): Mdc = + fromMap(Map(v1, v2, v3, v4, v5, v6, v7, v8, v9)) + def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record, v7: => Record, v8: => Record, v9: => Record, v10: => Record): Mdc = + fromMap(Map(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)) + + def fromSeq(seq: => Seq[Record]): Mdc = fromMap(seq.toMap) + + def fromMap(map: => Map[String, String]): Mdc = Context(() => map) + } implicit final val mdcSemigroup: Semigroup[Mdc] = Semigroup.instance { case (Empty, right) => right case (left, Empty) => left - case (Context(v1), Context(v2)) => Context(v1 ++ v2) + case (Context(v1), Context(v2)) => Context(() => v1() ++ v2()) } implicit final class MdcOps(val mdc: Mdc) extends AnyVal { def context: Option[NonEmptyMap[String, String]] = mdc match { case Empty => None - case Context(values) => Some(values) + case c: Context => NonEmptyMap.fromMap(c.values) } } } diff --git a/core/src/test/scala/com/evolutiongaming/catshelper/LogSpec.scala b/core/src/test/scala/com/evolutiongaming/catshelper/LogSpec.scala index f67d4e8f..a1a7e70c 100644 --- a/core/src/test/scala/com/evolutiongaming/catshelper/LogSpec.scala +++ b/core/src/test/scala/com/evolutiongaming/catshelper/LogSpec.scala @@ -47,25 +47,25 @@ class LogSpec extends AnyFunSuite with Matchers { val stateT = for { log0 <- logOf("source") log = log0.prefixed(">").mapK(FunctionK.id) - _ <- log.trace("trace", Log.Mdc(mdc)) - _ <- log.debug("debug", Log.Mdc(mdc)) - _ <- log.info("info", Log.Mdc(mdc)) - _ <- log.warn("warn", Log.Mdc(mdc)) - _ <- log.warn("warn", Error, Log.Mdc(mdc)) - _ <- log.error("error", Log.Mdc(mdc)) - _ <- log.error("error", Error, Log.Mdc(mdc)) + _ <- log.trace("trace", Log.Mdc.Lazy(mdc)) + _ <- log.debug("debug", Log.Mdc.Lazy(mdc)) + _ <- log.info("info", Log.Mdc.Lazy(mdc)) + _ <- log.warn("warn", Log.Mdc.Lazy(mdc)) + _ <- log.warn("warn", Error, Log.Mdc.Lazy(mdc)) + _ <- log.error("error", Log.Mdc.Lazy(mdc)) + _ <- log.error("error", Error, Log.Mdc.Lazy(mdc)) } yield {} val (state, _) = stateT.run(State(Nil)) state shouldEqual State(List( - Action.Error1("> error", Error, Log.Mdc(mdc)), - Action.Error0("> error", Log.Mdc(mdc)), - Action.Warn1("> warn", Error, Log.Mdc(mdc)), - Action.Warn0("> warn", Log.Mdc(mdc)), - Action.Info("> info", Log.Mdc(mdc)), - Action.Debug("> debug", Log.Mdc(mdc)), - Action.Trace("> trace", Log.Mdc(mdc)), + Action.Error1("> error", Error, Log.Mdc.Lazy(mdc)), + Action.Error0("> error", Log.Mdc.Lazy(mdc)), + Action.Warn1("> warn", Error, Log.Mdc.Lazy(mdc)), + Action.Warn0("> warn", Log.Mdc.Lazy(mdc)), + Action.Info("> info", Log.Mdc.Lazy(mdc)), + Action.Debug("> debug", Log.Mdc.Lazy(mdc)), + Action.Trace("> trace", Log.Mdc.Lazy(mdc)), Action.OfStr("source"))) } @@ -74,7 +74,7 @@ class LogSpec extends AnyFunSuite with Matchers { val io = for { logOf <- LogOf.slf4j[IO] log <- logOf(getClass) - _ <- log.info("whatever", Log.Mdc("k" -> "v")) + _ <- log.info("whatever", Log.Mdc.Lazy("k" -> "v")) } yield org.slf4j.MDC.getCopyOfContextMap io.unsafeRunSync() shouldEqual null diff --git a/core/src/test/scala/com/evolutiongaming/catshelper/ResourceCounterSpec.scala b/core/src/test/scala/com/evolutiongaming/catshelper/ResourceCounterSpec.scala index cbf1dcbd..68784003 100644 --- a/core/src/test/scala/com/evolutiongaming/catshelper/ResourceCounterSpec.scala +++ b/core/src/test/scala/com/evolutiongaming/catshelper/ResourceCounterSpec.scala @@ -3,7 +3,7 @@ package com.evolutiongaming.catshelper import cats.effect.std.CountDownLatch import cats.effect.syntax.all.* import cats.effect.unsafe.IORuntime -import cats.effect.{Deferred, IO, Ref, Resource} +import cats.effect.{IO, Ref, Resource} import com.evolutiongaming.catshelper.testkit.PureTest.ioTest import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers diff --git a/logback/src/main/scala/com/evolutiongaming/catshelper/LogOfFromLogback.scala b/logback/src/main/scala/com/evolutiongaming/catshelper/LogOfFromLogback.scala index af1f6515..f3da04a4 100644 --- a/logback/src/main/scala/com/evolutiongaming/catshelper/LogOfFromLogback.scala +++ b/logback/src/main/scala/com/evolutiongaming/catshelper/LogOfFromLogback.scala @@ -6,7 +6,7 @@ import ch.qos.logback.classic.spi.LoggingEvent import ch.qos.logback.classic.util.ContextInitializer import com.evolutiongaming.catshelper.Log.Mdc -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ /** * ===Motivation=== diff --git a/logback/src/test/scala/com/evolutiongaming/catshelper/LogOfFromLogbackSpec.scala b/logback/src/test/scala/com/evolutiongaming/catshelper/LogOfFromLogbackSpec.scala index 5a09ec1e..8653c968 100644 --- a/logback/src/test/scala/com/evolutiongaming/catshelper/LogOfFromLogbackSpec.scala +++ b/logback/src/test/scala/com/evolutiongaming/catshelper/LogOfFromLogbackSpec.scala @@ -11,7 +11,7 @@ class LogOfFromLogbackSpec extends AnyFunSuite with Matchers { val io = for { logOf <- LogOfFromLogback[IO] log <- logOf(getClass) - _ <- log.info("hello from logback", Log.Mdc("k" -> "test value for K")) + _ <- log.info("hello from logback", Log.Mdc.Lazy("k" -> "test value for K")) } yield () io.unsafeRunSync() diff --git a/testkit/src/main/scala/com/evolutiongaming/catshelper/testkit/TestFrameworkApi.scala b/testkit/src/main/scala/com/evolutiongaming/catshelper/testkit/TestFrameworkApi.scala index 6bc63ca1..ad2005e2 100644 --- a/testkit/src/main/scala/com/evolutiongaming/catshelper/testkit/TestFrameworkApi.scala +++ b/testkit/src/main/scala/com/evolutiongaming/catshelper/testkit/TestFrameworkApi.scala @@ -1,7 +1,6 @@ package com.evolutiongaming.catshelper.testkit import cats.effect.IO -import cats.implicits._ import cats.effect.testkit.TestContext import org.scalatest.exceptions.{TestCanceledException, TestFailedException} diff --git a/version.sbt b/version.sbt index e751e868..80a5a5e3 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -ThisBuild / version := "3.8.1-SNAPSHOT" +ThisBuild / version := "3.9.0-SNAPSHOT" From 49e466250c6f0e86f128a4d4197d3af4a18d6d50 Mon Sep 17 00:00:00 2001 From: Artem Khoruzhenko Date: Mon, 9 Oct 2023 13:12:51 +0200 Subject: [PATCH 2/4] Restore JavaConverters for backward compatibility --- .../scala/com/evolutiongaming/catshelper/LogOfFromLogback.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logback/src/main/scala/com/evolutiongaming/catshelper/LogOfFromLogback.scala b/logback/src/main/scala/com/evolutiongaming/catshelper/LogOfFromLogback.scala index f3da04a4..af1f6515 100644 --- a/logback/src/main/scala/com/evolutiongaming/catshelper/LogOfFromLogback.scala +++ b/logback/src/main/scala/com/evolutiongaming/catshelper/LogOfFromLogback.scala @@ -6,7 +6,7 @@ import ch.qos.logback.classic.spi.LoggingEvent import ch.qos.logback.classic.util.ContextInitializer import com.evolutiongaming.catshelper.Log.Mdc -import scala.jdk.CollectionConverters._ +import scala.collection.JavaConverters._ /** * ===Motivation=== From 13d88f1cdda132b201ef784ad5e106c798c26e9e Mon Sep 17 00:00:00 2001 From: Artem Khoruzhenko Date: Mon, 9 Oct 2023 13:16:17 +0200 Subject: [PATCH 3/4] Restore build --- .../evolutiongaming/catshelper/testkit/TestFrameworkApi.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/testkit/src/main/scala/com/evolutiongaming/catshelper/testkit/TestFrameworkApi.scala b/testkit/src/main/scala/com/evolutiongaming/catshelper/testkit/TestFrameworkApi.scala index ad2005e2..6bc63ca1 100644 --- a/testkit/src/main/scala/com/evolutiongaming/catshelper/testkit/TestFrameworkApi.scala +++ b/testkit/src/main/scala/com/evolutiongaming/catshelper/testkit/TestFrameworkApi.scala @@ -1,6 +1,7 @@ package com.evolutiongaming.catshelper.testkit import cats.effect.IO +import cats.implicits._ import cats.effect.testkit.TestContext import org.scalatest.exceptions.{TestCanceledException, TestFailedException} From 79bc8fbf8222771fa4b25446bf2a9aaa2cf20796 Mon Sep 17 00:00:00 2001 From: Artem Khoruzhenko Date: Mon, 9 Oct 2023 15:37:51 +0200 Subject: [PATCH 4/4] Lazy logger is based on the eager one --- .../com/evolutiongaming/catshelper/Log.scala | 86 ++++++++++++------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala b/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala index 7ad185c1..4ca6a002 100644 --- a/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala +++ b/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala @@ -5,6 +5,7 @@ import cats.effect.Sync import cats.{Applicative, Semigroup, ~>} import org.slf4j.{Logger, MDC} +import scala.annotation.tailrec import scala.collection.immutable.SortedMap trait Log[F[_]] { @@ -44,66 +45,91 @@ object Log { object Mdc { private object Empty extends Mdc - private final case class Context(getValues: () => Map[String, String]) extends Mdc { - lazy val values: SortedMap[String, String] = SortedMap(getValues().toSeq: _*) + private final case class EagerContext(values: NonEmptyMap[String, String]) extends Mdc { + override def toString: String = s"MDC(${values.toSortedMap.mkString(", ")})" + } + private final class LazyContext(val getMdc: () => Mdc) extends Mdc { - override def toString: String = s"MDC(${values.mkString(", ")})" + override def toString: String = getMdc().toString - override def hashCode(): Int = values.hashCode() + override def hashCode(): Int = getMdc().hashCode() override def equals(obj: Any): Boolean = obj match { - case that: Context => this.values.equals(that.values) + case that: LazyContext => this.getMdc().equals(that.getMdc()) case _ => false } } + private object LazyContext { + def apply(mdc: => Mdc): LazyContext = new LazyContext(() => mdc) + } val empty: Mdc = Empty type Record = (String, String) - @deprecated("Use Mdc.Lazy.apply. If it's not enough - Mdc.Lazy.fromMap", "3.9.0") - def apply(head: Record, tail: Record*): Mdc = Lazy.fromMap( (head +: tail).toMap) + @deprecated("Use Mdc.Lazy.apply or Mdc.Eager.apply", "3.9.0") + def apply(head: Record, tail: Record*): Mdc = Eager(head, tail*) + + @deprecated("Use Mdc.Lazy.fromSeq or Mdc.Eager.fromSeq", "3.9.0") + def fromSeq(seq: Seq[Record]): Mdc = Eager.fromSeq(seq) - @deprecated("Use Mdc.Lazy.fromSeq", "3.9.0") - def fromSeq(seq: Seq[Record]): Mdc = Lazy.fromSeq(seq) + @deprecated("Use Mdc.Lazy.fromMap or Mdc.Eager.fromMap", "3.9.0") + def fromMap(map: Map[String, String]): Mdc = Eager.fromMap(map) - @deprecated("Use Mdc.Lazy.fromMap", "3.9.0") - def fromMap(map: Map[String, String]): Mdc = Lazy.fromMap(map) + object Eager { + def apply(head: Record, tail: Record*): Mdc = EagerContext(NonEmptyMap.of(head, tail: _*)) + + def fromSeq(seq: Seq[Record]): Mdc = NonEmptyMap.fromMap(SortedMap(seq: _*)).fold(empty){ nem => EagerContext(nem) } + + def fromMap(map: Map[String, String]): Mdc = fromSeq(map.toSeq) + } object Lazy { - def apply(v1: => Record): Mdc = fromMap(Map(v1)) - def apply(v1: => Record, v2: => Record): Mdc = fromMap(Map(v1, v2)) - def apply(v1: => Record, v2: => Record, v3: => Record): Mdc = fromMap(Map(v1, v2, v3)) - def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record): Mdc = fromMap(Map(v1, v2, v3, v4)) + def apply(v1: => Record): Mdc = LazyContext(Eager(v1)) + def apply(v1: => Record, v2: => Record): Mdc = LazyContext(Eager(v1, v2)) + def apply(v1: => Record, v2: => Record, v3: => Record): Mdc = LazyContext(Eager(v1, v2, v3)) + def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record): Mdc = LazyContext(Eager(v1, v2, v3, v4)) def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record): Mdc = - fromMap(Map(v1, v2, v3, v4, v5)) + LazyContext(Eager(v1, v2, v3, v4, v5)) def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record): Mdc = - fromMap(Map(v1, v2, v3, v4, v5, v6)) + LazyContext(Eager(v1, v2, v3, v4, v5, v6)) def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record, v7: => Record): Mdc = - fromMap(Map(v1, v2, v3, v4, v5, v6, v7)) + LazyContext(Eager(v1, v2, v3, v4, v5, v6, v7)) def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record, v7: => Record, v8: => Record): Mdc = - fromMap(Map(v1, v2, v3, v4, v5, v6, v7, v8)) + LazyContext(Eager(v1, v2, v3, v4, v5, v6, v7, v8)) def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record, v7: => Record, v8: => Record, v9: => Record): Mdc = - fromMap(Map(v1, v2, v3, v4, v5, v6, v7, v8, v9)) + LazyContext(Eager(v1, v2, v3, v4, v5, v6, v7, v8, v9)) def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record, v7: => Record, v8: => Record, v9: => Record, v10: => Record): Mdc = - fromMap(Map(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)) + LazyContext(Eager(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)) - def fromSeq(seq: => Seq[Record]): Mdc = fromMap(seq.toMap) + def fromSeq(seq: => Seq[Record]): Mdc = LazyContext(Eager.fromSeq(seq)) - def fromMap(map: => Map[String, String]): Mdc = Context(() => map) + def fromMap(map: => Map[String, String]): Mdc = LazyContext(Eager.fromMap(map)) } - implicit final val mdcSemigroup: Semigroup[Mdc] = Semigroup.instance { - case (Empty, right) => right - case (left, Empty) => left - case (Context(v1), Context(v2)) => Context(() => v1() ++ v2()) + implicit final val mdcSemigroup: Semigroup[Mdc] = { + @tailrec def joinContexts(c1: Mdc, c2: Mdc): Mdc = (c1, c2) match { + case (Empty, right) => right + case (left, Empty) => left + case (EagerContext(v1), EagerContext(v2)) => EagerContext(v1 ++ v2) + case (c1: LazyContext, c2: LazyContext) => joinContexts(c1.getMdc(), c2.getMdc()) + case (c1: LazyContext, c2: EagerContext) => joinContexts(c1.getMdc(), c2) + case (c1: EagerContext, c2: LazyContext) => joinContexts(c1, c2.getMdc()) + } + + Semigroup.instance(joinContexts) } implicit final class MdcOps(val mdc: Mdc) extends AnyVal { - def context: Option[NonEmptyMap[String, String]] = mdc match { - case Empty => None - case c: Context => NonEmptyMap.fromMap(c.values) + def context: Option[NonEmptyMap[String, String]] = { + @tailrec def contextInner(mdc: Mdc): Option[NonEmptyMap[String, String]] = mdc match { + case Empty => None + case EagerContext(values) => Some(values) + case lc: LazyContext => contextInner(lc.getMdc()) + } + + contextInner(mdc) } } }