Skip to content

Commit

Permalink
Add dynamic and testing loggers
Browse files Browse the repository at this point in the history
  • Loading branch information
janstenpickle committed Dec 15, 2022
1 parent 5f2022a commit ae13d53
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 8 deletions.
34 changes: 31 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,46 @@ val CatsEffect = "3.4.2"
val Odin = "0.13.0"

val Scala213 = "2.13.10"
ThisBuild / crossScalaVersions := Seq(Scala213, "3.1.1")
ThisBuild / crossScalaVersions := Seq(Scala213, "3.2.1")
ThisBuild / scalaVersion := Scala213 // the default Scala

lazy val root = tlCrossRootProject.aggregate(slf4JBridge)
lazy val root = tlCrossRootProject.aggregate(dynamic, testing, slf4JBridge)

lazy val dynamic = project
.in(file("odin-dynamic"))
.settings(
name := "odin-dynamic",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % Cats,
"org.typelevel" %% "kittens" % "3.0.0",
"org.typelevel" %% "cats-effect-kernel" % CatsEffect,
"com.github.valskalla" %% "odin-core" % Odin,
"org.typelevel" %% "cats-effect" % CatsEffect % Test,
"org.typelevel" %% "cats-effect-testkit" % CatsEffect % Test,
"org.typelevel" %% "scalacheck-effect-munit" % "1.0.4" % Test,
"org.typelevel" %% "munit-cats-effect-3" % "1.0.7" % Test
)
)
.dependsOn(testing % "compile->test")

lazy val testing = project
.in(file("odin-testing"))
.settings(
name := "odin-testing",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % Cats,
"org.typelevel" %% "cats-effect-kernel" % CatsEffect,
"com.github.valskalla" %% "odin-core" % Odin
)
)

lazy val slf4JBridge = project
.in(file("odin-slf4j-bridge"))
.settings(
name := "odin-slf4j-bridge",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % Cats,
"org.typelevel" %% "cats-effect" % CatsEffect,
"org.typelevel" %% "cats-effect-std" % CatsEffect,
"com.github.valskalla" %% "odin-core" % Odin,
"org.slf4j" % "slf4j-api" % "1.7.36"
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.permutive.logging.dynamic.odin

import cats.effect.kernel._
import cats.kernel.Eq
import cats.syntax.eq._
import cats.syntax.flatMap._
import cats.syntax.functor._
import cats.{Applicative, Monad}
import com.permutive.logging.dynamic.odin.DynamicOdinConsoleLogger.RuntimeConfig
import io.odin.config.enclosureRouting
import io.odin.formatter.Formatter
import io.odin.loggers.DefaultLogger
import io.odin.syntax._
import io.odin.{consoleLogger, Level, Logger, LoggerMessage}

import scala.concurrent.duration._

trait DynamicOdinConsoleLogger[F[_]] extends Logger[F] {
def update(config: RuntimeConfig): F[Boolean]
def getConfig: F[RuntimeConfig]
}

class DynamicOdinConsoleLoggerImpl[F[_]: Monad: Clock] private[odin] (
ref: Ref[F, (RuntimeConfig, Logger[F])],
level: Level
)(make: RuntimeConfig => Logger[F])(implicit eq: Eq[RuntimeConfig])
extends DefaultLogger[F](level)
with DynamicOdinConsoleLogger[F] { outer =>
protected def withLogger(f: Logger[F] => F[Unit]): F[Unit] = ref.get.flatMap {
case (_, l) => f(l)
}

override def update(config: RuntimeConfig): F[Boolean] =
ref.get
.map(_._1.neqv(config))
.flatTap(Applicative[F].whenA(_)(ref.set(config -> make(config))))

override def getConfig: F[RuntimeConfig] = ref.get.map(_._1)

override def submit(msg: LoggerMessage): F[Unit] = withLogger(_.log(msg))

override def withMinimalLevel(level: Level): Logger[F] =
new DynamicOdinConsoleLoggerImpl[F](ref, level)(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
)

case class RuntimeConfig(
minLevel: Level,
levelMapping: Map[String, Level] = Map.empty
)
object RuntimeConfig {
implicit val eq: Eq[RuntimeConfig] = 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)
)

def create[F[_]: Async](
config: Config,
runtimeConfig: RuntimeConfig
)(
make: RuntimeConfig => Logger[F]
)(implicit
eq: Eq[RuntimeConfig]
): Resource[F, DynamicOdinConsoleLogger[F]] = {
val makeWithLevels: RuntimeConfig => Logger[F] = { config =>
val mainLogger = make(config)

if (config.levelMapping.isEmpty) mainLogger
else
enclosureRouting(
config.levelMapping.view
.mapValues(mainLogger.withMinimalLevel)
.toList: _*
)
.withFallback(mainLogger)
}

for {
ref <- Resource.eval(
Ref.of[F, (RuntimeConfig, Logger[F])](
runtimeConfig -> makeWithLevels(runtimeConfig)
)
)
underlying = new DynamicOdinConsoleLoggerImpl[F](
ref,
runtimeConfig.minLevel
)(makeWithLevels)
async <- underlying.withAsync(
config.asyncTimeWindow,
config.asyncMaxBufferSize
)
} yield new DefaultLogger[F](async.minLevel)
with DynamicOdinConsoleLogger[F] {
override def submit(msg: LoggerMessage): F[Unit] = async.log(msg)

override def update(config: RuntimeConfig): F[Boolean] =
underlying.update(config)
override def getConfig: F[RuntimeConfig] = underlying.getConfig

override def withMinimalLevel(level: Level): Logger[F] =
async.withMinimalLevel(level)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.permutive.logging.dynamic.odin

import cats.effect.unsafe.IORuntime
import cats.effect.{IO, Resource}
import com.permutive.logging.odin.testing.OdinRefLogger
import io.odin.{Level, LoggerMessage}
import io.odin.formatter.Formatter
import munit.{CatsEffectSuite, ScalaCheckEffectSuite}
import org.scalacheck.effect.PropF

import scala.collection.immutable.Queue
import scala.concurrent.duration._

class DynamicOdinLoggerSpec extends CatsEffectSuite with ScalaCheckEffectSuite {

implicit val runtime = IORuntime.global

test("record a message") {
PropF.forAllF { message: String =>
val messages = runTest(_.info(message))

messages.map(_.map(_.message.value).toList).assertEquals(List(message))
}
}

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
)
}
messages.map(_.map(_.message.value).toList).assertEquals(List(message1))
}
}

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)
}
messages
.map(_.map(_.message.value).toList)
.assertEquals(List(message1, message3))
}
}

