Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ lazy val testing = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.settings(
name := "log4cats-testing",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect" % catsEffectV,
"ch.qos.logback" % "logback-classic" % logbackClassicV % Test
"org.typelevel" %%% "cats-effect" % catsEffectV,
"org.typelevel" %%% "scalacheck-effect-munit" % "2.1.0-RC1" % Test,
"ch.qos.logback" % "logback-classic" % logbackClassicV % Test
)
)
.nativeSettings(commonNativeSettings)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2018 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.log4cats

import cats.*
import cats.syntax.all.*

/**
* A logger that delegates all logging operations to another logger that is wrapped in an effect
* F[_].
*
* This is useful when you need to modify the underlying logger for each log operation. A common use
* case is adding the current tracing context. For example, if your application uses Natchez, you
* might have something like:
* {{{
* def tracedLogger[F[_]](logger: SelfAwareStructuredLogger[F], trace: Trace[F]): SelfAwareStructuredLogger[F] =
* new DelegatingLogger(
* (trace.traceId, trace.spanId).mapN { (traceId, spanId) =>
* logger.addContext(Map(
* "trace.id" -> traceId.toString,
* "span.id" -> spanId.toString
* ))
* }
* )
* }}}
*
* @param delegate
* The effect containing the logger to delegate to. This effect is evaluated for each logging
* operation.
* @tparam F
* The effect type, which must have a FlatMap instance
*/
class DelegatingLogger[F[_]: FlatMap](delegate: F[SelfAwareStructuredLogger[F]])
extends SelfAwareStructuredLogger[F] {
override def isTraceEnabled: F[Boolean] = delegate.flatMap(_.isTraceEnabled)
override def isDebugEnabled: F[Boolean] = delegate.flatMap(_.isDebugEnabled)
override def isInfoEnabled: F[Boolean] = delegate.flatMap(_.isInfoEnabled)
override def isWarnEnabled: F[Boolean] = delegate.flatMap(_.isWarnEnabled)
override def isErrorEnabled: F[Boolean] = delegate.flatMap(_.isErrorEnabled)

override def trace(ctx: Map[String, String])(msg: => String): F[Unit] =
delegate.flatMap(_.trace(ctx)(msg))
override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
delegate.flatMap(_.trace(ctx, t)(msg))
override def debug(ctx: Map[String, String])(msg: => String): F[Unit] =
delegate.flatMap(_.debug(ctx)(msg))
override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
delegate.flatMap(_.debug(ctx, t)(msg))
override def info(ctx: Map[String, String])(msg: => String): F[Unit] =
delegate.flatMap(_.info(ctx)(msg))
override def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
delegate.flatMap(_.info(ctx, t)(msg))
override def warn(ctx: Map[String, String])(msg: => String): F[Unit] =
delegate.flatMap(_.warn(ctx)(msg))
override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
delegate.flatMap(_.warn(ctx, t)(msg))
override def error(ctx: Map[String, String])(msg: => String): F[Unit] =
delegate.flatMap(_.error(ctx)(msg))
override def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
delegate.flatMap(_.error(ctx, t)(msg))
override def error(t: Throwable)(message: => String): F[Unit] =
delegate.flatMap(_.error(t)(message))
override def warn(t: Throwable)(message: => String): F[Unit] =
delegate.flatMap(_.warn(t)(message))
override def info(t: Throwable)(message: => String): F[Unit] =
delegate.flatMap(_.info(t)(message))
override def debug(t: Throwable)(message: => String): F[Unit] =
delegate.flatMap(_.debug(t)(message))
override def trace(t: Throwable)(message: => String): F[Unit] =
delegate.flatMap(_.trace(t)(message))
override def error(message: => String): F[Unit] = delegate.flatMap(_.error(message))
override def warn(message: => String): F[Unit] = delegate.flatMap(_.warn(message))
override def info(message: => String): F[Unit] = delegate.flatMap(_.info(message))
override def debug(message: => String): F[Unit] = delegate.flatMap(_.debug(message))
override def trace(message: => String): F[Unit] = delegate.flatMap(_.trace(message))
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ object StructuredTestingLogger {
def ctx: Map[String, String]
def message: String
def throwOpt: Option[Throwable]

def mapCtx(f: Map[String, String] => Map[String, String]): LogMessage =
this match {
case TRACE(m, t, c) => TRACE(m, t, f(c))
case DEBUG(m, t, c) => DEBUG(m, t, f(c))
case INFO(m, t, c) => INFO(m, t, f(c))
case WARN(m, t, c) => WARN(m, t, f(c))
case ERROR(m, t, c) => ERROR(m, t, f(c))
}

def modifyString(f: String => String): LogMessage =
this match {
case TRACE(m, t, c) => TRACE(f(m), t, c)
case DEBUG(m, t, c) => DEBUG(f(m), t, c)
case INFO(m, t, c) => INFO(f(m), t, c)
case WARN(m, t, c) => WARN(f(m), t, c)
case ERROR(m, t, c) => ERROR(f(m), t, c)
}
}

final case class TRACE(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2018 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.log4cats

import cats.syntax.all.*
import cats.effect.*
import munit.{CatsEffectSuite, ScalaCheckEffectSuite}
import org.scalacheck.*
import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.effect.PropF
import org.typelevel.log4cats.testing.StructuredTestingLogger
import org.typelevel.log4cats.testing.StructuredTestingLogger.{
DEBUG,
ERROR,
INFO,
LogMessage,
TRACE,
WARN
}

class DelegatingLoggerSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
private val genLogMessage: Gen[LogMessage] =
for {
msg <- arbitrary[String]
ex <- arbitrary[Option[Throwable]]
ctx <- arbitrary[Map[String, String]]
constructor <- Gen.oneOf[(String, Option[Throwable], Map[String, String]) => LogMessage](
TRACE.apply _,
DEBUG.apply _,
INFO.apply _,
WARN.apply _,
ERROR.apply _
)
} yield constructor(msg, ex, ctx)

private implicit val arbLogMessage: Arbitrary[LogMessage] = Arbitrary(genLogMessage)

test("messages should be given to the underlying logger") {
PropF.forAllF {
(
expectedCtx: Map[String, String],
expectedStringModifier: String => String,
events: Vector[LogMessage]
) =>
for {
underlying <- StructuredTestingLogger.ref[IO]()

logger = new DelegatingLogger[IO](
underlying
.addContext(expectedCtx)
.withModifiedString(expectedStringModifier)
.pure[IO]
)

_ <- events.traverse {
case TRACE(message, Some(ex), ctx) if ctx.isEmpty => logger.trace(ex)(message)
case TRACE(message, None, ctx) if ctx.isEmpty => logger.trace(message)
case TRACE(message, Some(ex), ctx) => logger.trace(ctx, ex)(message)
case TRACE(message, None, ctx) => logger.trace(ctx)(message)

case DEBUG(message, Some(ex), ctx) if ctx.isEmpty => logger.debug(ex)(message)
case DEBUG(message, None, ctx) if ctx.isEmpty => logger.debug(message)
case DEBUG(message, Some(ex), ctx) => logger.debug(ctx, ex)(message)
case DEBUG(message, None, ctx) => logger.debug(ctx)(message)

case INFO(message, Some(ex), ctx) if ctx.isEmpty => logger.info(ex)(message)
case INFO(message, None, ctx) if ctx.isEmpty => logger.info(message)
case INFO(message, Some(ex), ctx) => logger.info(ctx, ex)(message)
case INFO(message, None, ctx) => logger.info(ctx)(message)

case WARN(message, Some(ex), ctx) if ctx.isEmpty => logger.warn(ex)(message)
case WARN(message, None, ctx) if ctx.isEmpty => logger.warn(message)
case WARN(message, Some(ex), ctx) => logger.warn(ctx, ex)(message)
case WARN(message, None, ctx) => logger.warn(ctx)(message)

case ERROR(message, Some(ex), ctx) if ctx.isEmpty => logger.error(ex)(message)
case ERROR(message, None, ctx) if ctx.isEmpty => logger.error(message)
case ERROR(message, Some(ex), ctx) => logger.error(ctx, ex)(message)
case ERROR(message, None, ctx) => logger.error(ctx)(message)
}

loggedEvents <- underlying.logged
expectedEvents = events.map(
_.mapCtx(expectedCtx ++ _).modifyString(expectedStringModifier)
)
} yield assertEquals(loggedEvents, expectedEvents)
}
}

test("transformation effect is applied for each subsequent log event") {
val indexes = (0 until 10).toVector
for {
countdown <- Ref[IO].of(indexes)
expectedLogEvents = indexes.map(i => INFO("log event", None, Map("index" -> i.toString)))

underlying <- StructuredTestingLogger.ref[IO]()

transform =
countdown.modify {
case i +: tail => (tail, underlying.addContext(Map("index" -> i.toString)))
case _ => throw new IllegalStateException("Countdown is empty")
}

logger = new DelegatingLogger[IO](transform)

_ <- indexes.traverse_(_ => logger.info("log event"))

loggedEvents <- underlying.logged
} yield assertEquals(loggedEvents, expectedLogEvents)
}
}