diff --git a/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala b/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala index ebdae01a..d86246bb 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,30 +45,91 @@ object Log { object Mdc { private object Empty extends Mdc - private final case class Context(values: NonEmptyMap[String, String]) extends Mdc { + 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 = getMdc().toString + + override def hashCode(): Int = getMdc().hashCode() + + override def equals(obj: Any): Boolean = obj match { + 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 - def apply(head: (String, String), tail: (String, String)*): Mdc = Context(NonEmptyMap.of(head, tail: _*)) + type Record = (String, String) + + @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) - def fromSeq(seq: Seq[(String, String)]): Mdc = - NonEmptyMap.fromMap(SortedMap(seq: _*)).fold(empty){ nem => Context(nem) } + @deprecated("Use Mdc.Lazy.fromMap or Mdc.Eager.fromMap", "3.9.0") + def fromMap(map: Map[String, String]): Mdc = Eager.fromMap(map) - def fromMap(map: Map[String, String]): Mdc = fromSeq(map.toSeq) + object Eager { + def apply(head: Record, tail: Record*): Mdc = EagerContext(NonEmptyMap.of(head, tail: _*)) - implicit final val mdcSemigroup: Semigroup[Mdc] = Semigroup.instance { - case (Empty, right) => right - case (left, Empty) => left - case (Context(v1), Context(v2)) => Context(v1 ++ v2) + 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 = 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 = + LazyContext(Eager(v1, v2, v3, v4, v5)) + def apply(v1: => Record, v2: => Record, v3: => Record, v4: => Record, v5: => Record, v6: => Record): Mdc = + 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 = + 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 = + 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 = + 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 = + LazyContext(Eager(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)) + + def fromSeq(seq: => Seq[Record]): Mdc = LazyContext(Eager.fromSeq(seq)) + + def fromMap(map: => Map[String, String]): Mdc = LazyContext(Eager.fromMap(map)) + } + + 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 Context(values) => Some(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) } } } diff --git a/core/src/test/scala/com/evolutiongaming/catshelper/LogSpec.scala b/core/src/test/scala/com/evolutiongaming/catshelper/LogSpec.scala index 4776210e..9a978250 100644 --- a/core/src/test/scala/com/evolutiongaming/catshelper/LogSpec.scala +++ b/core/src/test/scala/com/evolutiongaming/catshelper/LogSpec.scala @@ -46,25 +46,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"))) } @@ -73,7 +73,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/logback/src/test/scala/com/evolutiongaming/catshelper/LogOfFromLogbackSpec.scala b/logback/src/test/scala/com/evolutiongaming/catshelper/LogOfFromLogbackSpec.scala index b4b1a0e4..8d312ef0 100644 --- a/logback/src/test/scala/com/evolutiongaming/catshelper/LogOfFromLogbackSpec.scala +++ b/logback/src/test/scala/com/evolutiongaming/catshelper/LogOfFromLogbackSpec.scala @@ -10,7 +10,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/version.sbt b/version.sbt index 0f987b7f..15c5dfcf 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -ThisBuild / version := "2.14.1-SNAPSHOT" +ThisBuild / version := "2.15.0-SNAPSHOT"