diff --git a/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala b/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala index 19d4477..4dfb70f 100644 --- a/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala +++ b/core/src/main/scala/com/evolutiongaming/catshelper/Log.scala @@ -11,6 +11,17 @@ import org.slf4j.{Logger, MDC} import scala.annotation.tailrec import scala.collection.immutable.SortedMap +/** Named/prefixed logger instance. + * + * Use [[LogOf]] to create the new instances of the class. + * + * Avoid passing `Log` instances implicitly. Passing implicit instances around could "leak" a logger + * into an unrelated code, and create a confusion with wrongly attributed log messages. + * + * @see [[LogOf]] for usage examples. + * @see [[https://slf4j.org/api/org/slf4j/Logger.html Logger]] for a typical + * underlying implementation. + */ trait Log[F[_]] { @inline def trace(msg: => String): F[Unit] = trace(msg, mdc = Log.Mdc.empty) diff --git a/core/src/main/scala/com/evolutiongaming/catshelper/LogOf.scala b/core/src/main/scala/com/evolutiongaming/catshelper/LogOf.scala index e3b91d7..aa676de 100644 --- a/core/src/main/scala/com/evolutiongaming/catshelper/LogOf.scala +++ b/core/src/main/scala/com/evolutiongaming/catshelper/LogOf.scala @@ -8,6 +8,110 @@ import org.slf4j.{ILoggerFactory, LoggerFactory} import scala.reflect.ClassTag +/** Factory of [[Log]] instances. + * + * There are, currently, two ways of using the class and it is recommended to + * select one, describe it in project coding guidelines, and stick with it + * to avoid a confusion for the newcomers. Both ways are described below. + * + * =Traditional approach= + * + * The intented usage is to have a single instance of `LogOf` in the + * application and use it to create `Log` instances for each class. + * + * The following could be written somewhere in the application initialization + * code such as [[cats.effect.IOApp]] instance: + * {{{ + * implicit val logOf = LogOf.slf4j[F] + * }}} + * + * Then a typical example would be: + * {{{ + * object UserService { + * + * def of[F[_]: LogOf]: F[UserService[F]] = + * for { + * log <- LogOf[F].forClass[UserService] + * service = new UserService[F](log) + * } yield service + * + * } + * + * class UserService[F[_]: Monad](log: Log[F]) { + * + * def create(user: User): F[Unit] = + * for { + * _ <- log.info(s"Creating user...") + * _ <- ... + * } yield () + * + * } + * }}} + * + * The expected output, depending on the logging configuration, would be: + * {{{ + * [2025-01-13T12:24:23.0Z] INFO UserService - Creating user... + * }}} + * + * The main advantage of such approach is a minimal boilerplate when passing + * `LogOf` around, and an ability to reuse `LogOf` and `Log` instances, + * decreasing the performance overhead. + * + * =MDC-style approach= + * + * The alternative way to use `LogOf` is to create a child instance each time + * the additional information should be passed to the underlying call, making + * it a poor's man MDC. The additonial information is then to be passed using + * `withField` method on `LogOf` itself. + * + * If `LogOf` is used in such a way, it should not be passed implicitly, to + * avoid accidential mix up of the `LogOf` instances. + * + * For sake of uniformity, if this approach is used, the recommended practice + * is to never pass `Log` instance anywhere at all, creating a new one each + * time it is required. Saying that, it is acceptable to reuse them for + * performance or brevity reasons. + * + * A typical example of such usage could be following: + * {{{ + * class HandleService[F[_]: Monad] { + * + * def create(logOf: LogOf[F]): F[String] = + * for { + * log <- logOf.forClass[HandleService] + * _ <- log.info(s"Creating handle...") + * _ <- ... + * } yield () + * + * } + * + * class UserService[F[_]: Monad](handleService: HandleService[F]) { + * + * def create(user: User, logOf: LogOf[F]): F[Unit] = + * for { + * log <- logOf.forClass[UserService] + * _ <- log.info(s"Creating user...") + * handle <- handleService.create(logOf.withField("user", user.id)) + * _ <- ... + * } yield () + * + * } + * + * }}} + * + * The expected output, depending on the logging configuration, would be: + * {{{ + * [2025-01-13T12:24:23.0Z] INFO UserService - Creating user... + * [2025-01-13T12:24:23.0Z] INFO HandleService user=n32fkj43asxa45ak - Creating handle... + * }}} + * + * This approach introduces a slight overhead, because it requires creating + * both `LogOf` and `Log` instances on each call, but allows passing an + * additional context to the called methods. + * + * @see [[https://slf4j.org/api/org/slf4j/LoggerFactory.html LoggerFactory]] + * for a typical underlying implementation. + */ trait LogOf[F[_]] { def apply(source: String): F[Log[F]]