diff --git a/build.sbt b/build.sbt index 03d46d0c..706d416d 100644 --- a/build.sbt +++ b/build.sbt @@ -31,6 +31,7 @@ val catsEffectV = "3.6.1" val slf4jV = "1.7.36" val munitCatsEffectV = "2.1.0" val logbackClassicV = "1.2.13" +val catsMtlV = "1.4.0" Global / onChangedBuildSource := ReloadOnSourceChanges @@ -47,7 +48,8 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) name := "log4cats-core", libraryDependencies ++= Seq( "org.typelevel" %%% "cats-core" % catsV, - "org.typelevel" %%% "cats-effect-std" % catsEffectV + "org.typelevel" %%% "cats-effect-std" % catsEffectV, + "org.typelevel" %%% "cats-mtl" % catsMtlV ), libraryDependencies ++= { if (tlIsScala3.value) Seq.empty diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/LocalHelpers.scala b/core/shared/src/main/scala/org/typelevel/log4cats/LocalHelpers.scala new file mode 100644 index 00000000..3cda5cc8 --- /dev/null +++ b/core/shared/src/main/scala/org/typelevel/log4cats/LocalHelpers.scala @@ -0,0 +1,38 @@ +/* + * 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.FlatMap +import cats.mtl.Local + +object LocalHelpers { + implicit class LoggerOps[F[_]](sl: SelfAwareStructuredLogger[F]) { + def withLocalContext(implicit + local: Local[F, Map[String, String]], + F: FlatMap[F] + ): SelfAwareStructuredLogger[F] = + SelfAwareStructuredLogger.withContextF(sl)(local.ask) + } + + implicit class FactoryOps[F[_]](lf: LoggerFactory[F]) { + def withLocalContext(implicit + local: Local[F, Map[String, String]], + F: FlatMap[F] + ): LoggerFactory[F] = + LoggerFactory.withContextF(lf)(local.ask) + } +} diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/LoggerFactory.scala b/core/shared/src/main/scala/org/typelevel/log4cats/LoggerFactory.scala index f8d3ad94..ba6ddac3 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/LoggerFactory.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/LoggerFactory.scala @@ -16,13 +16,15 @@ package org.typelevel.log4cats +import cats.FlatMap import cats.Functor -import cats.Show.Shown +import cats.Monad +import cats.data.EitherT import cats.data.Kleisli +import cats.data.OptionT +import cats.Show.Shown import cats.syntax.functor.* import cats.~> -import cats.data.OptionT -import cats.data.EitherT import scala.annotation.implicitNotFound @@ -71,6 +73,22 @@ object LoggerFactory extends LoggerFactoryGenCompanion { } } + def withContextF[F[_]: FlatMap]( + lf: LoggerFactory[F] + )(ctx: F[Map[String, String]]): LoggerFactory[F] = + new LoggerFactory[F] { + override def getLoggerFromName(name: String): SelfAwareStructuredLogger[F] = + SelfAwareStructuredLogger.withContextF(lf.getLoggerFromName(name))(ctx) + + override def fromName(name: String): F[SelfAwareStructuredLogger[F]] = lf + .fromName(name) + .map(SelfAwareStructuredLogger.withContextF(_)(ctx)) + } + + def withContextFromKleisli[F[_]: Monad]( + lf: LoggerFactory[Kleisli[F, Map[String, String], *]] + ): LoggerFactory[Kleisli[F, Map[String, String], *]] = withContextF(lf)(Kleisli.ask) + private def addContext[F[_]: Functor]( lf: LoggerFactory[F], ctx: Map[String, String] @@ -94,5 +112,4 @@ object LoggerFactory extends LoggerFactoryGenCompanion { override def fromName(name: String): F[SelfAwareStructuredLogger[F]] = lf.fromName(name).map(_.withModifiedString(f)) } - } diff --git a/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala b/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala index 40907bcf..30db35d6 100644 --- a/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala +++ b/core/shared/src/main/scala/org/typelevel/log4cats/SelfAwareStructuredLogger.scala @@ -18,6 +18,8 @@ package org.typelevel.log4cats import cats.* import cats.Show.Shown +import cats.data.Kleisli +import cats.implicits.{toFlatMapOps, toFunctorOps} trait SelfAwareStructuredLogger[F[_]] extends SelfAwareLogger[F] with StructuredLogger[F] { override def mapK[G[_]](fk: F ~> G): SelfAwareStructuredLogger[G] = @@ -94,6 +96,90 @@ object SelfAwareStructuredLogger { sl.trace(modify(ctx), t)(message) } + def withContextFromKleisli[F[_]: Monad]( + sl: SelfAwareStructuredLogger[Kleisli[F, Map[String, String], *]] + ): SelfAwareStructuredLogger[Kleisli[F, Map[String, String], *]] = + withContextF(sl)( + Kleisli.ask[F, Map[String, String]] + ) + def withContextF[F[_]: FlatMap]( + sl: SelfAwareStructuredLogger[F] + )(ctx: F[Map[String, String]]): SelfAwareStructuredLogger[F] = + new ModifiedContextFSelfAwareStructuredLogger[F](sl)(existingCtx => ctx.map(_ ++ existingCtx)) + + private class ModifiedContextFSelfAwareStructuredLogger[F[_]: FlatMap]( + sl: SelfAwareStructuredLogger[F] + )( + modify: Map[String, String] => F[Map[String, String]] + ) extends SelfAwareStructuredLogger[F] { + private lazy val defaultCtx: F[Map[String, String]] = modify(Map.empty) + + def error(message: => String): F[Unit] = defaultCtx.flatMap(sl.error(_)(message)) + + def warn(message: => String): F[Unit] = defaultCtx.flatMap(sl.warn(_)(message)) + + def info(message: => String): F[Unit] = defaultCtx.flatMap(sl.info(_)(message)) + + def debug(message: => String): F[Unit] = defaultCtx.flatMap(sl.debug(_)(message)) + + def trace(message: => String): F[Unit] = defaultCtx.flatMap(sl.trace(_)(message)) + + def trace(ctx: Map[String, String])(msg: => String): F[Unit] = + modify(ctx).flatMap(sl.trace(_)(msg)) + + def debug(ctx: Map[String, String])(msg: => String): F[Unit] = + modify(ctx).flatMap(sl.debug(_)(msg)) + + def info(ctx: Map[String, String])(msg: => String): F[Unit] = + modify(ctx).flatMap(sl.info(_)(msg)) + + def warn(ctx: Map[String, String])(msg: => String): F[Unit] = + modify(ctx).flatMap(sl.warn(_)(msg)) + + def error(ctx: Map[String, String])(msg: => String): F[Unit] = + modify(ctx).flatMap(sl.error(_)(msg)) + + def isTraceEnabled: F[Boolean] = sl.isTraceEnabled + + def isDebugEnabled: F[Boolean] = sl.isDebugEnabled + + def isInfoEnabled: F[Boolean] = sl.isInfoEnabled + + def isWarnEnabled: F[Boolean] = sl.isWarnEnabled + + def isErrorEnabled: F[Boolean] = sl.isErrorEnabled + + def error(t: Throwable)(message: => String): F[Unit] = + defaultCtx.flatMap(sl.error(_, t)(message)) + + def warn(t: Throwable)(message: => String): F[Unit] = + defaultCtx.flatMap(sl.warn(_, t)(message)) + + def info(t: Throwable)(message: => String): F[Unit] = + defaultCtx.flatMap(sl.info(_, t)(message)) + + def debug(t: Throwable)(message: => String): F[Unit] = + defaultCtx.flatMap(sl.debug(_, t)(message)) + + def trace(t: Throwable)(message: => String): F[Unit] = + defaultCtx.flatMap(sl.trace(_, t)(message)) + + def error(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + modify(ctx).flatMap(sl.error(_, t)(message)) + + def warn(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + modify(ctx).flatMap(sl.warn(_, t)(message)) + + def info(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + modify(ctx).flatMap(sl.info(_, t)(message)) + + def debug(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + modify(ctx).flatMap(sl.debug(_, t)(message)) + + def trace(ctx: Map[String, String], t: Throwable)(message: => String): F[Unit] = + modify(ctx).flatMap(sl.trace(_, t)(message)) + } + private def withModifiedString[F[_]]( l: SelfAwareStructuredLogger[F], f: String => String