Skip to content

Commit b1f55ba

Browse files
committed
introduce DelegatingLogger
This idea was introduced in log4cats-natchez, and it was suggested it be moved upstream
1 parent 561a281 commit b1f55ba

4 files changed

Lines changed: 235 additions & 1 deletion

File tree

build.sbt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ lazy val testing = crossProject(JSPlatform, JVMPlatform, NativePlatform)
6363
name := "log4cats-testing",
6464
libraryDependencies ++= Seq(
6565
"org.typelevel" %%% "cats-effect" % catsEffectV,
66-
"ch.qos.logback" % "logback-classic" % logbackClassicV % Test
66+
"org.typelevel" %% "scalacheck-effect-munit" % "2.1.0-RC1" % Test,
67+
"ch.qos.logback" % "logback-classic" % logbackClassicV % Test
6768
)
6869
)
6970
.nativeSettings(commonNativeSettings)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
19+
import cats.*
20+
import cats.syntax.all.*
21+
22+
/**
23+
* A logger that delegates all logging operations to another logger that is wrapped in an effect
24+
* F[_].
25+
*
26+
* This is useful when you need to modify the underlying logger for each log operation. A common use
27+
* case is adding the current tracing context. For example, if your application uses Natchez, you
28+
* might have something like:
29+
* {{{
30+
* def tracedLogger[F[_]](logger: SelfAwareStructuredLogger[F], trace: Trace[F]): SelfAwareStructuredLogger[F] =
31+
* new DelegatingLogger(
32+
* (trace.traceId, trace.spanId).mapN { (traceId, spanId) =>
33+
* logger.addContext(Map(
34+
* "trace.id" -> traceId.toString,
35+
* "span.id" -> spanId.toString
36+
* ))
37+
* }
38+
* )
39+
* }}}
40+
*
41+
* @param delegate
42+
* The effect containing the logger to delegate to. This effect is evaluated for each logging
43+
* operation.
44+
* @tparam F
45+
* The effect type, which must have a FlatMap instance
46+
*/
47+
class DelegatingLogger[F[_]: FlatMap](delegate: F[SelfAwareStructuredLogger[F]])
48+
extends SelfAwareStructuredLogger[F] {
49+
override def isTraceEnabled: F[Boolean] = delegate.flatMap(_.isTraceEnabled)
50+
override def isDebugEnabled: F[Boolean] = delegate.flatMap(_.isDebugEnabled)
51+
override def isInfoEnabled: F[Boolean] = delegate.flatMap(_.isInfoEnabled)
52+
override def isWarnEnabled: F[Boolean] = delegate.flatMap(_.isWarnEnabled)
53+
override def isErrorEnabled: F[Boolean] = delegate.flatMap(_.isErrorEnabled)
54+
55+
override def trace(ctx: Map[String, String])(msg: => String): F[Unit] =
56+
delegate.flatMap(_.trace(ctx)(msg))
57+
override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
58+
delegate.flatMap(_.trace(ctx, t)(msg))
59+
override def debug(ctx: Map[String, String])(msg: => String): F[Unit] =
60+
delegate.flatMap(_.debug(ctx)(msg))
61+
override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
62+
delegate.flatMap(_.debug(ctx, t)(msg))
63+
override def info(ctx: Map[String, String])(msg: => String): F[Unit] =
64+
delegate.flatMap(_.info(ctx)(msg))
65+
override def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
66+
delegate.flatMap(_.info(ctx, t)(msg))
67+
override def warn(ctx: Map[String, String])(msg: => String): F[Unit] =
68+
delegate.flatMap(_.warn(ctx)(msg))
69+
override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
70+
delegate.flatMap(_.warn(ctx, t)(msg))
71+
override def error(ctx: Map[String, String])(msg: => String): F[Unit] =
72+
delegate.flatMap(_.error(ctx)(msg))
73+
override def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
74+
delegate.flatMap(_.error(ctx, t)(msg))
75+
override def error(t: Throwable)(message: => String): F[Unit] =
76+
delegate.flatMap(_.error(t)(message))
77+
override def warn(t: Throwable)(message: => String): F[Unit] =
78+
delegate.flatMap(_.warn(t)(message))
79+
override def info(t: Throwable)(message: => String): F[Unit] =
80+
delegate.flatMap(_.info(t)(message))
81+
override def debug(t: Throwable)(message: => String): F[Unit] =
82+
delegate.flatMap(_.debug(t)(message))
83+
override def trace(t: Throwable)(message: => String): F[Unit] =
84+
delegate.flatMap(_.trace(t)(message))
85+
override def error(message: => String): F[Unit] = delegate.flatMap(_.error(message))
86+
override def warn(message: => String): F[Unit] = delegate.flatMap(_.warn(message))
87+
override def info(message: => String): F[Unit] = delegate.flatMap(_.info(message))
88+
override def debug(message: => String): F[Unit] = delegate.flatMap(_.debug(message))
89+
override def trace(message: => String): F[Unit] = delegate.flatMap(_.trace(message))
90+
}

testing/shared/src/main/scala/org/typelevel/log4cats/testing/StructuredTestingLogger.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ object StructuredTestingLogger {
3535
def ctx: Map[String, String]
3636
def message: String
3737
def throwOpt: Option[Throwable]
38+
39+
def mapCtx(f: Map[String, String] => Map[String, String]): LogMessage =
40+
this match {
41+
case TRACE(m, t, c) => TRACE(m, t, f(c))
42+
case DEBUG(m, t, c) => DEBUG(m, t, f(c))
43+
case INFO(m, t, c) => INFO(m, t, f(c))
44+
case WARN(m, t, c) => WARN(m, t, f(c))
45+
case ERROR(m, t, c) => ERROR(m, t, f(c))
46+
}
47+
48+
def modifyString(f: String => String): LogMessage =
49+
this match {
50+
case TRACE(m, t, c) => TRACE(f(m), t, c)
51+
case DEBUG(m, t, c) => DEBUG(f(m), t, c)
52+
case INFO(m, t, c) => INFO(f(m), t, c)
53+
case WARN(m, t, c) => WARN(f(m), t, c)
54+
case ERROR(m, t, c) => ERROR(f(m), t, c)
55+
}
3856
}
3957

