From 82dea75de38635834fb9e98b550bb9b63ad4c877 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Thu, 26 Oct 2023 12:15:56 +0300 Subject: [PATCH 1/2] Allow adding effect's contextual environment to the log context --- .github/workflows/ci.yml | 4 +- build.sbt | 14 +- .../typelevel/log4cats/mtl/Contextual.scala | 72 ++++++++++ .../mtl/ContextualLoggerFactory.scala | 51 +++++++ .../ContextualSelfAwareStructuredLogger.scala | 124 ++++++++++++++++++ .../mtl/ContextualStructuredLogger.scala | 116 ++++++++++++++++ .../typelevel/log4cats/mtl/ToContext.scala | 40 ++++++ .../log4cats/mtl/syntax/package.scala | 108 +++++++++++++++ .../ContextualStructuredLoggerSuite.scala | 101 ++++++++++++++ 9 files changed, 626 insertions(+), 4 deletions(-) create mode 100644 mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/Contextual.scala create mode 100644 mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualLoggerFactory.scala create mode 100644 mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualSelfAwareStructuredLogger.scala create mode 100644 mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualStructuredLogger.scala create mode 100644 mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ToContext.scala create mode 100644 mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/syntax/package.scala create mode 100644 testing/shared/src/test/scala/org/typelevel/log4cats/ContextualStructuredLoggerSuite.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8af1cf8b..34807381 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,11 +102,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p testing/jvm/target noop/jvm/target core/native/target testing/native/target noop/native/target core/js/target js-console/target testing/js/target noop/js/target core/jvm/target slf4j/target project/target + run: mkdir -p mtl/native/target testing/jvm/target noop/jvm/target core/native/target testing/native/target noop/native/target core/js/target js-console/target testing/js/target noop/js/target core/jvm/target mtl/js/target mtl/jvm/target slf4j/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar testing/jvm/target noop/jvm/target core/native/target testing/native/target noop/native/target core/js/target js-console/target testing/js/target noop/js/target core/jvm/target slf4j/target project/target + run: tar cf targets.tar mtl/native/target testing/jvm/target noop/jvm/target core/native/target testing/native/target noop/native/target core/js/target js-console/target testing/js/target noop/js/target core/jvm/target mtl/js/target mtl/jvm/target slf4j/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') diff --git a/build.sbt b/build.sbt index d4f11946..f5f322b4 100644 --- a/build.sbt +++ b/build.sbt @@ -28,13 +28,14 @@ ThisBuild / tlVersionIntroduced := Map("3" -> "2.1.1") val catsV = "2.10.0" val catsEffectV = "3.5.2" +val catsMtlV = "1.4.0" val slf4jV = "1.7.36" val munitCatsEffectV = "2.0.0-M3" val logbackClassicV = "1.2.12" Global / onChangedBuildSource := ReloadOnSourceChanges -lazy val root = tlCrossRootProject.aggregate(core, testing, noop, slf4j, docs, `js-console`) +lazy val root = tlCrossRootProject.aggregate(core, mtl, testing, noop, slf4j, docs, `js-console`) lazy val docs = project .in(file("site")) @@ -56,9 +57,18 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) ) .nativeSettings(commonNativeSettings) -lazy val testing = crossProject(JSPlatform, JVMPlatform, NativePlatform) +lazy val mtl = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings(commonSettings) .dependsOn(core) + .settings( + name := "log4cats-mtl", + libraryDependencies += "org.typelevel" %%% "cats-mtl" % catsMtlV + ) + .nativeSettings(commonNativeSettings) + +lazy val testing = crossProject(JSPlatform, JVMPlatform, NativePlatform) + .settings(commonSettings) + .dependsOn(core, mtl % Test) .settings( name := "log4cats-testing", libraryDependencies ++= Seq( diff --git a/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/Contextual.scala b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/Contextual.scala new file mode 100644 index 00000000..163e9e90 --- /dev/null +++ b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/Contextual.scala @@ -0,0 +1,72 @@ +/* + * 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.mtl + +import cats.Applicative +import cats.mtl.Ask + +/** + * Represents the ability to access a contextual environment as a map of key-value pairs. + * + * @example + * {{{ + * case class LogContext(logId: String) + * + * // how to transform the contextual environment into the log context + * implicit val toLogContext: ToContext[LogContext] = + * ctx => Map("log_id" -> ctx.logId) + * + * // available out of the box for Kleisli, Reader, etc + * implicit val askLogContext: Ask[F, LogContext] = ??? + * + * implicit val contextual: Contextual[F] = Contextual.fromAsk + * }}} + * + * @tparam F + * the higher-kinded type of a polymorphic effect + */ +@scala.annotation.implicitNotFound(""" +Couldn't find `Contextual` for type `${F}`. Make sure you have the following implicit instances: +1) `org.typelevel.log4cats.mtl.ToContext[Ctx]` +2) `cats.mtl.Ask[${F}, Ctx]` +""") +trait Contextual[F[_]] { + + /** + * Retrieves the current contextual environment as a map of key-value pairs. + */ + def current: F[Map[String, String]] +} + +object Contextual { + + def apply[F[_]](implicit ev: Contextual[F]): Contextual[F] = ev + + /** + * Creates a [[Contextual]] instance that always returns the given `ctx`. + */ + def const[F[_]: Applicative](ctx: Map[String, String]): Contextual[F] = + new Contextual[F] { + val current: F[Map[String, String]] = Applicative[F].pure(ctx) + } + + implicit def fromAsk[F[_], A: ToContext](implicit A: Ask[F, A]): Contextual[F] = + new Contextual[F] { + def current: F[Map[String, String]] = A.reader(ToContext[A].extract) + } + +} diff --git a/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualLoggerFactory.scala b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualLoggerFactory.scala new file mode 100644 index 00000000..5f2edaef --- /dev/null +++ b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualLoggerFactory.scala @@ -0,0 +1,51 @@ +/* + * 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 +package mtl + +import cats.FlatMap +import cats.syntax.functor._ +import org.typelevel.log4cats.mtl.syntax._ + +object ContextualLoggerFactory { + + /** + * Creates a new [[LoggerFactory]] that returns [[SelfAwareStructuredLogger]] that adds + * information captured by `Contextual` to the context. + * + * @example + * {{{ + * case class LogContext(logId: String) + * + * implicit val toLogContext: ToContext[LogContext] = + * ctx => Map("log_id" -> ctx.logId) + * + * implicit val askLogContext: Ask[F, LogContext] = ??? + * + * val loggerFactory: LoggerFactory[F] = ??? // the general factory, e.g. Slf4jFactory + * val contextual: LoggerFactory[F] = ContextualLoggerFactory(loggerFactory) + * }}} + */ + def apply[F[_]: Contextual: FlatMap](factory: LoggerFactory[F]): LoggerFactory[F] = + new LoggerFactory[F] { + def getLoggerFromName(name: String): SelfAwareStructuredLogger[F] = + factory.getLoggerFromName(name).contextual + + def fromName(name: String): F[SelfAwareStructuredLogger[F]] = + factory.fromName(name).map(logger => logger.contextual) + } +} diff --git a/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualSelfAwareStructuredLogger.scala b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualSelfAwareStructuredLogger.scala new file mode 100644 index 00000000..1a688d22 --- /dev/null +++ b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualSelfAwareStructuredLogger.scala @@ -0,0 +1,124 @@ +/* + * 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 +package mtl + +import cats.FlatMap +import cats.syntax.flatMap._ +import cats.syntax.functor._ + +private[mtl] class ContextualSelfAwareStructuredLogger[F[_]: Contextual: FlatMap]( + sl: SelfAwareStructuredLogger[F] +) extends SelfAwareStructuredLogger[F] { + + private val defaultCtx: F[Map[String, String]] = Contextual[F].current + + private def modify(ctx: Map[String, String]): F[Map[String, String]] = + defaultCtx.map(c => c ++ ctx) + + 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(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 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 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 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)) +} + +object ContextualSelfAwareStructuredLogger { + + /** + * Creates a new [[SelfAwareStructuredLogger]] that adds information captured by `Contextual` to + * the context. + * + * @example + * {{{ + * case class LogContext(logId: String) + * + * implicit val toLogContext: ToContext[LogContext] = + * ctx => Map("log_id" -> ctx.logId) + * + * implicit val askLogContext: Ask[F, LogContext] = ??? + * + * val logger: SelfAwareStructuredLogger[F] = ??? // the general logger, e.g. Slf4jLogger + * val contextual: SelfAwareStructuredLogger[F] = ContextualSelfAwareStructuredLogger(logger) + * }}} + */ + def apply[F[_]: Contextual: FlatMap]( + logger: SelfAwareStructuredLogger[F] + ): SelfAwareStructuredLogger[F] = + new ContextualSelfAwareStructuredLogger[F](logger) + +} diff --git a/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualStructuredLogger.scala b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualStructuredLogger.scala new file mode 100644 index 00000000..3bb0abc3 --- /dev/null +++ b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ContextualStructuredLogger.scala @@ -0,0 +1,116 @@ +/* + * 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 +package mtl + +import cats.FlatMap +import cats.syntax.flatMap._ +import cats.syntax.functor._ + +private[mtl] class ContextualStructuredLogger[F[_]: Contextual: FlatMap]( + sl: StructuredLogger[F] +) extends StructuredLogger[F] { + + private val defaultCtx: F[Map[String, String]] = Contextual[F].current + + private def modify(ctx: Map[String, String]): F[Map[String, String]] = + defaultCtx.map(c => c ++ ctx) + + 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 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 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 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)) +} + +object ContextualStructuredLogger { + + /** + * Creates a new [[StructuredLogger]] that adds information captured by `Contextual` to the + * context. + * + * @example + * {{{ + * case class LogContext(logId: String) + * + * implicit val toLogContext: ToContext[LogContext] = + * ctx => Map("log_id" -> ctx.logId) + * + * implicit val askLogContext: Ask[F, LogContext] = ??? + * + * val logger: StructuredLogger[F] = ??? // the general logger, e.g. Slf4jLogger + * val contextual: StructuredLogger[F] = ContextualStructureLogger(logger) + * }}} + */ + def apply[F[_]: Contextual: FlatMap](logger: StructuredLogger[F]): StructuredLogger[F] = + new ContextualStructuredLogger[F](logger) + +} diff --git a/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ToContext.scala b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ToContext.scala new file mode 100644 index 00000000..a4611224 --- /dev/null +++ b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/ToContext.scala @@ -0,0 +1,40 @@ +/* + * 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.mtl + +/** + * Creates context information from the value `A`. + * + * @example + * {{{ + * case class LogContext(logId: String) + * + * implicit val toLogContext: ToContext[LogContext] = + * ctx => Map("log_id" -> ctx.logId) + * }}} + */ +trait ToContext[A] { + def extract(a: A): Map[String, String] +} + +object ToContext { + def apply[A](implicit ev: ToContext[A]): ToContext[A] = ev + + implicit val toContextFromMap: ToContext[Map[String, String]] = + (a: Map[String, String]) => a + +} diff --git a/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/syntax/package.scala b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/syntax/package.scala new file mode 100644 index 00000000..bd12b6a1 --- /dev/null +++ b/mtl/shared/src/main/scala/org/typelevel/log4cats/mtl/syntax/package.scala @@ -0,0 +1,108 @@ +/* + * 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 +package mtl + +import cats.FlatMap + +package object syntax { + + implicit final class LoggerFactorySyntax[F[_]]( + private val factory: LoggerFactory[F] + ) extends AnyVal { + + /** + * Creates a new [[LoggerFactory]] that returns [[SelfAwareStructuredLogger]] that adds + * information captured by `Contextual` to the context. + * + * @example + * {{{ + * import org.typelevel.log4cats.mtl.syntax._ + * + * case class LogContext(logId: String) + * + * implicit val toLogContext: ToContext[LogContext] = + * ctx => Map("log_id" -> ctx.logId) + * + * implicit val askLogContext: Ask[F, LogContext] = ??? + * + * val loggerFactory: LoggerFactory[F] = ??? // the general factory, e.g. Slf4jFactory + * val contextual: LoggerFactory[F] = loggerFactory.contextual + * }}} + */ + def contextual(implicit C: Contextual[F], F: FlatMap[F]): LoggerFactory[F] = + ContextualLoggerFactory(factory) + + } + + implicit final class SelfAwareStructuredLoggerSyntax[F[_]]( + private val logger: SelfAwareStructuredLogger[F] + ) extends AnyVal { + + /** + * Creates a new [[SelfAwareStructuredLogger]] that adds information captured by `Contextual` to + * the context. + * + * @example + * {{{ + * import org.typelevel.log4cats.mtl.syntax._ + * + * case class LogContext(logId: String) + * + * implicit val toLogContext: ToContext[LogContext] = + * ctx => Map("log_id" -> ctx.logId) + * + * implicit val askLogContext: Ask[F, LogContext] = ??? + * + * val logger: SelfAwareStructuredLogger[F] = ??? // the general logger, e.g. Slf4jLogger + * val contextual: SelfAwareStructuredLogger[F] = logger.contextual + * }}} + */ + def contextual(implicit C: Contextual[F], F: FlatMap[F]): SelfAwareStructuredLogger[F] = + ContextualSelfAwareStructuredLogger[F](logger) + + } + + implicit final class StructuredLoggerSyntax[F[_]]( + private val logger: StructuredLogger[F] + ) extends AnyVal { + + /** + * Creates a new [[StructuredLogger]] that adds information captured by `Contextual` to the + * context. + * + * @example + * {{{ + * import org.typelevel.log4cats.mtl.syntax._ + * + * case class LogContext(logId: String) + * + * implicit val toLogContext: ToContext[LogContext] = + * ctx => Map("log_id" -> ctx.logId) + * + * implicit val askLogContext: Ask[F, LogContext] = ??? + * + * val logger: StructuredLogger[F] = ??? // the general logger, e.g. Slf4jLogger + * val contextual: StructuredLogger[F] = logger.contextual + * }}} + */ + def contextual(implicit C: Contextual[F], F: FlatMap[F]): StructuredLogger[F] = + ContextualStructuredLogger[F](logger) + + } + +} diff --git a/testing/shared/src/test/scala/org/typelevel/log4cats/ContextualStructuredLoggerSuite.scala b/testing/shared/src/test/scala/org/typelevel/log4cats/ContextualStructuredLoggerSuite.scala new file mode 100644 index 00000000..381bc750 --- /dev/null +++ b/testing/shared/src/test/scala/org/typelevel/log4cats/ContextualStructuredLoggerSuite.scala @@ -0,0 +1,101 @@ +/* + * 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.arrow.FunctionK +import cats.data.{Kleisli, ReaderT} +import cats.effect._ +import cats.mtl.Local +import cats.mtl.syntax.local._ +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import cats.{~>, FlatMap} +import munit._ +import org.typelevel.log4cats.mtl.ToContext +import org.typelevel.log4cats.mtl.syntax._ +import org.typelevel.log4cats.testing.StructuredTestingLogger + +class ContextualStructuredLoggerSuite extends CatsEffectSuite { + import StructuredTestingLogger.{ERROR, INFO} + + test("Kleisli - capture contextual information") { + test[IO, Kleisli[IO, LogContext, *]](Kleisli.liftK).run(LogContext("external")) + } + + test("ReaderT - capture contextual information") { + test[IO, ReaderT[IO, LogContext, *]](ReaderT.liftK).run(LogContext("external")) + } + + test("IOLocal - capture contextual information") { + IOLocal(LogContext("external")).flatMap { implicit ioLocal => + test[IO, IO](FunctionK.id) + } + } + + private def test[F[_]: Sync, G[_]: FlatMap]( + fg: F ~> G + )(implicit local: Local[G, LogContext]): G[Unit] = { + val logger: StructuredTestingLogger[F] = + StructuredTestingLogger.impl() + + val contextual: StructuredLogger[G] = + logger.mapK(fg).contextual + + val io: G[Unit] = + for { + _ <- contextual.info("simple message") + _ <- contextual.info(Map("a" -> "b"))("with context") + _ <- contextual.info(Map("c" -> "d"))("inner with context").scope(LogContext("inner")) + _ <- contextual.error("the error one") + } yield () + + val expected = Vector( + INFO("simple message", None, Map("log_id" -> "global")), + INFO("with context", None, Map("log_id" -> "global", "a" -> "b")), + INFO("inner with context", None, Map("log_id" -> "inner", "c" -> "d")), + ERROR("the error one", None, Map("log_id" -> "global")) + ) + + for { + _ <- io.scope(LogContext("global")) + logged <- fg(logger.logged) + } yield assertEquals(logged, expected) + } + + case class LogContext(logId: String) + + implicit val toLogContext: ToContext[LogContext] = + ctx => Map("log_id" -> ctx.logId) + + // We hope this instance is moved into Cats Effect. + // copy-pasted from otel4s + implicit def localForIoLocal[F[_]: MonadCancelThrow: LiftIO, E](implicit + ioLocal: IOLocal[E] + ): Local[F, E] = + new Local[F, E] { + def applicative = + MonadCancelThrow[F] + + def ask[E2 >: E] = + MonadCancelThrow[F].widen[E, E2](ioLocal.get.to[F]) + + def local[A](fa: F[A])(f: E => E): F[A] = + MonadCancelThrow[F] + .bracket(ioLocal.modify(e => (f(e), e)).to[F])(_ => fa)(ioLocal.set(_).to[F]) + } + +} From 87b0ac57ed51596ce581380be418f80fd809bfd5 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Mon, 30 Oct 2023 13:23:20 +0200 Subject: [PATCH 2/2] Remove `tlVersionIntroduced` from `mtl` module --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index f5f322b4..d6982db8 100644 --- a/build.sbt +++ b/build.sbt @@ -64,7 +64,6 @@ lazy val mtl = crossProject(JSPlatform, JVMPlatform, NativePlatform) name := "log4cats-mtl", libraryDependencies += "org.typelevel" %%% "cats-mtl" % catsMtlV ) - .nativeSettings(commonNativeSettings) lazy val testing = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings(commonSettings)