Getting Started
Project | Maven Central |
---|---|
logger-f-cats | |
logger-f-slf4j | |
logger-f-log4j | |
logger-f-log4s | |
logger-f-sbt-logging |
- Supported Scala Versions:
3
,2.13
and2.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
If you use logback, please use this.
- sbt
- sbt (with libraryDependencies)
- scala-cli
In build.sbt
,
"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-slf4j" % "2.0.0",
In build.sbt
,
libraryDependencies ++= Seq(
"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-slf4j" % "2.0.0",
)
//> using dep "io.kevinlee::logger-f-cats:2.0.0"
//> using dep "io.kevinlee::logger-f-slf4j:2.0.0"
With Log4j
- sbt
- sbt (with libraryDependencies)
- scala-cli
In build.sbt
,
"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-log4j" % "2.0.0",
In build.sbt
,
libraryDependencies ++= Seq(
"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-log4j" % "2.0.0",
)
//> using dep "io.kevinlee::logger-f-cats:2.0.0"
//> using dep "io.kevinlee::logger-f-log4j:2.0.0"
With Log4s
- sbt
- sbt (with libraryDependencies)
- scala-cli
In build.sbt
,
"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-log4s" % "2.0.0",
In build.sbt
,
libraryDependencies ++= Seq(
"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-log4s" % "2.0.0",
)
//> using dep "io.kevinlee::logger-f-cats:2.0.0"
//> using dep "io.kevinlee::logger-f-log4s:2.0.0"
With sbt Logging Util
For sbt plugin development,
- sbt
- sbt (with libraryDependencies)
- scala-cli
In build.sbt
,
"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-sbt-logging" % "2.0.0",
In build.sbt
,
libraryDependencies ++= Seq(
"io.kevinlee" %% "logger-f-cats" % "2.0.0",
"io.kevinlee" %% "logger-f-sbt-logging" % "2.0.0",
)
//> using dep "io.kevinlee::logger-f-cats:2.0.0"
//> using dep "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