From b334b94b2c1b43579ba5d591c140072da41fb814 Mon Sep 17 00:00:00 2001 From: Kevin Lee Date: Sat, 29 Jul 2023 17:16:45 +1000 Subject: [PATCH] Close #461 - Add LoggerFLogHandler to support doobie's LogHandler --- .github/workflows/build.yml | 6 +- .github/workflows/coverage.yml | 2 +- .github/workflows/release.yml | 6 +- build.sbt | 43 +++-- .../main/scala-3/loggerf/instances/show.scala | 4 +- .../src/main/scala-3/loggerf/core/ToLog.scala | 4 +- .../loggerf/doobie1/LoggerFLogHandler.scala | 58 ++++++ .../doobie1/LoggerFLogHandlerSpec.scala | 166 ++++++++++++++++++ .../logger/logback/Monix3MdcAdapter.scala | 8 +- .../logger/logback/Monix3MdcAdapterSpec.scala | 10 +- project/plugins.sbt | 16 +- 11 files changed, 287 insertions(+), 36 deletions(-) create mode 100644 modules/logger-f-doobie1/shared/src/main/scala/loggerf/doobie1/LoggerFLogHandler.scala create mode 100644 modules/logger-f-doobie1/shared/src/test/scala/loggerf/doobie1/LoggerFLogHandlerSpec.scala diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98e9c905..89a1b1bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,9 +19,9 @@ jobs: strategy: matrix: scala: - - { name: "Scala 2", version: "2.12.13", binary-version: "2.12", java-version: "11", java-distribution: "temurin", report: "" } - - { name: "Scala 2", version: "2.13.6", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "report" } - - { name: "Scala 3", version: "3.0.2", binary-version: "3", java-version: "11", java-distribution: "temurin", report: "" } + - { name: "Scala 2", version: "2.12.18", binary-version: "2.12", java-version: "11", java-distribution: "temurin", report: "" } + - { name: "Scala 2", version: "2.13.11", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "report" } + - { name: "Scala 3", version: "3.3.0", binary-version: "3", java-version: "11", java-distribution: "temurin", report: "" } steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index aae17477..75dcc50c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: scala: - - { name: "Scala 2", version: "2.13.6", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "report" } + - { name: "Scala 2", version: "2.13.11", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "report" } steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1f7bafa..7f887c16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,9 +22,9 @@ jobs: strategy: matrix: scala: - - { name: "Scala 2", version: "2.12.13", binary-version: "2.12", java-version: "11", java-distribution: "temurin", report: "" } - - { name: "Scala 2", version: "2.13.6", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "" } - - { name: "Scala 3", version: "3.0.2", binary-version: "3", java-version: "11", java-distribution: "temurin", report: "" } + - { name: "Scala 2", version: "2.12.18", binary-version: "2.12", java-version: "11", java-distribution: "temurin", report: "" } + - { name: "Scala 2", version: "2.13.11", binary-version: "2.13", java-version: "11", java-distribution: "temurin", report: "" } + - { name: "Scala 3", version: "3.3.0", binary-version: "3", java-version: "11", java-distribution: "temurin", report: "" } steps: - uses: actions/checkout@v4 diff --git a/build.sbt b/build.sbt index 9bf094c5..3e995edf 100644 --- a/build.sbt +++ b/build.sbt @@ -83,6 +83,8 @@ lazy val loggerF = (project in file(".")) catsJs, logbackMdcMonix3Jvm, logbackMdcMonix3Js, + doobie1Jvm, + doobie1Js, testKitJvm, testKitJs, catsEffectJvm, @@ -276,6 +278,28 @@ lazy val logbackMdcMonix3 = module(ProjectName("logback-mdc-monix3"), crossPr lazy val logbackMdcMonix3Jvm = logbackMdcMonix3.jvm lazy val logbackMdcMonix3Js = logbackMdcMonix3.js +lazy val doobie1 = module(ProjectName("doobie1"), crossProject(JVMPlatform, JSPlatform)) + .settings( + description := "Logger for F[_] - for Doobie v1", + libraryDependencies ++= Seq( + libs.doobieFree, + libs.tests.effectieCatsEffect3, + libs.tests.extrasHedgehogCatsEffect3, + ) ++ libs.tests.hedgehogLibs, + libraryDependencies := libraryDependenciesRemoveScala3Incompatible( + scalaVersion.value, + libraryDependencies.value, + ), + ) + .dependsOn( + core, + cats, + testKit % Test, + slf4jLogger % Test, + ) +lazy val doobie1Jvm = doobie1.jvm +lazy val doobie1Js = doobie1.js + lazy val testKit = module(ProjectName("test-kit"), crossProject(JVMPlatform, JSPlatform)) .settings( @@ -514,8 +538,8 @@ lazy val props = final val GitHubUsername = "Kevin-Lee" final val RepoName = "logger-f" - final val Scala3Versions = List("3.0.2") - final val Scala2Versions = List("2.13.6", "2.12.13") + final val Scala3Versions = List("3.3.0") + final val Scala2Versions = List("2.13.11", "2.12.18") // final val ProjectScalaVersion = Scala3Versions.head final val ProjectScalaVersion = Scala2Versions.head @@ -550,6 +574,8 @@ lazy val props = val Monix3Version = "3.4.0" + val Doobie1Version = "1.0.0-RC4" + final val LoggerF1Version = "1.20.0" final val ExtrasVersion = "0.25.0" @@ -593,10 +619,14 @@ lazy val libs = lazy val logbackScalaInterop = "io.kevinlee" % "logback-scala-interop" % props.LogbackScalaInteropVersion + lazy val doobieFree = "org.tpolecat" %% "doobie-free" % props.Doobie1Version + lazy val tests = new { lazy val monix = "io.monix" %% "monix" % props.Monix3Version % Test + lazy val effectieCatsEffect3 = "io.kevinlee" %% "effectie-cats-effect3" % props.EffectieVersion % Test + lazy val effectieMonix3 = "io.kevinlee" %% "effectie-monix3" % props.EffectieVersion % Test lazy val hedgehogLibs: List[ModuleID] = List( @@ -625,14 +655,7 @@ def prefixedProjectName(name: String) = s"${props.RepoName}${if (name.isEmpty) " def libraryDependenciesRemoveScala3Incompatible( scalaVersion: String, libraries: Seq[ModuleID], -): Seq[ModuleID] = - ( - if (scalaVersion.startsWith("3.")) - libraries - .filterNot(props.removeDottyIncompatible) - else - libraries - ) +): Seq[ModuleID] = libraries lazy val mavenCentralPublishSettings: SettingsDefinition = List( /* Publish to Maven Central { */ diff --git a/modules/logger-f-cats/shared/src/main/scala-3/loggerf/instances/show.scala b/modules/logger-f-cats/shared/src/main/scala-3/loggerf/instances/show.scala index 4b17a820..aa0288be 100644 --- a/modules/logger-f-cats/shared/src/main/scala-3/loggerf/instances/show.scala +++ b/modules/logger-f-cats/shared/src/main/scala-3/loggerf/instances/show.scala @@ -7,7 +7,9 @@ import loggerf.core.ToLog * @since 2022-02-19 */ trait show { - inline given showToLog[A: Show]: ToLog[A] = Show[A].show(_) + given showToLog[A: Show]: ToLog[A] with { + inline def toLogMessage(a: A): String = Show[A].show(a) + } } object show extends show diff --git a/modules/logger-f-core/shared/src/main/scala-3/loggerf/core/ToLog.scala b/modules/logger-f-core/shared/src/main/scala-3/loggerf/core/ToLog.scala index aeba91a0..68d22a48 100644 --- a/modules/logger-f-core/shared/src/main/scala-3/loggerf/core/ToLog.scala +++ b/modules/logger-f-core/shared/src/main/scala-3/loggerf/core/ToLog.scala @@ -15,5 +15,7 @@ object ToLog { @SuppressWarnings(Array("org.wartremover.warts.ToString")) def fromToString[A]: ToLog[A] = _.toString - inline given stringToLog: ToLog[String] = identity(_) + given stringToLog: ToLog[String] with { + inline def toLogMessage(a: String): String = a + } } diff --git a/modules/logger-f-doobie1/shared/src/main/scala/loggerf/doobie1/LoggerFLogHandler.scala b/modules/logger-f-doobie1/shared/src/main/scala/loggerf/doobie1/LoggerFLogHandler.scala new file mode 100644 index 00000000..b1ee7cc8 --- /dev/null +++ b/modules/logger-f-doobie1/shared/src/main/scala/loggerf/doobie1/LoggerFLogHandler.scala @@ -0,0 +1,58 @@ +package loggerf.doobie1 + +import cats.syntax.all._ +import doobie.util.log +import doobie.util.log.LogHandler +import loggerf.core.Log +import loggerf.syntax.all._ + +/** @author Kevin Lee + * @since 2023-07-28 + */ +object LoggerFLogHandler { + def apply[F[*]: Log]: LogHandler[F] = new LoggerFLogHandlerF[F] + + // format: off + final private class LoggerFLogHandlerF[F[*]: Log] extends LogHandler[F] { + override def run(logEvent: log.LogEvent): F[Unit] = logEvent match { + case log.Success(sql, args, label, e1, e2) => + logS_( + show"""Successful Statement Execution: + | + | ${sql.linesIterator.dropWhile(_.trim.isEmpty).mkString("\n ")} + | + | arguments = [${args.mkString(", ")}] + | label = $label + | elapsed = ${e1.toMillis} ms exec + ${e2.toMillis} ms processing (${(e1 + e2).toMillis} ms total) + |""".stripMargin + )(info) + + case log.ProcessingFailure(sql, args, label, e1, e2, failure) => + logS_( + show"""Failed Resultset Processing: + | + | ${sql.linesIterator.dropWhile(_.trim.isEmpty).mkString("\n ")} + | + | arguments = [${args.mkString(", ")}] + | label = $label + | elapsed = ${e1.toMillis} ms exec + ${e2.toMillis} ms processing (failed) (${(e1 + e2).toMillis} ms total) + | failure = ${failure.getMessage} + |""".stripMargin + )(error) + + case log.ExecFailure(sql, args, label, e1, failure) => + logS_( + show"""Failed Statement Execution: + | + | ${sql.linesIterator.dropWhile(_.trim.isEmpty).mkString("\n ")} + | + | arguments = [${args.mkString(", ")}] + | label = $label + | elapsed = ${e1.toMillis} ms exec (failed) + | failure = ${failure.getMessage} + |""".stripMargin + )(error) + } + } + // format: on +} diff --git a/modules/logger-f-doobie1/shared/src/test/scala/loggerf/doobie1/LoggerFLogHandlerSpec.scala b/modules/logger-f-doobie1/shared/src/test/scala/loggerf/doobie1/LoggerFLogHandlerSpec.scala new file mode 100644 index 00000000..02c6ab2b --- /dev/null +++ b/modules/logger-f-doobie1/shared/src/test/scala/loggerf/doobie1/LoggerFLogHandlerSpec.scala @@ -0,0 +1,166 @@ +package loggerf.doobie1 + +import cats.effect._ +import doobie.util.log.{ExecFailure, ProcessingFailure, Success} +import effectie.instances.ce3.fx.ioFx +import extras.hedgehog.ce3.syntax.runner._ +import hedgehog._ +import hedgehog.runner._ +import loggerf.instances.cats.logF +import loggerf.testing.CanLog4Testing +import loggerf.testing.CanLog4Testing.OrderedMessages + +import scala.concurrent.duration._ + +/** @author Kevin Lee + * @since 2023-07-29 + */ +object LoggerFLogHandlerSpec extends Properties { + + override def tests: List[Prop] = List( + property("testSuccess", testSuccess), + property("testExecFailure", testExecFailure), + property("testProcessingFailure", testProcessingFailure), + ) + + def testSuccess: Property = + for { + columns <- Gen.string(Gen.alpha, Range.linear(3, 10)).list(Range.linear(1, 5)).log("columns") + table <- Gen.string(Gen.alpha, Range.linear(3, 10)).log("table") + args <- Gen.string(Gen.alpha, Range.linear(3, 10)).list(Range.linear(0, 5)).log("args") + label <- Gen.string(Gen.alpha, Range.linear(3, 10)).log("label") + exec <- Gen.int(Range.linear(10, 3000)).log("exec") + processing <- Gen.int(Range.linear(10, 3000)).log("processing") + } yield runIO { + implicit val canLog: CanLog4Testing = CanLog4Testing() + + val expected = + OrderedMessages( + Vector( + ( + 0, + loggerf.Level.info, + s"""Successful Statement Execution: + | + | SELECT ${columns.mkString(", ")} FROM $table + | + | arguments = ${args.mkString("[", ", ", "]")} + | label = $label + | elapsed = ${exec.toString} ms exec + ${processing.toString} ms processing (${(exec + processing).toString} ms total) + |""".stripMargin, + ) + ) + ) + + LoggerFLogHandler[IO] + .run( + Success( + s"SELECT ${columns.mkString(", ")} FROM $table", + args, + label, + exec.milliseconds, + processing.milliseconds, + ) + ) *> IO { + val actual = canLog.getOrderedMessages + actual ==== expected + } + } + + def testExecFailure: Property = + for { + columns <- Gen.string(Gen.alpha, Range.linear(3, 10)).list(Range.linear(1, 5)).log("columns") + table <- Gen.string(Gen.alpha, Range.linear(3, 10)).log("table") + args <- Gen.string(Gen.alpha, Range.linear(3, 10)).list(Range.linear(0, 5)).log("args") + label <- Gen.string(Gen.alpha, Range.linear(3, 10)).log("label") + exec <- Gen.int(Range.linear(10, 3000)).log("exec") + errMessage <- Gen.string(Gen.alpha, Range.linear(3, 10)).log("errMessage") + } yield runIO { + implicit val canLog: CanLog4Testing = CanLog4Testing() + + val expectedException = new RuntimeException(errMessage) + + val expected = + OrderedMessages( + Vector( + ( + 0, + loggerf.Level.error, + s"""Failed Statement Execution: + | + | SELECT ${columns.mkString(", ")} FROM $table + | + | arguments = ${args.mkString("[", ", ", "]")} + | label = $label + | elapsed = ${exec.toString} ms exec (failed) + | failure = ${expectedException.getMessage} + |""".stripMargin, + ) + ) + ) + + LoggerFLogHandler[IO] + .run( + ExecFailure( + s"SELECT ${columns.mkString(", ")} FROM $table", + args, + label, + exec.milliseconds, + expectedException, + ) + ) *> IO { + val actual = canLog.getOrderedMessages + actual ==== expected + } + } + + def testProcessingFailure: Property = + for { + columns <- Gen.string(Gen.alpha, Range.linear(3, 10)).list(Range.linear(1, 5)).log("columns") + table <- Gen.string(Gen.alpha, Range.linear(3, 10)).log("table") + args <- Gen.string(Gen.alpha, Range.linear(3, 10)).list(Range.linear(0, 5)).log("args") + label <- Gen.string(Gen.alpha, Range.linear(3, 10)).log("label") + exec <- Gen.int(Range.linear(10, 3000)).log("exec") + processing <- Gen.int(Range.linear(10, 3000)).log("processing") + errMessage <- Gen.string(Gen.alpha, Range.linear(3, 10)).log("errMessage") + } yield runIO { + implicit val canLog: CanLog4Testing = CanLog4Testing() + + val expectedException = new RuntimeException(errMessage) + + val expected = + OrderedMessages( + Vector( + ( + 0, + loggerf.Level.error, + s"""Failed Resultset Processing: + | + | SELECT ${columns.mkString(", ")} FROM $table + | + | arguments = ${args.mkString("[", ", ", "]")} + | label = $label + | elapsed = ${exec.toString} ms exec + ${processing.toString} ms processing (failed) (${(exec + processing).toString} ms total) + | failure = ${expectedException.getMessage} + |""".stripMargin, + ) + ) + ) + + LoggerFLogHandler[IO] + .run( + ProcessingFailure( + s"SELECT ${columns.mkString(", ")} FROM $table", + args, + label, + exec.milliseconds, + processing.milliseconds, + expectedException, + ) + ) *> IO { + val actual = canLog.getOrderedMessages + actual ==== expected + } + } + +} diff --git a/modules/logger-f-logback-mdc-monix3/shared/src/main/scala/loggerf/logger/logback/Monix3MdcAdapter.scala b/modules/logger-f-logback-mdc-monix3/shared/src/main/scala/loggerf/logger/logback/Monix3MdcAdapter.scala index 00ae59af..be0d921a 100644 --- a/modules/logger-f-logback-mdc-monix3/shared/src/main/scala/loggerf/logger/logback/Monix3MdcAdapter.scala +++ b/modules/logger-f-logback-mdc-monix3/shared/src/main/scala/loggerf/logger/logback/Monix3MdcAdapter.scala @@ -44,18 +44,18 @@ object Monix3MdcAdapter extends Monix3MdcAdapterOps trait Monix3MdcAdapterOps { - @SuppressWarnings(Array("org.wartremover.warts.Null")) + @SuppressWarnings(Array("org.wartremover.warts.Null", "scalafix:DisableSyntax.null")) protected def initialize0(monix3MdcAdapter: Monix3MdcAdapter): Monix3MdcAdapter = { val field = classOf[MDC].getDeclaredField("mdcAdapter") field.setAccessible(true) - field.set(null, monix3MdcAdapter) // scalafix:ok DisableSyntax.null + field.set(null, monix3MdcAdapter) field.setAccessible(false) monix3MdcAdapter } - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) + @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf", "scalafix:DisableSyntax.asInstanceOf")) protected def getLoggerContext(): LoggerContext = - LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] // scalafix:ok DisableSyntax.asInstanceOf + LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] def initialize(): Monix3MdcAdapter = initializeWithMonix3MdcAdapterAndLoggerContext(new Monix3MdcAdapter, getLoggerContext()) diff --git a/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala b/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala index a420484e..c8decd75 100644 --- a/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala +++ b/modules/logger-f-logback-mdc-monix3/shared/src/test/scala/loggerf/logger/logback/Monix3MdcAdapterSpec.scala @@ -249,7 +249,7 @@ object Monix3MdcAdapterSpec extends Properties { .runSyncUnsafe() } - @SuppressWarnings(Array("org.wartremover.warts.Null")) + @SuppressWarnings(Array("org.wartremover.warts.Null", "scalafix:DisableSyntax.null")) def testRemoveMultipleIsolatedNestedModifications: Property = for { a <- Gen.string(Gen.alpha, Range.linear(1, 10)).map("1:" + _).log("a") @@ -286,12 +286,12 @@ object Monix3MdcAdapterSpec extends Properties { for { isolated2Key1Before <- Task(MDC.get("key-1")).map(_ ==== a) isolated2Key2Before <- - Task(MDC.get("key-2")).map(_ ==== null) // scalafix:ok DisableSyntax.null + Task(MDC.get("key-2")).map(_ ==== null) _ <- Task(MDC.put("key-2", c)) isolated2Key2After <- Task(MDC.get("key-2")).map(_ ==== c) _ <- Task(MDC.remove("key-2")) isolated2Key2AfterRemove <- - Task(MDC.get("key-2")).map(_ ==== null) // scalafix:ok DisableSyntax.null + Task(MDC.get("key-2")).map(_ ==== null) } yield ( isolated2Key1Before, isolated2Key2Before, @@ -303,11 +303,11 @@ object Monix3MdcAdapterSpec extends Properties { (isolated2Key1Before, isolated2Key2Before, isolated2Key2After, isolated2Key2AfterRemove) = isolated3 key1After = (MDC.get("key-1") ==== a).log(s"""After: MDC.get("key-1") is not $a""") key2After = - (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null""") // scalafix:ok DisableSyntax.null + (MDC.get("key-2") ==== null).log("""After: MDC.get("key-2") is not null""") _ <- Task(MDC.remove("key-1")) key1AfterRemove = (MDC.get("key-1") ==== null) - .log("""After Remove: MDC.get("key-1") is not null""") // scalafix:ok DisableSyntax.null + .log("""After Remove: MDC.get("key-1") is not null""") } yield Result.all( List( before.log("before"), diff --git a/project/plugins.sbt b/project/plugins.sbt index 9da7e607..26e232e9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,18 +2,18 @@ logLevel := sbt.Level.Warn addDependencyTreePlugin -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") -addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.4.15") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") +addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.1.3") //addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.0.5") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.4") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.6") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.8") addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.2") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.2") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") addSbtPlugin("io.kevinlee" % "sbt-docusaur" % "0.15.0") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") val sbtDevOopsVersion = "3.0.0" addSbtPlugin("io.kevinlee" % "sbt-devoops-scala" % sbtDevOopsVersion)