Skip to content

Commit 50265ab

Browse files
Add log4cats integration
1 parent ab1a96f commit 50265ab

File tree

5 files changed

+322
-0
lines changed

5 files changed

+322
-0
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A **pure** _(in both senses of the word!)_ **Scala 3** logging library with **no
1515
- [Can I use `SLF4J`?](#can-i-use-slf4j)
1616
- [Limitations of SLF4J bindings](#limitations-of-slf4j-bindings)
1717
- [Can I use `http4s`?](#can-i-use-http4s)
18+
- [Can I use `log4cats`?](#can-i-use-log4cats)
1819
- [Structured Logging](#structured-logging)
1920

2021
## Highlights
@@ -228,6 +229,44 @@ mainHttp4s.unsafeRunSync()
228229
// 2023-03-13 09:00:43 [INFO ] repl.MdocSession$.MdocApp: Got response headers: Headers(X-Trace-Id: 33a38390-647a-4876-9a05-7898a8f4db44) (README.md:147)
229230
```
230231

232+
## Can I use `log4cats`?
233+
Yes, you can. Create a Woof `Logger[F]` instance, and wrap it into Log4Cats' `LoggerFactory[F]`:
234+
```scala mdoc
235+
import cats.effect.IO
236+
237+
import org.legogroup.woof.ColorPrinter
238+
import org.legogroup.woof.DefaultLogger
239+
import org.legogroup.woof.Filter
240+
import org.legogroup.woof.log4cats.WoofFactory
241+
import org.legogroup.woof.Output
242+
import org.legogroup.woof.Printer
243+
244+
import org.typelevel.log4cats.Logger
245+
import org.typelevel.log4cats.LoggerFactory
246+
import org.typelevel.log4cats.syntax.*
247+
248+
249+
def program(using LoggerFactory[IO]): IO[Unit] =
250+
given Logger[IO] = LoggerFactory[IO].getLogger
251+
252+
for
253+
_ <- error"This is some error from log4cats!"
254+
_ <- warn"This is some warn from log4cats!"
255+
_ <- info"This is some info from log4cats!"
256+
_ <- debug"This is some debug from log4cats!"
257+
_ <- trace"This is some trace from log4cats!"
258+
yield ()
259+
260+
val main: IO[Unit] =
261+
given Filter = Filter.everything
262+
given Printer = ColorPrinter()
263+
264+
for
265+
given LoggerFactory[IO] <- DefaultLogger.makeIo(Output.fromConsole).map(WoofFactory[IO](_))
266+
_ <- program
267+
yield ()
268+
```
269+
231270
## Structured Logging
232271

233272
Structured logging is useful when your logs are collected and inspected by a monitoring system. Having a well structured log output can save you

build.sbt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ val V = new {
55
val catsEffect = "3.5.4"
66
val circe = "0.14.8"
77
val http4s = "0.23.28"
8+
val log4cats = "2.7.0"
89
val munit = "1.0.0-M11"
910
val munitCatsEffect = "2.0.0"
1011
val scala = "3.3.4"
@@ -21,6 +22,7 @@ val D = new {
2122
val catsEffect = Def.setting("org.typelevel" %%% "cats-effect" % V.catsEffect)
2223
val catsEffectTestKit = Def.setting("org.typelevel" %%% "cats-effect-testkit" % V.catsEffect)
2324
val http4s = Def.setting("org.http4s" %%% "http4s-core" % V.http4s)
25+
val log4cats = Def.setting("org.typelevel" %%% "log4cats-core" % V.log4cats)
2426
val munit = Def.setting("org.scalameta" %%% "munit" % V.munit)
2527
val munitCatsEffect = Def.setting("org.typelevel" %%% "munit-cats-effect" % V.munitCatsEffect)
2628
val munitScalacheck = Def.setting("org.scalameta" %%% "munit-scalacheck" % V.munit)
@@ -84,6 +86,7 @@ lazy val root =
8486
slf4j,
8587
slf4j2,
8688
slf4jCommon,
89+
log4cats,
8790
).flatMap(_.componentProjects).map(_.project): _*
8891
)
8992
.settings(
@@ -108,6 +111,17 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
108111
),
109112
)
110113

114+
val log4catsFolder = file("./modules/log4cats")
115+
lazy val log4cats = crossProject(JSPlatform, JVMPlatform, NativePlatform)
116+
.crossType(CrossType.Pure)
117+
.in(log4catsFolder)
118+
.settings(
119+
name := nameForFile(log4catsFolder),
120+
libraryDependencies += D.log4cats.value,
121+
)
122+
.settings(commonSettings)
123+
.dependsOn(core % "compile->compile;test->test") // we also want the test utils
124+
111125
val http4sFolder = file("./modules/http4s")
112126
lazy val http4s = crossProject(JSPlatform, JVMPlatform, NativePlatform)
113127
.crossType(CrossType.Pure)

docs/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A **pure** _(in both senses of the word!)_ **Scala 3** logging library with **no
1515
- [Can I use `SLF4J`?](#can-i-use-slf4j)
1616
- [Limitations of SLF4J bindings](#limitations-of-slf4j-bindings)
1717
- [Can I use `http4s`?](#can-i-use-http4s)
18+
- [Can I use `log4cats`?](#can-i-use-log4cats)
1819
- [Structured Logging](#structured-logging)
1920

2021
## Highlights
@@ -217,6 +218,44 @@ the correlation ID is also returned in the header of the response.
217218
mainHttp4s.unsafeRunSync()
218219
```
219220

221+
## Can I use `log4cats`?
222+
Yes, you can. Create a Woof `Logger[F]` instance, and wrap it into Log4Cats' `LoggerFactory[F]`:
223+
```scala mdoc
224+
import cats.effect.IO
225+
226+
import org.legogroup.woof.ColorPrinter
227+
import org.legogroup.woof.DefaultLogger
228+
import org.legogroup.woof.Filter
229+
import org.legogroup.woof.log4cats.WoofFactory
230+
import org.legogroup.woof.Output
231+
import org.legogroup.woof.Printer
232+
233+
import org.typelevel.log4cats.Logger
234+
import org.typelevel.log4cats.LoggerFactory
235+
import org.typelevel.log4cats.syntax.*
236+
237+
238+
def program(using LoggerFactory[IO]): IO[Unit] =
239+
given Logger[IO] = LoggerFactory[IO].getLogger
240+
241+
for
242+
_ <- error"This is some error from log4cats!"
243+
_ <- warn"This is some warn from log4cats!"
244+
_ <- info"This is some info from log4cats!"
245+
_ <- debug"This is some debug from log4cats!"
246+
_ <- trace"This is some trace from log4cats!"
247+
yield ()
248+
249+
val main: IO[Unit] =
250+
given Filter = Filter.everything
251+
given Printer = ColorPrinter()
252+
253+
for
254+
given LoggerFactory[IO] <- DefaultLogger.makeIo(Output.fromConsole).map(WoofFactory[IO](_))
255+
_ <- program
256+
yield ()
257+
```
258+
220259
## Structured Logging
221260

222261
Structured logging is useful when your logs are collected and inspected by a monitoring system. Having a well structured log output can save you
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.legogroup.woof.log4cats
2+
3+
import cats.Applicative
4+
import org.legogroup.woof
5+
import org.legogroup.woof.Logger.withLogContext
6+
import org.typelevel.log4cats
7+
8+
object WoofFactory:
9+
10+
def apply[F[_]: Applicative](logger: woof.Logger[F]): log4cats.LoggerFactory[F] =
11+
new WoofFactory[F](logger)
12+
13+
end WoofFactory
14+
15+
private class WoofFactory[F[_]: Applicative](logger: woof.Logger[F]) extends log4cats.LoggerFactory[F]:
16+
17+
override def getLoggerFromName(name: String): log4cats.SelfAwareStructuredLogger[F] =
18+
new WoofLog4CatsLogger[F](logger, name)
19+
20+
override def fromName(name: String): F[log4cats.SelfAwareStructuredLogger[F]] =
21+
Applicative[F].pure(getLoggerFromName(name))
22+
23+
end WoofFactory
24+
25+
private class WoofLog4CatsLogger[F[_]: Applicative](logger: woof.Logger[F], name: String)
26+
extends log4cats.SelfAwareStructuredLogger[F]:
27+
28+
private def logInfo(): woof.LogInfo =
29+
val stacktraceElements = (new Throwable).getStackTrace()
30+
val lastIndex = stacktraceElements.reverse.indexWhere(s => s.getClassName == this.getClass.getName)
31+
val callingMethodIndex = stacktraceElements.size - lastIndex
32+
val callingMethod = stacktraceElements(callingMethodIndex)
33+
val fileName =
34+
(callingMethod.getClassName.replace('.', '/') + ".scala").split("\\/").takeRight(1).mkString
35+
val lineNumber = callingMethod.getLineNumber - 1
36+
woof.LogInfo(woof.EnclosingClass(name), fileName, lineNumber)
37+
end logInfo
38+
39+
private def thrMsg(m: String, t: Throwable): String =
40+
(try s"$m ${t.getMessage}"
41+
catch case _ => s"$m ")
42+
43+
override def error(ctx: Map[String, String], t: Throwable)(m: => String): F[Unit] =
44+
logger.error(thrMsg(m, t))(using logInfo()).withLogContext(ctx.toList*)(using logger)
45+
override def warn(ctx: Map[String, String], t: Throwable)(m: => String): F[Unit] =
46+
logger.warn(thrMsg(m, t))(using logInfo()).withLogContext(ctx.toList*)(using logger)
47+
override def info(ctx: Map[String, String], t: Throwable)(m: => String): F[Unit] =
48+
logger.info(thrMsg(m, t))(using logInfo()).withLogContext(ctx.toList*)(using logger)
49+
override def debug(ctx: Map[String, String], t: Throwable)(m: => String): F[Unit] =
50+
logger.debug(thrMsg(m, t))(using logInfo()).withLogContext(ctx.toList*)(using logger)
51+
override def trace(ctx: Map[String, String], t: Throwable)(m: => String): F[Unit] =
52+
logger.trace(thrMsg(m, t))(using logInfo()).withLogContext(ctx.toList*)(using logger)
53+
54+
override def error(t: Throwable)(m: => String): F[Unit] = logger.error(thrMsg(m, t))(using logInfo())
55+
override def warn(t: Throwable)(m: => String): F[Unit] = logger.warn(thrMsg(m, t))(using logInfo())
56+
override def info(t: Throwable)(m: => String): F[Unit] = logger.info(thrMsg(m, t))(using logInfo())
57+
override def debug(t: Throwable)(m: => String): F[Unit] = logger.debug(thrMsg(m, t))(using logInfo())
58+
override def trace(t: Throwable)(m: => String): F[Unit] = logger.trace(thrMsg(m, t))(using logInfo())
59+
60+
override def error(m: => String): F[Unit] = logger.error(m)(using logInfo())
61+
override def warn(m: => String): F[Unit] = logger.warn(m)(using logInfo())
62+
override def info(m: => String): F[Unit] = logger.info(m)(using logInfo())
63+
override def debug(m: => String): F[Unit] = logger.debug(m)(using logInfo())
64+
override def trace(m: => String): F[Unit] = logger.trace(m)(using logInfo())
65+
66+
override def isErrorEnabled: F[Boolean] = Applicative[F].pure(true)
67+
override def isWarnEnabled: F[Boolean] = Applicative[F].pure(true)
68+
override def isInfoEnabled: F[Boolean] = Applicative[F].pure(true)
69+
override def isDebugEnabled: F[Boolean] = Applicative[F].pure(true)
70+
override def isTraceEnabled: F[Boolean] = Applicative[F].pure(true)
71+
72+
override def error(ctx: Map[String, String])(m: => String): F[Unit] =
73+
logger.error(m)(using logInfo()).withLogContext(ctx.toList*)(using logger)
74+
override def warn(ctx: Map[String, String])(m: => String): F[Unit] =
75+
logger.warn(m)(using logInfo()).withLogContext(ctx.toList*)(using logger)
76+
override def info(ctx: Map[String, String])(m: => String): F[Unit] =
77+
logger.info(m)(using logInfo()).withLogContext(ctx.toList*)(using logger)
78+
override def debug(ctx: Map[String, String])(m: => String): F[Unit] =
79+
logger.debug(m)(using logInfo()).withLogContext(ctx.toList*)(using logger)
80+
override def trace(ctx: Map[String, String])(m: => String): F[Unit] =
81+
logger.trace(m)(using logInfo()).withLogContext(ctx.toList*)(using logger)
82+
83+
end WoofLog4CatsLogger
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package org.legogroup.woof.log4cats
2+
3+
import cats.effect.IO
4+
import cats.effect.kernel.Clock
5+
import cats.effect.std.Dispatcher
6+
import cats.Id
7+
import org.legogroup.woof.*
8+
import org.typelevel.log4cats.LoggerFactory
9+
import scala.concurrent.duration.*
10+
11+
class Log4CatsSuite extends munit.CatsEffectSuite:
12+
13+
override def munitIOTimeout = 10.minutes
14+
15+
private val ctx = Map("a" -> "a", "my context" -> "MY CONTEXT")
16+
17+
test("should log stuff") {
18+
given Printer = NoColorPrinter(testFormatTime)
19+
given Filter = Filter.everything
20+
given Clock[IO] = leetClock
21+
22+
for
23+
stringOutput <- newStringWriter
24+
woofLogger <- DefaultLogger.makeIo(stringOutput)
25+
log4catsLogger <- IO(WoofFactory[IO](woofLogger).getLogger)
26+
_ <- log4catsLogger.info("HELLO, Log4Cats!")
27+
result <- stringOutput.get
28+
yield assertEquals(
29+
result,
30+
"1987-05-31 13:37:00 [INFO ] org.legogroup.woof.log4cats.Log4CatsSuite: HELLO, Log4Cats! (Log4CatsSuite.scala:26)\n",
31+
)
32+
end for
33+
}
34+
35+
test("should respect log levels") {
36+
given Printer = NoColorPrinter(testFormatTime)
37+
given Filter = Filter.exactLevel(LogLevel.Warn)
38+
given Clock[IO] = leetClock
39+
40+
for
41+
stringWriter <- newStringWriter
42+
woofLogger <- DefaultLogger.makeIo(stringWriter)
43+
log4catsLogger <- IO(WoofFactory[IO](woofLogger).getLogger)
44+
_ <- log4catsLogger.error("ERROR, Log4Cats!")
45+
_ <- log4catsLogger.warn("WARN, Log4Cats!")
46+
_ <- log4catsLogger.info("INFO, Log4Cats!")
47+
_ <- log4catsLogger.debug("DEBUG, Log4Cats!")
48+
_ <- log4catsLogger.debug("TRACE, Log4Cats!")
49+
result <- stringWriter.get
50+
yield assertEquals(
51+
result,
52+
"1987-05-31 13:37:00 [WARN ] org.legogroup.woof.log4cats.Log4CatsSuite: WARN, Log4Cats! (Log4CatsSuite.scala:45)\n",
53+
)
54+
end for
55+
}
56+
57+
test("should log context") {
58+
given Printer = NoColorPrinter(testFormatTime)
59+
given Filter = Filter.everything
60+
given Clock[IO] = leetClock
61+
62+
for
63+
stringOutput <- newStringWriter
64+
woofLogger <- DefaultLogger.makeIo(stringOutput)
65+
log4catsLogger <- IO(WoofFactory[IO](woofLogger).getLogger)
66+
_ <- log4catsLogger.info(Map("a" -> "a", "my context" -> "MY CONTEXT"))("HELLO, CONTEXT!")
67+
result <- stringOutput.get
68+
yield assertEquals(
69+
result,
70+
"1987-05-31 13:37:00 [INFO ] a=a, my context=MY CONTEXT org.legogroup.woof.log4cats.Log4CatsSuite: HELLO, CONTEXT! (Log4CatsSuite.scala:66)\n",
71+
)
72+
end for
73+
}
74+
75+
test("should log throwable") {
76+
given Printer = NoColorPrinter(testFormatTime)
77+
given Filter = Filter.everything
78+
given Clock[IO] = leetClock
79+
80+
for
81+
stringOutput <- newStringWriter
82+
woofLogger <- DefaultLogger.makeIo(stringOutput)
83+
log4catsLogger <- IO(WoofFactory[IO](woofLogger).getLogger)
84+
_ <- log4catsLogger.info(new RuntimeException("BOOM!"))("THROWABLE")
85+
result <- stringOutput.get
86+
yield assertEquals(
87+
result,
88+
"1987-05-31 13:37:00 [INFO ] org.legogroup.woof.log4cats.Log4CatsSuite: THROWABLE BOOM! (Log4CatsSuite.scala:84)\n",
89+
)
90+
end for
91+
}
92+
93+
test("should log context and throwable") {
94+
given Printer = NoColorPrinter(testFormatTime)
95+
given Filter = Filter.everything
96+
given Clock[IO] = leetClock
97+
98+
for
99+
stringOutput <- newStringWriter
100+
woofLogger <- DefaultLogger.makeIo(stringOutput)
101+
log4catsLogger <- IO(WoofFactory[IO](woofLogger).getLogger)
102+
_ <- log4catsLogger.info(ctx, new RuntimeException("BOOM!"))("CONTEXT + THROWABLE")
103+
result <- stringOutput.get
104+
yield assertEquals(
105+
result,
106+
"1987-05-31 13:37:00 [INFO ] a=a, my context=MY CONTEXT org.legogroup.woof.log4cats.Log4CatsSuite: CONTEXT + THROWABLE BOOM! (Log4CatsSuite.scala:102)\n",
107+
)
108+
end for
109+
}
110+
111+
test("should not fail on null throwable") {
112+
given Printer = NoColorPrinter(testFormatTime)
113+
given Filter = Filter.everything
114+
given Clock[IO] = leetClock
115+
116+
for
117+
stringWriter <- newStringWriter
118+
woofLogger <- DefaultLogger.makeIo(stringWriter)
119+
log4catsLogger <- IO(WoofFactory[IO](woofLogger).getLogger)
120+
_ <- log4catsLogger.debug(null: Throwable)("NULL THROWABLE")
121+
result <- stringWriter.get
122+
yield assertEquals(
123+
result,
124+
"1987-05-31 13:37:00 [DEBUG] org.legogroup.woof.log4cats.Log4CatsSuite: NULL THROWABLE (Log4CatsSuite.scala:120)\n",
125+
)
126+
end for
127+
}
128+
129+
test("should not fail on null throwable with context") {
130+
given Printer = NoColorPrinter(testFormatTime)
131+
given Filter = Filter.everything
132+
given Clock[IO] = leetClock
133+
134+
for
135+
stringWriter <- newStringWriter
136+
woofLogger <- DefaultLogger.makeIo(stringWriter)
137+
log4catsLogger <- IO(WoofFactory[IO](woofLogger).getLogger)
138+
_ <- log4catsLogger.debug(ctx, null: Throwable)("NULL THROWABLE + CONTEXT")
139+
result <- stringWriter.get
140+
yield assertEquals(
141+
result,
142+
"1987-05-31 13:37:00 [DEBUG] a=a, my context=MY CONTEXT org.legogroup.woof.log4cats.Log4CatsSuite: NULL THROWABLE + CONTEXT (Log4CatsSuite.scala:138)\n",
143+
)
144+
end for
145+
}
146+
147+
end Log4CatsSuite

0 commit comments

Comments
 (0)