Skip to main content
Version: 2.0.0

Getting Started

Build Status Release Status Latest version

ProjectMaven Central
logger-f-catsMaven Central
logger-f-slf4jMaven Central
logger-f-log4jMaven Central
logger-f-log4sMaven Central
logger-f-sbt-loggingMaven Central
  • Supported Scala Versions: 3, 2.13 and 2.12

LoggerF - Logger for F[_]

LoggerF is a tool for logging tagless final with an effect library. LoggerF requires Effectie to construct F[_]. All the example code in this doc site uses Effectie so if you're not familiar with it, please check out Effectie website.

Why LoggerF? Why not just log with map or flatMap? Please read "Why?" section.

Getting Started

Get LoggerF For Cats Effect

Get LoggerF For Cats

logger-f can be used wit any effect library or Future as long as there is an instance of Fx from effectie. Effectie provides instances of Fx for Cats Effect 2 and 3, and Monix 3.

With SLF4J

info

If you use logback, please use this.

In build.sbt,

"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-slf4j" % "2.0.0",

With Log4j

In build.sbt,

"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-log4j" % "2.0.0",

With Log4s

In build.sbt,

"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-log4s" % "2.0.0",

With sbt Logging Util

For sbt plugin development,

In build.sbt,

"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-sbt-logging" % "2.0.0",

Why

Log without LoggerF

If you code tagless final and use some effect library like Cats Effect and Monix or use Future, you may have inconvenience in logging.

What inconvenience? I can just log with flatMap like.

for {
a <- foo(n) // F[A]
_ <- Sync[F].delay(logger.debug(s"a is $a")) // F[Unit]
b <- bar(a) // F[B]
_ <- Sync[F].delay(logger.debug(s"b is $b")) // F[Unit]
} yield b

That's true, but it's distracting to have log in each flatMap. So,

1 line for the actual code
1 line for logging
1 line for the actual code
1 line for logging

Log with LoggerF

It can be simplified by logger-f.

for {
a <- foo(n).log(a => debug(s"a is $a")) // F[A]
b <- bar(a).log(b => debug(s"b is $b")) // F[B]
} yield b

Log without LoggerF (Option and OptionT)

What about F[_] with Option and Either? What happens if you want to use Option or Either? If you use F[_] with Option or Either, you may have more inconvenience or may not get the result you want.

e.g.)

import cats.syntax.all._
import cats.effect._

import org.slf4j.LoggerFactory
val logger = LoggerFactory.getLogger("test-logger")
// logger: org.slf4j.Logger = Logger[test-logger]

def foo[F[_]: Sync](n: Int): F[Option[Int]] = for {
a <- Sync[F].pure(n.some)
_ <- Sync[F].delay(
a match {
case Some(value) =>
logger.debug(s"a is $value")
case None =>
logger.debug("No 'a' value found")
}
) // F[Unit]
b <- Sync[F].pure(none[Int])
_ <- Sync[F].delay(
b match {
case Some(value) =>
logger.debug(s"b is $value")
case None =>
() // don't log anything for None case
}
) // F[Unit]
c <- Sync[F].pure(123.some)
_ <- Sync[F].delay(
c match {
case Some(value) =>
() // don't log anything for None case
case None =>
logger.debug("No 'c' value found")
}
) // F[Unit]
} yield c

So much noise for logging!

Now, let's think about the result.

foo[IO](1).unsafeRunSync() // You probably want to have None here.
// res1: Option[Int] = Some(value = 123)

You expect None for the result due to Sync[F].pure(none[Int]) yet you get Some(123) instead. That's because b is from F[Option[Int]] not from Option[Int].

The same issue exists for F[Either[A, B]] as well.

So you need to use OptionT for F[Option[A]] and EitherT for F[Either[A, B]].

Let's write it again with OptionT.

import cats.data._
import cats.syntax.all._
import cats.effect._

import org.slf4j.LoggerFactory
val logger = LoggerFactory.getLogger("test-logger")
// logger: org.slf4j.Logger = Logger[test-logger]

def foo[F[_]: Sync](n: Int): F[Option[Int]] = (for {
a <- OptionT(Sync[F].pure(n.some))
_ <- OptionT.liftF(Sync[F].delay(logger.debug(s"a is $a"))) // Now, you can't log None case.
b <- OptionT(Sync[F].pure(none[Int]))
_ <- OptionT.liftF(Sync[F].delay(logger.debug(s"b is $b"))) // You can't log None case.
c <- OptionT(Sync[F].pure(123.some))
_ <- OptionT.liftF(Sync[F].delay(logger.debug(s"c is $c"))) // You can't log None case.
} yield c).value
foo[IO](1).unsafeRunSync() // You expect None here.
// res3: Option[Int] = None

The problem's gone! Now each flatMap handles only Some case and that's what you want. However, because of that, it's hard to log None case.


Log with LoggerF (Option and OptionT)

LoggerF can solve this issue for you!

import cats._
import cats.data._
import cats.syntax.all._
import cats.effect._

import effectie.core._
import effectie.syntax.all._

import loggerf.core._
import loggerf.syntax.all._

def foo[F[_]: Fx: Monad: Log](n: Int): F[Option[Int]] =
(for {
a <- OptionT(effectOf(n.some)).log(
ifEmpty = error("a is empty"),
a => debug(s"a is $a")
)
b <- OptionT(effectOf(none[Int])).log(
error("b is empty"),
b => debug(s"b is $b")
)
c <- OptionT(effectOf(123.some)).log(
warn("c is empty"),
c => debug(s"c is $c")
)
} yield c).value
import loggerf.logger._

// or Slf4JLogger.slf4JLogger[MyClass]
implicit val canLog: CanLog = Slf4JLogger.slf4JCanLog("MyLogger")
// canLog: CanLog = loggerf.logger.Slf4JLogger@5599b7b3

import effectie.instances.ce2.fx._
import loggerf.instances.cats._

foo[IO](1).unsafeRunSync() // You expect None here.
// res5: Option[Int] = None

With logs like

00:17:33.983 [main] DEBUG MyLogger - a is 1
00:17:33.995 [main] ERROR MyLogger - b is empty

Log with LoggerF (EitherT)

Another example with EitherT (F[Either[A, B]] case is similar),

import cats._
import cats.data._
import cats.syntax.all._
import cats.effect._

import effectie.core._
import effectie.syntax.all._

import loggerf.core._
import loggerf.syntax.all._

def foo[F[_]: Fx: Monad: Log](n: Int): F[Either[String, Int]] =
(for {
a <- EitherT(effectOf(n.asRight[String])).log(
err => error(s"Error: $err"),
a => debug(s"a is $a")
)
b <- EitherT(effectOf("Some Error".asLeft[Int])).log(
err => error(s"Error: $err"),
b => debug(s"b is $b")
)
c <- EitherT(effectOf(123.asRight[String])).log(
err => warn(s"Error: $err"),
c => debug(s"c is $c")
)
} yield c).value
import loggerf.logger._

// or Slf4JLogger.slf4JLogger[MyClass]
implicit val canLog: CanLog = Slf4JLogger.slf4JCanLog("MyLogger")
// canLog: CanLog = loggerf.logger.Slf4JLogger@3c9db70d

import effectie.instances.ce2.fx._
import loggerf.instances.cats._

foo[IO](1).unsafeRunSync() // You expect Left("Some Error") here.
// res7: Either[String, Int] = Left(value = "Some Error")

With logs like

00:40:48.663 [main] DEBUG MyLogger - a is 1
00:40:48.667 [main] ERROR MyLogger - Error: Some Error

Usage

Please check out