From 4559b15ed320484e8155d0fe9344a457ba992bcf Mon Sep 17 00:00:00 2001 From: Marissa Date: Tue, 8 Apr 2025 23:02:26 -0400 Subject: [PATCH] Add logger with `Local` semantics --- build.sbt | 9 +- .../typelevel/log4cats/LocalLogContext.scala | 127 +++++++++ .../org/typelevel/log4cats/LocalLogger.scala | 254 ++++++++++++++++++ .../log4cats/LocalLoggerFactory.scala | 67 +++++ 4 files changed, 455 insertions(+), 2 deletions(-) create mode 100644 core/shared/src/main/scala/org/typelevel/log4cats/LocalLogContext.scala create mode 100644 core/shared/src/main/scala/org/typelevel/log4cats/LocalLogger.scala create mode 100644 core/shared/src/main/scala/org/typelevel/log4cats/LocalLoggerFactory.scala diff --git a/build.sbt b/build.sbt index d18264ca..52b2b8eb 100644 --- a/build.sbt +++ b/build.sbt @@ -28,10 +28,13 @@ ThisBuild / tlVersionIntroduced := Map("3" -> "2.1.1") val catsV = "2.11.0" val catsEffectV = "3.6.0" +val catsMtlV = "1.4.0" val slf4jV = "1.7.36" val munitCatsEffectV = "2.1.0" val logbackClassicV = "1.2.13" +val tempOtel4sV = "0.11.0" + Global / onChangedBuildSource := ReloadOnSourceChanges lazy val root = tlCrossRootProject.aggregate(core, testing, noop, slf4j, docs, `js-console`) @@ -46,8 +49,10 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( name := "log4cats-core", libraryDependencies ++= Seq( - "org.typelevel" %%% "cats-core" % catsV, - "org.typelevel" %%% "cats-effect-std" % catsEffectV + "org.typelevel" %%% "cats-core" % catsV, + "org.typelevel" %%% "cats-effect-std" % catsEffectV, + "org.typelevel" %%% "cats-mtl" % catsMtlV, + "org.typelevel" %%% "otel4s-core-common" % tempOtel4sV ), libraryDependencies ++= { if (tlIsScala3.value) Seq.empty diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/LocalLogContext.scala b/core/shared/src/main/scala/org/typelevel/log4cats/LocalLogContext.scala new file mode 100644 index 00000000..2b7a9ed7 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/LocalLogContext.scala @@ -0,0 +1,127 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import cats.mtl.{Ask, Local} +import cats.syntax.functor.* +import cats.syntax.traverse.* +import cats.{~>, Applicative, Show} +import org.typelevel.otel4s.KindTransformer + +import scala.collection.immutable.ArraySeq + +sealed trait LocalLogContext[F[_]] { + private[log4cats] def currentLogContext: F[Map[String, String]] + + private[log4cats] def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A] + + private[log4cats] final def withAddedContext[A](ctx: (String, Show.Shown)*)(fa: F[A]): F[A] = + withAddedContext { + ctx.view.map { case (k, v) => k -> v.toString }.toMap + }(fa) + + def withHighPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F] + + def withLowPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F] + + def mapK[G[_]: Applicative](implicit kt: KindTransformer[F, G]): LocalLogContext[G] +} + +object LocalLogContext { + private[this] type AskContext[F[_]] = Ask[F, Map[String, String]] + + private[this] final class MappedKLocal[F[_], G[_], E]( + localF: Local[F, E] + )(implicit + val applicative: Applicative[G], + kt: KindTransformer[F, G] + ) extends Local[G, E] { + def ask[E2 >: E]: G[E2] = + kt.liftK(localF.ask[E2]) + def local[A](ga: G[A])(f: E => E): G[A] = + kt.limitedMapK(ga) { + new (F ~> F) { + def apply[B](fb: F[B]): F[B] = localF.local(fb)(f) + } + } + } + + private[this] final class MappedKAsk[F[_], G[_], E]( + askF: Ask[F, E], + fk: F ~> G + )(implicit val applicative: Applicative[G]) + extends Ask[G, E] { + def ask[E2 >: E]: G[E2] = fk(askF.ask[E2]) + } + + private[this] final class MultiAskContext[F[_]] private[MultiAskContext] ( + asks: Seq[AskContext[F]] /* never empty */ + ) extends AskContext[F] { + implicit def applicative: Applicative[F] = asks.head.applicative + def ask[E2 >: Map[String, String]]: F[E2] = + asks + .traverse(_.ask[Map[String, String]]) + .map(_.reduceLeft(_ ++ _)) + def prependLowPriority(ask: AskContext[F]): MultiAskContext[F] = + new MultiAskContext(ask +: asks) + def appendHighPriority(ask: AskContext[F]): MultiAskContext[F] = + new MultiAskContext(asks :+ ask) + } + + private[this] object MultiAskContext { + def apply[F[_]](ask: AskContext[F]): MultiAskContext[F] = + ask match { + case multi: MultiAskContext[F] => multi + case other => new MultiAskContext(ArraySeq(other)) + } + } + + private[this] final class Impl[F[_]]( + localCtx: Local[F, Map[String, String]], + askCtx: AskContext[F] + ) extends LocalLogContext[F] { + private[log4cats] def currentLogContext: F[Map[String, String]] = + askCtx.ask[Map[String, String]] + private[log4cats] def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A] = + localCtx.local(fa)(_ ++ ctx) + + def withHighPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F] = + new Impl( + localCtx, + MultiAskContext(askCtx).appendHighPriority(ask) + ) + + def withLowPriorityAskedContext(ask: Ask[F, Map[String, String]]): LocalLogContext[F] = + new Impl( + localCtx, + MultiAskContext(askCtx).prependLowPriority(ask) + ) + + def mapK[G[_]](implicit G: Applicative[G], kt: KindTransformer[F, G]): LocalLogContext[G] = { + val localF = localCtx + val askF = askCtx + val localG = new MappedKLocal(localF) + val askG = + if (askF eq localF) localG + else new MappedKAsk(askF, kt.liftK) + new Impl(localG, askG) + } + } + + def fromLocal[F[_]](implicit localCtx: Local[F, Map[String, String]]): LocalLogContext[F] = + new Impl(localCtx, localCtx) +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/LocalLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/LocalLogger.scala new file mode 100644 index 00000000..d86739a3 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/LocalLogger.scala @@ -0,0 +1,254 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import cats.mtl.Local +import cats.syntax.flatMap.* +import cats.syntax.functor.* +import cats.{~>, Monad, Show} +import org.typelevel.otel4s.KindTransformer + +sealed trait LocalLogger[F[_]] extends SelfAwareLogger[F] { + def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A] + + def withAddedContext[A](ctx: (String, Show.Shown)*)(fa: F[A]): F[A] + + def error(ctx: Map[String, String])(msg: => String): F[Unit] + def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] + def warn(ctx: Map[String, String])(msg: => String): F[Unit] + def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] + def info(ctx: Map[String, String])(msg: => String): F[Unit] + def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] + def debug(ctx: Map[String, String])(msg: => String): F[Unit] + def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] + def trace(ctx: Map[String, String])(msg: => String): F[Unit] + def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] + + @deprecated( + "use the overload that takes a `KindTransformer` and `Monad` instead", + since = "2.8.0" + ) + override def mapK[G[_]](fk: F ~> G): SelfAwareLogger[G] = super.mapK(fk) + + def mapK[G[_]: Monad](implicit kt: KindTransformer[F, G]): LocalLogger[G] + + override def withModifiedString(f: String => String): LocalLogger[F] + + @deprecated( + "`StructuredLogger` is cumbersome and lacks `cats.mtl.Local` semantics", + since = "2.8.0" + ) + def asStructuredLogger: SelfAwareStructuredLogger[F] +} + +object LocalLogger { + private[this] final class Impl[F[_]]( + localLogContext: LocalLogContext[F], + underlying: SelfAwareStructuredLogger[F] + )(implicit F: Monad[F]) + extends LocalLogger[F] + with SelfAwareStructuredLogger[F] { + def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A] = + localLogContext.withAddedContext(ctx)(fa) + def withAddedContext[A](ctx: (String, Show.Shown)*)(fa: F[A]): F[A] = + localLogContext.withAddedContext(ctx*)(fa) + + override def addContext(ctx: Map[String, String]): Impl[F] = + new Impl(localLogContext, underlying.addContext(ctx)) + override def addContext(pairs: (String, Show.Shown)*): Impl[F] = + new Impl(localLogContext, underlying.addContext(pairs*)) + + def isErrorEnabled: F[Boolean] = underlying.isErrorEnabled + def isWarnEnabled: F[Boolean] = underlying.isWarnEnabled + def isInfoEnabled: F[Boolean] = underlying.isInfoEnabled + def isDebugEnabled: F[Boolean] = underlying.isDebugEnabled + def isTraceEnabled: F[Boolean] = underlying.isTraceEnabled + + @deprecated( + "use the overload that takes a `KindTransformer` and `Monad` instead", + since = "2.8.0" + ) + override def mapK[G[_]](fk: F ~> G): SelfAwareStructuredLogger[G] = + super.mapK(fk) + def mapK[G[_]: Monad](implicit kt: KindTransformer[F, G]): LocalLogger[G] = + new Impl(localLogContext.mapK[G], underlying.mapK(kt.liftK)) + override def withModifiedString(f: String => String): Impl[F] = + new Impl(localLogContext, underlying.withModifiedString(f)) + + @deprecated( + "`StructuredLogger` is cumbersome and lacks `cats.mtl.Local` semantics", + since = "2.8.0" + ) + def asStructuredLogger: SelfAwareStructuredLogger[F] = this + + def error(message: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.error(localCtx)(message) + } else F.unit + } + def error(t: Throwable)(message: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.error(localCtx, t)(message) + } else F.unit + } + def error(ctx: Map[String, String])(msg: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.error(localCtx ++ ctx)(msg) + } else F.unit + } + def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.error(localCtx ++ ctx, t)(msg) + } else F.unit + } + + def warn(message: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.warn(localCtx)(message) + } else F.unit + } + def warn(t: Throwable)(message: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.warn(localCtx, t)(message) + } else F.unit + } + def warn(ctx: Map[String, String])(msg: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.warn(localCtx ++ ctx)(msg) + } else F.unit + } + def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.warn(localCtx ++ ctx, t)(msg) + } else F.unit + } + + def info(message: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.info(localCtx)(message) + } else F.unit + } + def info(t: Throwable)(message: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.info(localCtx, t)(message) + } else F.unit + } + def info(ctx: Map[String, String])(msg: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.info(localCtx ++ ctx)(msg) + } else F.unit + } + def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.info(localCtx ++ ctx, t)(msg) + } else F.unit + } + + def debug(message: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.debug(localCtx)(message) + } else F.unit + } + def debug(t: Throwable)(message: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.debug(localCtx, t)(message) + } else F.unit + } + def debug(ctx: Map[String, String])(msg: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.debug(localCtx ++ ctx)(msg) + } else F.unit + } + def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.debug(localCtx ++ ctx, t)(msg) + } else F.unit + } + + def trace(message: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.trace(localCtx)(message) + } else F.unit + } + def trace(t: Throwable)(message: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.trace(localCtx, t)(message) + } else F.unit + } + def trace(ctx: Map[String, String])(msg: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.trace(localCtx ++ ctx)(msg) + } else F.unit + } + def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] = + underlying.isErrorEnabled.flatMap { enabled => + if (enabled) { + for (localCtx <- localLogContext.currentLogContext) + yield underlying.trace(localCtx ++ ctx, t)(msg) + } else F.unit + } + } + + def apply[F[_]: Monad]( + localLogContext: LocalLogContext[F], + underlying: SelfAwareStructuredLogger[F] + ): LocalLogger[F] = + new Impl(localLogContext, underlying) + + def fromLocal[F[_]: Monad]( + underlying: SelfAwareStructuredLogger[F] + )(implicit localCtx: Local[F, Map[String, String]]): LocalLogger[F] = + apply(LocalLogContext.fromLocal, underlying) +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/LocalLoggerFactory.scala b/core/shared/src/main/scala/org/typelevel/log4cats/LocalLoggerFactory.scala new file mode 100644 index 00000000..26756ff5 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/LocalLoggerFactory.scala @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.log4cats + +import cats.mtl.Local +import cats.syntax.functor.* +import cats.{Functor, Monad, Show} +import org.typelevel.otel4s.KindTransformer + +sealed trait LocalLoggerFactory[F[_]] extends LoggerFactoryGen[F] { + final type LoggerType = LocalLogger[F] + + def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A] + + def withAddedContext[A](ctx: (String, Show.Shown)*)(fa: F[A]): F[A] + + def mapK[G[_]: Monad](implicit kt: KindTransformer[F, G]): LocalLoggerFactory[G] + + def withModifiedString(f: String => String)(implicit F: Functor[F]): LocalLoggerFactory[F] +} + +object LocalLoggerFactory { + private[this] final class Impl[F[_]: Monad]( + localLogContext: LocalLogContext[F], + underlying: LoggerFactory[F] + ) extends LocalLoggerFactory[F] { + def withAddedContext[A](ctx: Map[String, String])(fa: F[A]): F[A] = + localLogContext.withAddedContext(ctx)(fa) + def withAddedContext[A](ctx: (String, Show.Shown)*)(fa: F[A]): F[A] = + localLogContext.withAddedContext(ctx*)(fa) + + def mapK[G[_]: Monad](implicit kt: KindTransformer[F, G]): LocalLoggerFactory[G] = + new Impl(localLogContext.mapK[G], underlying.mapK(kt.liftK)) + def withModifiedString(f: String => String)(implicit F: Functor[F]): LocalLoggerFactory[F] = + new Impl(localLogContext, underlying.withModifiedString(f)) + + def getLoggerFromName(name: String): LocalLogger[F] = + LocalLogger(localLogContext, underlying.getLoggerFromName(name)) + def fromName(name: String): F[LocalLogger[F]] = + underlying.fromName(name).map(LocalLogger(localLogContext, _)) + } + + def apply[F[_]: Monad]( + localLogContext: LocalLogContext[F], + underlying: LoggerFactory[F] + ): LocalLoggerFactory[F] = + new Impl(localLogContext, underlying) + + def fromLocal[F[_]: Monad]( + underlying: LoggerFactory[F] + )(implicit localCtx: Local[F, Map[String, String]]): LocalLoggerFactory[F] = + apply(LocalLogContext.fromLocal, underlying) +}