def runTest(
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))
_ <- Resource.eval(useLogger(dynamic))
} yield testLogger)
.use { testLogger =>
IO.sleep(50.millis) >> testLogger.getMessages
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import io.odin.{Level, Logger}

import scala.util.control.NonFatal

/**
* Utilities for getting a logger that can be used by SLF4J.
/** Utilities for getting a logger that can be used by SLF4J.
*/
object GlobalLogger {
private val default: OdinTranslator = OdinTranslator.unsafeConsole(level = Level.Info, Formatter.default)
private val default: OdinTranslator =
OdinTranslator.unsafeConsole(level = Level.Info, Formatter.default)

private val ref = new AtomicReference[OdinTranslator]

Expand All @@ -28,7 +28,12 @@ object GlobalLogger {
} catch {
case NonFatal(th) =>
// Use default logger if there is an error getting the logger from the atomic ref
default.run(this.getClass.getName, Level.Error, "Failed to get logger from atomic ref", Some(th))
default.run(
this.getClass.getName,
Level.Error,
"Failed to get logger from atomic ref",
Some(th)
)
default
}

Expand All @@ -40,7 +45,11 @@ object GlobalLogger {
val set = Dispatcher
.sequential[F]
.map(implicit dis => OdinTranslator[F](logger))
.flatMap(log => Resource.make(Sync[F].delay(ref.set(log)))(_ => Sync[F].delay(ref.set(null))))
.flatMap(log =>
Resource.make(Sync[F].delay(ref.set(log)))(_ =>
Sync[F].delay(ref.set(null))
)
)

val error: Resource[F, Unit] = new RuntimeException(
"Logger already set: Cannot set the global logger multiple times"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.permutive.logging.odin.testing

import cats.Monad
import cats.effect.kernel.{Clock, Ref}
import io.odin.{Level, Logger, LoggerMessage}
import io.odin.loggers.DefaultLogger
import cats.syntax.functor._

import scala.collection.immutable.Queue

class OdinRefLogger[F[_]: Clock: Monad] private (
minLevel: Level,
private val ref: Ref[F, Queue[LoggerMessage]]
) extends DefaultLogger[F](minLevel) {
def getMessages: F[Queue[LoggerMessage]] = ref.get

override def submit(msg: LoggerMessage): F[Unit] = ref.update(_.appended(msg))

override def withMinimalLevel(level: Level): Logger[F] =
new OdinRefLogger[F](level, ref)
}

object OdinRefLogger {
def create[F[_]: Monad: Clock: Ref.Make](
minLevel: Level = Level.Info
): F[OdinRefLogger[F]] =
Ref.of(Queue.empty[LoggerMessage]).map { ref =>
new OdinRefLogger[F](minLevel, ref)
}
}

0 comments on commit ae13d53

Please sign in to comment.