4058
final case class TRACE(
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2018 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.log4cats
18+
19+
import cats.syntax.all.*
20+
import cats.effect.*
21+
import munit.{CatsEffectSuite, ScalaCheckEffectSuite}
22+
import org.scalacheck.*
23+
import org.scalacheck.Arbitrary.arbitrary
24+
import org.scalacheck.effect.PropF
25+
import org.typelevel.log4cats.testing.StructuredTestingLogger
26+
import org.typelevel.log4cats.testing.StructuredTestingLogger.{
27+
DEBUG,
28+
ERROR,
29+
INFO,
30+
LogMessage,
31+
TRACE,
32+
WARN
33+
}
34+
35+
class DelegatingLoggerSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
36+
private val genLogMessage: Gen[LogMessage] =
37+
for {
38+
msg <- arbitrary[String]
39+
ex <- arbitrary[Option[Throwable]]
40+
ctx <- arbitrary[Map[String, String]]
41+
constructor <- Gen.oneOf[(String, Option[Throwable], Map[String, String]) => LogMessage](
42+
TRACE.apply _,
43+
DEBUG.apply _,
44+
INFO.apply _,
45+
WARN.apply _,
46+
ERROR.apply _
47+
)
48+
} yield constructor(msg, ex, ctx)
49+
50+
private implicit val arbLogMessage: Arbitrary[LogMessage] = Arbitrary(genLogMessage)
51+
52+
test("messages should be given to the underlying logger") {
53+
PropF.forAllF {
54+
(
55+
expectedCtx: Map[String, String],
56+
expectedStringModifier: String => String,
57+
events: Vector[LogMessage]
58+
) =>
59+
for {
60+
underlying <- StructuredTestingLogger.ref[IO]()
61+
62+
logger = new DelegatingLogger[IO](
63+
underlying
64+
.addContext(expectedCtx)
65+
.withModifiedString(expectedStringModifier)
66+
.pure[IO]
67+
)
68+
69+
_ <- events.traverse {
70+
case TRACE(message, Some(ex), ctx) if ctx.isEmpty => logger.trace(ex)(message)
71+
case TRACE(message, None, ctx) if ctx.isEmpty => logger.trace(message)
72+
case TRACE(message, Some(ex), ctx) => logger.trace(ctx, ex)(message)
73+
case TRACE(message, None, ctx) => logger.trace(ctx)(message)
74+
75+
case DEBUG(message, Some(ex), ctx) if ctx.isEmpty => logger.debug(ex)(message)
76+
case DEBUG(message, None, ctx) if ctx.isEmpty => logger.debug(message)
77+
case DEBUG(message, Some(ex), ctx) => logger.debug(ctx, ex)(message)
78+
case DEBUG(message, None, ctx) => logger.debug(ctx)(message)
79+
80+
case INFO(message, Some(ex), ctx) if ctx.isEmpty => logger.info(ex)(message)
81+
case INFO(message, None, ctx) if ctx.isEmpty => logger.info(message)
82+
case INFO(message, Some(ex), ctx) => logger.info(ctx, ex)(message)
83+
case INFO(message, None, ctx) => logger.info(ctx)(message)
84+
85+
case WARN(message, Some(ex), ctx) if ctx.isEmpty => logger.warn(ex)(message)
86+
case WARN(message, None, ctx) if ctx.isEmpty => logger.warn(message)
87+
case WARN(message, Some(ex), ctx) => logger.warn(ctx, ex)(message)
88+
case WARN(message, None, ctx) => logger.warn(ctx)(message)
89+
90+
case ERROR(message, Some(ex), ctx) if ctx.isEmpty => logger.error(ex)(message)
91+
case ERROR(message, None, ctx) if ctx.isEmpty => logger.error(message)
92+
case ERROR(message, Some(ex), ctx) => logger.error(ctx, ex)(message)
93+
case ERROR(message, None, ctx) => logger.error(ctx)(message)
94+
}
95+
96+
loggedEvents <- underlying.logged
97+
expectedEvents = events.map(
98+
_.mapCtx(expectedCtx ++ _).modifyString(expectedStringModifier)
99+
)
100+
} yield assertEquals(loggedEvents, expectedEvents)
101+
}
102+
}
103+
104+
test("transformation effect is applied for each subsequent log event") {
105+
val indexes = (0 until 10).toVector
106+
for {
107+
countdown <- Ref[IO].of(indexes)
108+
expectedLogEvents = indexes.map(i => INFO("log event", None, Map("index" -> i.toString)))
109+
110+
underlying <- StructuredTestingLogger.ref[IO]()
111+
112+
transform =
113+
countdown.modify {
114+
case i +: tail => (tail, underlying.addContext(Map("index" -> i.toString)))
115+
case _ => throw new IllegalStateException("Countdown is empty")
116+
}
117+
118+
logger = new DelegatingLogger[IO](transform)
119+
120+
_ <- indexes.traverse_(_ => logger.info("log event"))
121+
122+
loggedEvents <- underlying.logged
123+
} yield assertEquals(loggedEvents, expectedLogEvents)
124+
}
125+
}

0 commit comments

Comments
 (0)