Skip to content

Commit 1ccb243

Browse files
Add log4cats integration
1 parent ab1a96f commit 1ccb243

File tree

4 files changed

+297
-0
lines changed

4 files changed

+297
-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

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: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
def WoofFactory[F[_]: Applicative]: Conversion[woof.Logger[F], log4cats.LoggerFactory[F]] =
9+
new Conversion[woof.Logger[F], log4cats.LoggerFactory[F]]:
10+
11+
def apply(logger: woof.Logger[F]): log4cats.LoggerFactory[F] =
12+
new log4cats.LoggerFactory[F]:
13+
14+
override def fromName(name: String): F[log4cats.SelfAwareStructuredLogger[F]] =
15+
Applicative[F].pure(getLoggerFromName(name))
16+
17+
override def getLoggerFromName(name: String): log4cats.SelfAwareStructuredLogger[F] =
18+
new log4cats.SelfAwareStructuredLogger[F]:
19+
20+
private def logInfo(): woof.LogInfo =
21+
val stacktraceElements = (new Throwable).getStackTrace()
22+
val lastIndex = stacktraceElements.reverse.indexWhere(s => s.getClassName == this.getClass.getName)
23+
val callingMethodIndex = stacktraceElements.size - lastIndex
24+
val callingMethod = stacktraceElements(callingMethodIndex)
25+
val fileName =
26+
(callingMethod.getClassName.replace('.', '/') + ".scala").split("\\/").takeRight(1).mkString
27+
val lineNumber = callingMethod.getLineNumber - 1
28+
woof.LogInfo(woof.EnclosingClass(name), fileName, lineNumber)
29+
30+
private def thrMsg(m: String, t: Throwable): String =
31+
(try s"$m ${t.getMessage}"
32+
catch case _ => s"$m ")
33+
34+
override def error(t: Throwable)(m: => String): F[Unit] = logger.error(thrMsg(m, t))(using logInfo())
35+
override def warn(t: Throwable)(m: => String): F[Unit] = logger.warn(thrMsg(m, t))(using logInfo())
36+
override def info(t: Throwable)(m: => String): F[Unit] = logger.info(thrMsg(m, t))(using logInfo())
37+
override def debug(t: Throwable)(m: => String): F[Unit] = logger.debug(thrMsg(m, t))(using logInfo())
38+
override def trace(t: Throwable)(m: => String): F[Unit] = logger.trace(thrMsg(m, t))(using logInfo())
39+
40+
override def error(m: => String): F[Unit] = logger.error(m)(using logInfo())
41+
override def warn(m: => String): F[Unit] = logger.warn(m)(using logInfo())
42+
override def info(m: => String): F[Unit] = logger.info(m)(using logInfo())
43+
override def debug(m: => String): F[Unit] = logger.debug(m)(using logInfo())
44+
override def trace(m: => String): F[Unit] = logger.trace(m)(using logInfo())
45+
46+
override def isErrorEnabled: F[Boolean] = Applicative[F].pure(true)
47+
override def isWarnEnabled: F[Boolean] = Applicative[F].pure(true)
48+
override def isInfoEnabled: F[Boolean] = Applicative[F].pure(true)
49+
override def isDebugEnabled: F[Boolean] = Applicative[F].pure(true)
50+
override def isTraceEnabled: F[Boolean] = Applicative[F].pure(true)
51+
52+
override def error(ctx: Map[String, String])(m: => String): F[Unit] =
53+
logger.error(m)(using logInfo()).withLogContext(ctx.toList*)(using logger)
54+
override def warn(ctx: Map[String, String])(m: => String): F[Unit] =
55+
logger.warn(m)(using logInfo()).withLogContext(ctx.toList*)(using logger)
56+
override def info(ctx: Map[String, String])(m: => String): F[Unit] =
57+
logger.info(m)(using logInfo()).withLogContext(ctx.toList*)(using logger)
58+
override def debug(ctx: Map[String, String])(m: => String): F[Unit] =
59+
logger.debug(m)(using logInfo()).withLogContext(ctx.toList*)(using logger)
60+
override def trace(ctx: Map[String, String])(m: => String): F[Unit] =
61+
logger.trace(m)(using logInfo()).withLogContext(ctx.toList*)(using logger)
62+
63+
override def error(ctx: Map[String, String], t: Throwable)(m: => String): F[Unit] =
64+
logger.error(thrMsg(m, t))(using logInfo()).withLogContext(ctx.toList*)(using logger)
65+
override def warn(ctx: Map[String, String], t: Throwable)(m: => String): F[Unit] =
66+
logger.warn(thrMsg(m, t))(using logInfo()).withLogContext(ctx.toList*)(using logger)
67+
override def info(ctx: Map[String, String], t: Throwable)(m: => String): F[Unit] =
68+
logger.info(thrMsg(m, t))(using logInfo()).withLogContext(ctx.toList*)(using logger)
69+
override def debug(ctx: Map[String, String], t: Throwable)(m: => String): F[Unit] =
70+
logger.debug(thrMsg(m, t))(using logInfo()).withLogContext(ctx.toList*)(using logger)
71+
override def trace(ctx: Map[String, String], t: Throwable)(m: => String): F[Unit] =
72+
logger.trace(thrMsg(m, t))(using logInfo()).withLogContext(ctx.toList*)(using logger)
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)