-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5f2022a
commit ae13d53
Showing
5 changed files
with
264 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
odin-dynamic/src/main/scala/com/permutive/logging/dynamic/odin/DynamicOdinLogger.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
odin-dynamic/src/test/scala/com/permutive/logging/dynamic/odin/DynamicOdinLoggerSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
odin-testing/src/main/scala/com/permutive/logging/odin/testing/OdinRefLogger.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |