From 378f79fe953f807ab4a6ec0492deb28ec00f8034 Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Sun, 27 Oct 2024 23:12:48 -0700 Subject: [PATCH] [core] Log improvements (#783) Following up on https://github.com/getkyo/kyo/pull/780#discussion_r1818130462 --- .../kyo/internal/LogPlatformSpecific.scala | 2 +- .../kyo/internal/LogPlatformSpecific.scala | 38 +++-- .../shared/src/main/scala/kyo/KyoApp.scala | 6 +- kyo-core/shared/src/main/scala/kyo/Log.scala | 140 ++++++++++++------ .../main/scala/kyo/scheduler/IOPromise.scala | 6 +- .../src/test/scala/kyo/KyoAppTest.scala | 6 +- .../shared/src/test/scala/kyo/LogTest.scala | 46 ++++-- .../main/scala/examples/ledger/db/DB.scala | 2 +- 8 files changed, 162 insertions(+), 84 deletions(-) diff --git a/kyo-core/js/src/main/scala/kyo/internal/LogPlatformSpecific.scala b/kyo-core/js/src/main/scala/kyo/internal/LogPlatformSpecific.scala index fb2ffde70..c8a57bae4 100644 --- a/kyo-core/js/src/main/scala/kyo/internal/LogPlatformSpecific.scala +++ b/kyo-core/js/src/main/scala/kyo/internal/LogPlatformSpecific.scala @@ -3,4 +3,4 @@ package kyo.internal import kyo.Log trait LogPlatformSpecific: - val unsafe: Log.Unsafe = Log.Unsafe.ConsoleLogger("kyo.logs") + val live: Log = Log(Log.Unsafe.ConsoleLogger("kyo.logs", Log.Level.debug)) diff --git a/kyo-core/jvm/src/main/scala/kyo/internal/LogPlatformSpecific.scala b/kyo-core/jvm/src/main/scala/kyo/internal/LogPlatformSpecific.scala index 997f1d34f..7964b1b77 100644 --- a/kyo-core/jvm/src/main/scala/kyo/internal/LogPlatformSpecific.scala +++ b/kyo-core/jvm/src/main/scala/kyo/internal/LogPlatformSpecific.scala @@ -3,9 +3,10 @@ package kyo.internal import kyo.AllowUnsafe import kyo.Frame import kyo.Log +import kyo.Log.Level trait LogPlatformSpecific: - val unsafe: Log.Unsafe = LogPlatformSpecific.Unsafe.SLF4J("kyo.logs") + val live: Log = Log(LogPlatformSpecific.Unsafe.SLF4J("kyo.logs")) object LogPlatformSpecific: @@ -16,45 +17,42 @@ object LogPlatformSpecific: def apply(name: String) = new SLF4J(org.slf4j.LoggerFactory.getLogger(name)) class SLF4J(logger: org.slf4j.Logger) extends Log.Unsafe: - inline def traceEnabled: Boolean = logger.isTraceEnabled - - inline def debugEnabled: Boolean = logger.isDebugEnabled - - inline def infoEnabled: Boolean = logger.isInfoEnabled - - inline def warnEnabled: Boolean = logger.isWarnEnabled - - inline def errorEnabled: Boolean = logger.isErrorEnabled + def level = + if logger.isTraceEnabled() then Level.trace + else if logger.isDebugEnabled() then Level.debug + else if logger.isInfoEnabled() then Level.info + else if logger.isWarnEnabled() then Level.warn + else Level.error inline def trace(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit = - if traceEnabled then logger.trace(s"[${frame.parse.position}] $msg") + if Level.trace.enabled(level) then logger.trace(s"[${frame.parse.position}] $msg") inline def trace(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit = - if traceEnabled then logger.trace(s"[${frame.parse.position}] $msg", t) + if Level.trace.enabled(level) then logger.trace(s"[${frame.parse.position}] $msg", t) inline def debug(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit = - if debugEnabled then logger.debug(s"[${frame.parse.position}] $msg") + if Level.debug.enabled(level) then logger.debug(s"[${frame.parse.position}] $msg") inline def debug(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit = - if debugEnabled then logger.debug(s"[${frame.parse.position}] $msg", t) + if Level.debug.enabled(level) then logger.debug(s"[${frame.parse.position}] $msg", t) inline def info(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit = - if infoEnabled then logger.info(s"[${frame.parse.position}] $msg") + if Level.info.enabled(level) then logger.info(s"[${frame.parse.position}] $msg") inline def info(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit = - if infoEnabled then logger.info(s"[${frame.parse.position}] $msg", t) + if Level.info.enabled(level) then logger.info(s"[${frame.parse.position}] $msg", t) inline def warn(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit = - if warnEnabled then logger.warn(s"[${frame.parse.position}] $msg") + if Level.warn.enabled(level) then logger.warn(s"[${frame.parse.position}] $msg") inline def warn(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit = - if warnEnabled then logger.warn(s"[${frame.parse.position}] $msg", t) + if Level.warn.enabled(level) then logger.warn(s"[${frame.parse.position}] $msg", t) inline def error(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit = - if errorEnabled then logger.error(s"[${frame.parse.position}] $msg") + if Level.error.enabled(level) then logger.error(s"[${frame.parse.position}] $msg") inline def error(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit = - if errorEnabled then logger.error(s"[${frame.parse.position}] $msg", t) + if Level.error.enabled(level) then logger.error(s"[${frame.parse.position}] $msg", t) end SLF4J end Unsafe end LogPlatformSpecific diff --git a/kyo-core/shared/src/main/scala/kyo/KyoApp.scala b/kyo-core/shared/src/main/scala/kyo/KyoApp.scala index 0133adf40..2149ad078 100644 --- a/kyo-core/shared/src/main/scala/kyo/KyoApp.scala +++ b/kyo-core/shared/src/main/scala/kyo/KyoApp.scala @@ -10,9 +10,9 @@ import scala.collection.mutable.ListBuffer * Note: This class and its methods are unsafe and should only be used as the entrypoint of an application. */ abstract class KyoApp extends KyoApp.Base[KyoApp.Effects]: - def log: Log.Unsafe = Log.unsafe - def random: Random = Random.live - def clock: Clock = Clock.live + def log: Log = Log.live + def random: Random = Random.live + def clock: Clock = Clock.live override protected def handle[A: Flat](v: A < KyoApp.Effects)(using Frame): Unit = import AllowUnsafe.embrace.danger diff --git a/kyo-core/shared/src/main/scala/kyo/Log.scala b/kyo-core/shared/src/main/scala/kyo/Log.scala index 7fda6dbd0..8bd707c37 100644 --- a/kyo-core/shared/src/main/scala/kyo/Log.scala +++ b/kyo-core/shared/src/main/scala/kyo/Log.scala @@ -2,10 +2,35 @@ package kyo import kyo.internal.LogPlatformSpecific +final case class Log(unsafe: Log.Unsafe) extends AnyVal: + def level: Log.Level = unsafe.level + inline def trace(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.trace(msg)) + inline def trace(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.trace(msg, t)) + inline def debug(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.debug(msg)) + inline def debug(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.debug(msg, t)) + inline def info(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.info(msg)) + inline def info(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.info(msg, t)) + inline def warn(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.warn(msg)) + inline def warn(inline msg: => String, t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.warn(msg, t)) + inline def error(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.error(msg)) + inline def error(inline msg: => String, t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.error(msg, t)) +end Log + /** Logging utility object for Kyo applications. */ object Log extends LogPlatformSpecific: - private val local = Local.init[Unsafe](unsafe) + final class Level private (private val priority: Int) extends AnyVal: + def enabled(minLevel: Level) = priority >= minLevel.priority + + object Level: + val trace: Level = Level(10) + val debug: Level = Level(20) + val info: Level = Level(30) + val warn: Level = Level(40) + val error: Level = Level(50) + end Level + + private val local = Local.init(live) /** Executes a function with a custom Unsafe logger. * @@ -16,16 +41,52 @@ object Log extends LogPlatformSpecific: * @return * The result of the function execution */ - def let[A, S](u: Unsafe)(f: => A < (IO & S))(using Frame): A < (IO & S) = - local.let(u)(f) + def let[A, S](log: Log)(f: => A < S)(using Frame): A < S = + local.let(log)(f) + + /** Gets the current logger from the local context. + * + * @return + * The current Log instance wrapped in an effect + */ + def get(using Frame): Log < Any = local.get + + /** Executes a function with access to the current logger. + * + * @param f + * The function to execute, which takes a Log instance as input + * @return + * The result of the function execution + */ + def use[A, S](f: Log => A < S)(using Frame): A < S = local.use(f) + + /** Executes an effect with a console logger using the default name "kyo.logs" and debug level. + * + * @param v + * The effect to execute with the console logger + * @return + * The result of executing the effect with the console logger + */ + def withConsoleLogger[A, S](v: A < S)(using Frame): A < S = + withConsoleLogger()(v) + + /** Executes an effect with a console logger using a custom name and log level. + * + * @param name + * The name to use for the console logger + * @param level + * The log level + * @param v + * The effect to execute with the console logger + * @return + * The result of executing the effect with the console logger + */ + def withConsoleLogger[A, S](name: String = "kyo.logs", level: Level = Level.debug)(v: A < S)(using Frame): A < S = + let(Log(Unsafe.ConsoleLogger(name, level)))(v) /** WARNING: Low-level API meant for integrations, libraries, and performance-sensitive code. See AllowUnsafe for more details. */ abstract class Unsafe: - def traceEnabled: Boolean - def debugEnabled: Boolean - def infoEnabled: Boolean - def warnEnabled: Boolean - def errorEnabled: Boolean + def level: Level def trace(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit def trace(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit @@ -37,89 +98,82 @@ object Log extends LogPlatformSpecific: def warn(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit def error(msg: => String)(using frame: Frame, allow: AllowUnsafe): Unit def error(msg: => String, t: => Throwable)(using frame: Frame, allow: AllowUnsafe): Unit + + def safe: Log = Log(this) end Unsafe /** WARNING: Low-level API meant for integrations, libraries, and performance-sensitive code. See AllowUnsafe for more details. */ object Unsafe: - class ConsoleLogger(name: String) extends Log.Unsafe: - inline def traceEnabled: Boolean = true - - inline def debugEnabled: Boolean = true - - inline def infoEnabled: Boolean = true - - inline def warnEnabled: Boolean = true - - inline def errorEnabled: Boolean = true - + case class ConsoleLogger(name: String, level: Level) extends Log.Unsafe: inline def trace(msg: => String)( using frame: Frame, allow: AllowUnsafe - ): Unit = if traceEnabled then println(s"TRACE $name -- [${frame.parse.position}] $msg") + ): Unit = if Level.trace.enabled(level) then println(s"TRACE $name -- [${frame.parse.position}] $msg") inline def trace(msg: => String, t: => Throwable)( using frame: Frame, allow: AllowUnsafe - ): Unit = if traceEnabled then println(s"TRACE $name -- [${frame.parse.position}] $msg $t") + ): Unit = if Level.trace.enabled(level) then println(s"TRACE $name -- [${frame.parse.position}] $msg $t") inline def debug(msg: => String)( using frame: Frame, allow: AllowUnsafe - ): Unit = if debugEnabled then println(s"DEBUG $name -- [${frame.parse.position}] $msg") + ): Unit = + if Level.debug.enabled(level) then println(s"DEBUG $name -- [${frame.parse.position}] $msg") inline def debug(msg: => String, t: => Throwable)( using frame: Frame, allow: AllowUnsafe - ): Unit = if debugEnabled then println(s"DEBUG $name -- [${frame.parse.position}] $msg $t") + ): Unit = if Level.debug.enabled(level) then println(s"DEBUG $name -- [${frame.parse.position}] $msg $t") inline def info(msg: => String)( using frame: Frame, allow: AllowUnsafe - ): Unit = if infoEnabled then println(s"INFO $name -- [${frame.parse.position}] $msg") + ): Unit = if Level.info.enabled(level) then println(s"INFO $name -- [${frame.parse.position}] $msg") inline def info(msg: => String, t: => Throwable)( using frame: Frame, allow: AllowUnsafe - ): Unit = if infoEnabled then println(s"INFO $name -- [${frame.parse.position}] $msg $t") + ): Unit = if Level.info.enabled(level) then println(s"INFO $name -- [${frame.parse.position}] $msg $t") inline def warn(msg: => String)( using frame: Frame, allow: AllowUnsafe - ): Unit = if warnEnabled then println(s"WARN $name -- [${frame.parse.position}] $msg") + ): Unit = if Level.warn.enabled(level) then println(s"WARN $name -- [${frame.parse.position}] $msg") inline def warn(msg: => String, t: => Throwable)( using frame: Frame, allow: AllowUnsafe - ): Unit = if warnEnabled then println(s"WARN $name -- [${frame.parse.position}] $msg $t") + ): Unit = if Level.warn.enabled(level) then println(s"WARN $name -- [${frame.parse.position}] $msg $t") inline def error(msg: => String)( using frame: Frame, allow: AllowUnsafe - ): Unit = if errorEnabled then println(s"ERROR $name -- [${frame.parse.position}] $msg") + ): Unit = if Level.error.enabled(level) then println(s"ERROR $name -- [${frame.parse.position}] $msg") inline def error(msg: => String, t: => Throwable)( using frame: Frame, allow: AllowUnsafe - ): Unit = if errorEnabled then println(s"ERROR $name -- [${frame.parse.position}] $msg $t") + ): Unit = if Level.error.enabled(level) then println(s"ERROR $name -- [${frame.parse.position}] $msg $t") end ConsoleLogger end Unsafe - private inline def logWhen(inline enabled: Unsafe => Boolean)(inline log: AllowUnsafe ?=> Unsafe => Unit)(using + private inline def logWhen(inline level: Level)(inline doLog: Log => Unit < IO)(using inline frame: Frame ): Unit < IO = - local.use { unsafe => - if enabled(unsafe) then - IO.Unsafe(log(unsafe)) + use { log => + if level.enabled(log.level) then + IO.Unsafe(doLog(log)) else ( ) @@ -133,7 +187,7 @@ object Log extends LogPlatformSpecific: * An IO effect that logs the message */ inline def trace(inline msg: => String)(using inline frame: Frame): Unit < IO = - logWhen(_.traceEnabled)(_.trace(msg)) + logWhen(Level.trace)(_.trace(msg)) /** Logs a trace message with an exception. * @@ -145,7 +199,7 @@ object Log extends LogPlatformSpecific: * An IO effect that logs the message and exception */ inline def trace(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = - logWhen(_.traceEnabled)(_.trace(msg, t)) + logWhen(Level.trace)(_.trace(msg, t)) /** Logs a debug message. * @@ -155,7 +209,7 @@ object Log extends LogPlatformSpecific: * An IO effect that logs the message */ inline def debug(inline msg: => String)(using inline frame: Frame): Unit < IO = - logWhen(_.debugEnabled)(_.debug(msg)) + logWhen(Level.debug)(_.debug(msg)) /** Logs a debug message with an exception. * @@ -167,7 +221,7 @@ object Log extends LogPlatformSpecific: * An IO effect that logs the message and exception */ inline def debug(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = - logWhen(_.debugEnabled)(_.debug(msg, t)) + logWhen(Level.debug)(_.debug(msg, t)) /** Logs an info message. * @@ -177,7 +231,7 @@ object Log extends LogPlatformSpecific: * An IO effect that logs the message */ inline def info(inline msg: => String)(using inline frame: Frame): Unit < IO = - logWhen(_.infoEnabled)(_.info(msg)) + logWhen(Level.info)(_.info(msg)) /** Logs an info message with an exception. * @@ -189,7 +243,7 @@ object Log extends LogPlatformSpecific: * An IO effect that logs the message and exception */ inline def info(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = - logWhen(_.infoEnabled)(_.info(msg, t)) + logWhen(Level.info)(_.info(msg, t)) /** Logs a warning message. * @@ -199,7 +253,7 @@ object Log extends LogPlatformSpecific: * An IO effect that logs the message */ inline def warn(inline msg: => String)(using inline frame: Frame): Unit < IO = - logWhen(_.warnEnabled)(_.warn(msg)) + logWhen(Level.warn)(_.warn(msg)) /** Logs a warning message with an exception. * @@ -211,7 +265,7 @@ object Log extends LogPlatformSpecific: * An IO effect that logs the message and exception */ inline def warn(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = - logWhen(_.warnEnabled)(_.warn(msg, t)) + logWhen(Level.warn)(_.warn(msg, t)) /** Logs an error message. * @@ -221,7 +275,7 @@ object Log extends LogPlatformSpecific: * An IO effect that logs the message */ inline def error(inline msg: => String)(using inline frame: Frame): Unit < IO = - logWhen(_.errorEnabled)(_.error(msg)) + logWhen(Level.error)(_.error(msg)) /** Logs an error message with an exception. * @@ -233,6 +287,6 @@ object Log extends LogPlatformSpecific: * An IO effect that logs the message and exception */ inline def error(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = - logWhen(_.errorEnabled)(_.error(msg, t)) + logWhen(Level.error)(_.error(msg, t)) end Log diff --git a/kyo-core/shared/src/main/scala/kyo/scheduler/IOPromise.scala b/kyo-core/shared/src/main/scala/kyo/scheduler/IOPromise.scala index acab95321..4ed298699 100644 --- a/kyo-core/shared/src/main/scala/kyo/scheduler/IOPromise.scala +++ b/kyo-core/shared/src/main/scala/kyo/scheduler/IOPromise.scala @@ -58,7 +58,7 @@ private[kyo] class IOPromise[+E, +A](init: State[E, A]) extends Safepoint.Interc catch case ex if NonFatal(ex) => import AllowUnsafe.embrace.danger - Log.unsafe.error("uncaught exception", ex) + Log.live.unsafe.error("uncaught exception", ex) interruptsLoop(this) end interrupts @@ -135,7 +135,7 @@ private[kyo] class IOPromise[+E, +A](init: State[E, A]) extends Safepoint.Interc case ex if NonFatal(ex) => given Frame = Frame.internal import AllowUnsafe.embrace.danger - Log.unsafe.error("uncaught exception", ex) + Log.live.unsafe.error("uncaught exception", ex) onCompleteLoop(this) end onComplete @@ -256,7 +256,7 @@ private[kyo] object IOPromise extends IOPromisePlatformSpecific: case ex if NonFatal(ex) => given Frame = Frame.internal import AllowUnsafe.embrace.danger - Log.unsafe.error("uncaught exception", ex) + Log.live.unsafe.error("uncaught exception", ex) end try self end run diff --git a/kyo-core/shared/src/test/scala/kyo/KyoAppTest.scala b/kyo-core/shared/src/test/scala/kyo/KyoAppTest.scala index 454f1bea4..883edd18c 100644 --- a/kyo-core/shared/src/test/scala/kyo/KyoAppTest.scala +++ b/kyo-core/shared/src/test/scala/kyo/KyoAppTest.scala @@ -110,9 +110,9 @@ class KyoAppTest extends Test: override def unsafe = ??? app = new KyoApp: - override val log: Log.Unsafe = Log.Unsafe.ConsoleLogger("ConsoleLogger") - override val clock: Clock = testClock - override val random: Random = testRandom + override val log: Log = Log(Log.Unsafe.ConsoleLogger("ConsoleLogger", Log.Level.debug)) + override val clock: Clock = testClock + override val random: Random = testRandom run { for _ <- Clock.now.map(i => instantRef.update(_ => i)) diff --git a/kyo-core/shared/src/test/scala/kyo/LogTest.scala b/kyo-core/shared/src/test/scala/kyo/LogTest.scala index 390712e0d..84a8409d9 100644 --- a/kyo-core/shared/src/test/scala/kyo/LogTest.scala +++ b/kyo-core/shared/src/test/scala/kyo/LogTest.scala @@ -24,16 +24,42 @@ class LogTest extends Test: "unsafe" in { import AllowUnsafe.embrace.danger - Log.unsafe.trace("trace") - Log.unsafe.debug("debug") - Log.unsafe.info("info") - Log.unsafe.warn("warn") - Log.unsafe.error("error") - Log.unsafe.trace("trace", ex) - Log.unsafe.debug("debug", ex) - Log.unsafe.info("info", ex) - Log.unsafe.warn("warn", ex) - Log.unsafe.error("error", ex) + Log.live.unsafe.trace("trace") + Log.live.unsafe.debug("debug") + Log.live.unsafe.info("info") + Log.live.unsafe.warn("warn") + Log.live.unsafe.error("error") + Log.live.unsafe.trace("trace", ex) + Log.live.unsafe.debug("debug", ex) + Log.live.unsafe.info("info", ex) + Log.live.unsafe.warn("warn", ex) + Log.live.unsafe.error("error", ex) succeed } + + "withConsoleLogger" in { + val output = new StringBuilder + scala.Console.withOut(new java.io.PrintStream(new java.io.OutputStream: + override def write(b: Int): Unit = output.append(b.toChar) + )) { + import AllowUnsafe.embrace.danger + IO.Unsafe.run { + for + _ <- Log.withConsoleLogger("test.logger", Log.Level.debug) { + for + _ <- Log.trace("won't show up") + _ <- Log.debug("test message") + _ <- Log.info("info message") + _ <- Log.warn("warning", new Exception("test exception")) + yield () + } + yield + val logs = output.toString.trim.split("\n") + assert(logs.length == 3) + assert(logs(0).matches("DEBUG test.logger -- \\[.*\\] test message")) + assert(logs(1).matches("INFO test.logger -- \\[.*\\] info message")) + assert(logs(2).matches("WARN test.logger -- \\[.*\\] warning java.lang.Exception: test exception")) + }.eval + } + } end LogTest diff --git a/kyo-examples/jvm/src/main/scala/examples/ledger/db/DB.scala b/kyo-examples/jvm/src/main/scala/examples/ledger/db/DB.scala index b9b215d9d..b53f7eb85 100644 --- a/kyo-examples/jvm/src/main/scala/examples/ledger/db/DB.scala +++ b/kyo-examples/jvm/src/main/scala/examples/ledger/db/DB.scala @@ -30,7 +30,7 @@ object DB: Live(index, log) } - class Live(index: Index, log: Log) extends DB: + class Live(index: Index, log: db.Log) extends DB: def transaction(account: Int, amount: Int, desc: String): Result < IO = index.transaction(account, amount, desc).map {