diff --git a/build.sbt b/build.sbt index d12b7cd..56a1913 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,5 @@ // https://typelevel.org/sbt-typelevel/faq.html#what-is-a-base-version-anyway -ThisBuild / tlBaseVersion := "1.0" // your current series x.y +ThisBuild / tlBaseVersion := "2.0" // your current series x.y ThisBuild / organization := "com.permutive" ThisBuild / organizationName := "Permutive" diff --git a/odin-dynamic/src/main/scala/com/permutive/logging/dynamic/odin/DynamicOdinLogger.scala b/odin-dynamic/src/main/scala/com/permutive/logging/dynamic/odin/DynamicOdinLogger.scala index 5ae2c10..9f083e6 100644 --- a/odin-dynamic/src/main/scala/com/permutive/logging/dynamic/odin/DynamicOdinLogger.scala +++ b/odin-dynamic/src/main/scala/com/permutive/logging/dynamic/odin/DynamicOdinLogger.scala @@ -37,10 +37,9 @@ trait DynamicOdinConsoleLogger[F[_]] extends Logger[F] { } class DynamicOdinConsoleLoggerImpl[F[_]: Monad: Clock] private[odin] ( - ref: Ref[F, (RuntimeConfig, Logger[F])], - level: Level + ref: Ref[F, (RuntimeConfig, Logger[F])] )(make: RuntimeConfig => Logger[F])(implicit eq: Eq[RuntimeConfig]) - extends DefaultLogger[F](level) + extends DefaultLogger[F](Level.Trace) with DynamicOdinConsoleLogger[F] { outer => protected def withLogger(f: Logger[F] => F[Unit]): F[Unit] = ref.get.flatMap { case (_, l) => f(l) @@ -56,32 +55,74 @@ class DynamicOdinConsoleLoggerImpl[F[_]: Monad: Clock] private[odin] ( override def submit(msg: LoggerMessage): F[Unit] = withLogger(_.log(msg)) override def withMinimalLevel(level: Level): Logger[F] = - new DynamicOdinConsoleLoggerImpl[F](ref, level)(make) { + new DynamicOdinConsoleLoggerImpl[F](ref)(make) { override protected def withLogger(f: Logger[F] => F[Unit]): F[Unit] = outer.withLogger(l => f(l.withMinimalLevel(level))) } } object DynamicOdinConsoleLogger { - case class Config( - formatter: Formatter, - asyncTimeWindow: FiniteDuration = 1.millis, - asyncMaxBufferSize: Option[Int] = None + sealed abstract class Config private ( + val formatter: Formatter, + val asyncTimeWindow: FiniteDuration, + val asyncMaxBufferSize: Option[Int] ) - case class RuntimeConfig( - minLevel: Level, - levelMapping: Map[String, Level] = Map.empty + object Config { + def apply( + formatter: Formatter, + asyncTimeWindow: FiniteDuration = 1.millis, + asyncMaxBufferSize: Option[Int] = None + ): Config = new Config(formatter, asyncTimeWindow, asyncMaxBufferSize) {} + } + + sealed abstract class RuntimeConfig private ( + val defaultLevel: Level, + val levelMapping: Map[String, LevelConfig] ) object RuntimeConfig { - implicit val eq: Eq[RuntimeConfig] = cats.derived.semiauto.eq + def apply( + defaultLevel: Level, + levelMapping: Map[String, LevelConfig] = Map.empty + ): RuntimeConfig = new RuntimeConfig(defaultLevel, levelMapping) {} + + implicit val eq: Eq[RuntimeConfig] = + Eq.by(config => (config.defaultLevel, config.levelMapping)) + } + + sealed trait LevelConfig + + object LevelConfig { + private[odin] trait ToLevel { self: LevelConfig => + def toLevel: Level + } + + case object Trace extends LevelConfig with ToLevel { + val toLevel = Level.Trace + } + case object Debug extends LevelConfig with ToLevel { + val toLevel = Level.Debug + } + case object Info extends LevelConfig with ToLevel { + val toLevel = Level.Info + } + case object Warn extends LevelConfig with ToLevel { + val toLevel = Level.Warn + } + case object Error extends LevelConfig with ToLevel { + val toLevel = Level.Error + } + case object Unknown extends LevelConfig + case object Off extends LevelConfig + + implicit val eq: Eq[LevelConfig] = cats.derived.semiauto.eq } def console[F[_]: Async](config: Config, initialConfig: RuntimeConfig)( implicit eq: Eq[RuntimeConfig] ): Resource[F, DynamicOdinConsoleLogger[F]] = create(config, initialConfig)(c => - consoleLogger(config.formatter, c.minLevel) + consoleLogger(config.formatter, c.defaultLevel) ) def create[F[_]: Async]( @@ -95,14 +136,17 @@ object DynamicOdinConsoleLogger { val makeWithLevels: RuntimeConfig => Logger[F] = { config => val mainLogger = make(config) - if (config.levelMapping.isEmpty) mainLogger + if (config.levelMapping.isEmpty) + mainLogger.withMinimalLevel(config.defaultLevel) else enclosureRouting( - config.levelMapping.view - .mapValues(mainLogger.withMinimalLevel) - .toList: _* - ) - .withFallback(mainLogger) + config.levelMapping.view.mapValues { + case _: LevelConfig.Off.type => Logger.noop + case _: LevelConfig.Unknown.type => mainLogger + case level: LevelConfig.ToLevel => + mainLogger.withMinimalLevel(level.toLevel) + }.toList: _* + ).withFallback(mainLogger.withMinimalLevel(config.defaultLevel)) } for { @@ -112,8 +156,7 @@ object DynamicOdinConsoleLogger { ) ) underlying = new DynamicOdinConsoleLoggerImpl[F]( - ref, - runtimeConfig.minLevel + ref )(makeWithLevels) async <- underlying.withAsync( config.asyncTimeWindow, diff --git a/odin-dynamic/src/test/scala/com/permutive/logging/dynamic/odin/DynamicOdinLoggerSpec.scala b/odin-dynamic/src/test/scala/com/permutive/logging/dynamic/odin/DynamicOdinLoggerSpec.scala index 7f1fd05..692cebc 100644 --- a/odin-dynamic/src/test/scala/com/permutive/logging/dynamic/odin/DynamicOdinLoggerSpec.scala +++ b/odin-dynamic/src/test/scala/com/permutive/logging/dynamic/odin/DynamicOdinLoggerSpec.scala @@ -16,12 +16,17 @@ package com.permutive.logging.dynamic.odin -import cats.effect.unsafe.IORuntime import cats.effect.{IO, Resource} +import com.permutive.logging.dynamic.odin.DynamicOdinConsoleLogger.{ + LevelConfig, + RuntimeConfig +} import com.permutive.logging.odin.testing.OdinRefLogger -import io.odin.{Level, LoggerMessage} import io.odin.formatter.Formatter +import io.odin.meta.Position +import io.odin.{Level, LoggerMessage} import munit.{CatsEffectSuite, ScalaCheckEffectSuite} +import org.scalacheck.Arbitrary import org.scalacheck.effect.PropF import scala.collection.immutable.Queue @@ -29,56 +34,139 @@ import scala.concurrent.duration._ class DynamicOdinLoggerSpec extends CatsEffectSuite with ScalaCheckEffectSuite { - implicit val runtime: IORuntime = IORuntime.global + private implicit val arbPosition: Arbitrary[Position] = + Arbitrary( + Arbitrary + .arbitrary[(String, String, String, Int)] + .map((Position.apply _).tupled) + ) - test("record a message") { - PropF.forAllF { (message: String) => - val messages = runTest(_.info(message)) + private val defaultConfig = RuntimeConfig(Level.Info) - messages.map(_.map(_.message.value).toList).assertEquals(List(message)) + test("record only messages at the min level") { + PropF.forAllF { (debugMessage: String, infoMessage: String) => + val messages = runTest()(logger => + logger.debug(debugMessage) >> + logger.info(infoMessage) + ) + + messages + .map(_.map(_.message.value).toList) + .assertEquals(List(infoMessage)) } } - test("update global log level") { - PropF.forAllF { (message1: String, message2: String) => - val messages = runTest { logger => - logger.info(message1) >> IO.sleep(10.millis) >> logger.update( - DynamicOdinConsoleLogger.RuntimeConfig(Level.Warn) - ) >> logger.info( - message2 + test("disable logging for a particular enclosure") { + PropF.forAllF { + ( + pos1Msg: String, + pos2Msg: String, + position1: Position + ) => + val position2 = + position1.copy(enclosureName = position1.enclosureName + "2") + val messages = runTest( + RuntimeConfig( + Level.Info, + Map( + position2.enclosureName -> LevelConfig.Off + ) + ) + )(logger => + logger.info(pos1Msg)(implicitly, position1) >> + logger.error(pos2Msg)(implicitly, position2) ) + + messages + .map(_.map(_.message.value).toList) + .assertEquals(List(pos1Msg)) + } + } + + test("raises default-level config") { + PropF.forAllF { (messageBeforeChange: String, messageAfterChange: String) => + val messages = runTest() { logger => + logger.info(messageBeforeChange) >> + IO.sleep(10.millis) >> + logger.update(RuntimeConfig(defaultLevel = Level.Warn)) >> + logger.info(messageAfterChange) + } + messages + .map(_.map(_.message.value).toList) + .assertEquals(List(messageBeforeChange)) + } + } + + test("lowers default-level config") { + PropF.forAllF { (messageBeforeChange: String, messageAfterChange: String) => + val messages = runTest() { logger => + logger.info(messageBeforeChange) >> + IO.sleep(10.millis) >> + logger.update(RuntimeConfig(defaultLevel = Level.Debug)) >> + logger.debug(messageAfterChange) } - messages.map(_.map(_.message.value).toList).assertEquals(List(message1)) + messages + .map(_.map(_.message.value).toList) + .assertEquals(List(messageBeforeChange, messageAfterChange)) } } - test("update enclosure log level") { - PropF.forAllF { (message1: String, message2: String, message3: String) => - val messages = runTest { logger => - logger.info(message1) >> IO.sleep(10.millis) >> logger.update( - DynamicOdinConsoleLogger.RuntimeConfig( - Level.Info, - Map("com.permutive" -> Level.Warn) - ) - ) >> logger.info( - message2 - ) >> logger.warn(message3) + test("overrides default level for a certain package") { + PropF.forAllF { (message: String) => + val messages = runTest( + RuntimeConfig( + defaultLevel = Level.Warn, + Map("com.permutive.logging.dynamic.odin" -> LevelConfig.Debug) + ) + ) { logger => + logger.debug(message) } messages .map(_.map(_.message.value).toList) - .assertEquals(List(message1, message3)) + .assertEquals(List(message)) + } + } + + test("update enclosure log level") { + PropF.forAllNoShrinkF { + ( + infoMsg1Pos1: String, + infoMsg2Pos1: String, + warnMsg1Pos1: String, + infoMsg2Pos2: String, + position1: Position, + position2: Position + ) => + val messages = runTest() { logger => + val positionWhichChangesLevel = + position1.copy(enclosureName = position1.enclosureName + "changes") + logger.info(infoMsg1Pos1)(implicitly, positionWhichChangesLevel) >> + IO.sleep(10.millis) >> + logger.update( + RuntimeConfig( + Level.Info, + Map(positionWhichChangesLevel.enclosureName -> LevelConfig.Warn) + ) + ) >> + logger.info(infoMsg2Pos1)(implicitly, positionWhichChangesLevel) >> + logger.warn(warnMsg1Pos1)(implicitly, positionWhichChangesLevel) >> + logger.info(infoMsg2Pos2)(implicitly, position2) + } + messages + .map(_.map(_.message.value).toList) + .assertEquals(List(infoMsg1Pos1, warnMsg1Pos1, infoMsg2Pos2)) } } - def runTest( + private def runTest(initialConfig: RuntimeConfig = defaultConfig)( useLogger: DynamicOdinConsoleLogger[IO] => IO[Unit] ): IO[Queue[LoggerMessage]] = (for { testLogger <- Resource.eval(OdinRefLogger.create[IO]()) dynamic <- DynamicOdinConsoleLogger.create[IO]( DynamicOdinConsoleLogger .Config(formatter = Formatter.default, asyncTimeWindow = 0.nanos), - DynamicOdinConsoleLogger.RuntimeConfig(Level.Info) - )(config => testLogger.withMinimalLevel(config.minLevel)) + initialConfig + )(config => testLogger.withMinimalLevel(config.defaultLevel)) _ <- Resource.eval(useLogger(dynamic)) } yield testLogger) .use { testLogger =>